RC: W4 D5 — Two techniques to remove flickering when printing on the terminal

March 8, 2024

Today, I came back on the little game of life that Ludwig and I had implemented a few weeks ago because there was one thing was not very satisfying about it and I thought it was time to solve it.

Conway’s game of life is a cellular automation that evolves based on some simple rules which state whether a given cell should be live or dead at the next iteration, depending on the current state of its neighbors. After implementing the rules that allow to generate a new grid based on the previous one, we wrote the following to display each new grid on the terminal:

import os, sys
from time import sleep

Grid = list[list[str]]


def initialize_grid() -> Grid:
    ...


def apply_rules(grid: Grid) -> Grid:
    ...


def display(grid: Grid):
    for x in range(len(grid)):
        for y in range(len(grid[x])):
            print("█" if grid[x][y] else " ", end="")
        print("")


if __name__ == '__main__':
    grid = initialize_grid()

    while True:
        # Compute new grid
        new_grid = apply_rules(grid)

        # Display new grid
        os.system("clear")
        display(new_grid)
        sys.stdout.flush()

        # Wait before running next iteration
        sleep(1 / 30.0)
        grid = new_grid

So at each iteration, we compute the new grid, call os.system("clear") to clear the terminal, display the new grid and wait one 30th of a second to execute the next iteration (so that the result is visible on the screen).

This worked well. BUT: the terminal was sometimes flickering, which was kind of annoying. In the recording below, if you focus on the two crosses that are in the center-right part of the screen, you can see the stutter at least 3 times (they fully disappear and reappear when they have no reason to).

I wanted to understand why and figured that there were two problems with the way we had implemented it.

The first one is the fact that we displayed the grid line by line (display function). Instead, we can prepare the entire content in a single string and then print it all at once. This technique is called double buffering: it holds the block of data that is being created in a buffer (here, the string containing the full grid content) and only display it once it is complete. This allows to read the complete block of data (the full grid) instead of a partially written one (the first lines of the grid), and thus helps to prevent stutter.

Using this technique, the display function becomes:

def display(grid):
    output = '\n'.join(''.join("█" if grid[x][y] else " "
                               for y in range(len(grid[x])))
                       for x in range(len(grid)))
    print(output)

The second problem was the way we cleared the screen: we used os.system("clear") which is a heavy operation. Under the hood, here is what this instruction does:

So, this introduces some overhead to start the process, execute the command and clean up once the process is finished, and thus partly explains why the screen stutters.

Instead, to be more efficient, we can do the following:

def clear_screen():
    print("\033[H\033[J", end='')

This cryptic command tells the terminal to move the cursor to the top-left part of the screen (\033[H) and to clear the screen from that position until the end of the screen (\033[J).

Combining these two changes, here is the result in action:

No more flickering!! 🍾