How to Prevent Flickering When Drawing Directly to the Linux Framebuffer in Python
Rendering graphics directly to the Linux framebuffer (/dev/fb0) is a popular, lightweight way to build kiosks, embedded dashboards, or UI tools without the overhead of X11 or Wayland. However, developers frequently encounter a common issue: severe screen flickering or tearing.
If your graphics appear to strobe or blink intermittently, you are running into a classic display timing problem. Here is an in-depth breakdown of why this happens and how to fix it in Python.
Why Does the Framebuffer Flicker?
In your rendering loop, two operations run continuously in a tight cycle:
while True:
blank(fb) # Fills the buffer with black
fill_rectangle(fb, ...) # Draws the red rectangle
Your monitor's display controller refreshes the physical screen at a fixed rate (typically 60Hz), reading raw pixel bytes line by line directly from the framebuffer memory mapped in fb.
Because your script writes directly to the active front buffer while the display controller reads from it, race conditions occur:
- Mid-scan clearance: The display hardware scans the screen while
blank(fb)is executing. This sends a blank black screen to your monitor. - Tearing & Stutter: The next scan catches the screen halfway through
fill_rectangle(), or after drawing finishes. - Unthrottled loop: Because there is no rate limiting, your CPU loop alternates between black and red thousands of times per second asynchronously with your monitor's refresh clock.
Solution 1: Use an In-Memory Back Buffer (Quickest Fix)
The simplest way to eliminate the flicker caused by blank() is to draw your entire frame into a local Python bytearray (an off-screen back buffer) and write to fb in a single, atomic-like memory copy.
Instead of wiping the visible screen and slowly redrawing rectangles directly onto hardware memory, do your clearing and drawing in RAM, then blit the final image to the framebuffer:
import mmap
import sys
import time
fbN = 'fb0'
# Read virtual resolution
with open(f"/sys/class/graphics/{fbN}/virtual_size", "r") as f:
w, h = map(int, f.read().strip().split(','))
# Read bits per pixel
with open(f"/sys/class/graphics/{fbN}/bits_per_pixel", "r") as f:
bpp = int(f.read().strip())
bytes_per_pixel = bpp // 8
screen_size = w * h * bytes_per_pixel
# Open and map the framebuffer
fbdev = open(f"/dev/{fbN}", mode='r+b')
fb = mmap.mmap(fbdev.fileno(), screen_size, mmap.MAP_SHARED, mmap.PROT_WRITE | mmap.PROT_READ)
# Create an off-screen buffer in system memory
offscreen_buffer = bytearray(screen_size)
def fill_rectangle(buf, width, x1, y1, x2, y2, color_bytes):
row = color_bytes * (x2 - x1 + 1)
row_size = len(row)
for y in range(y1, y2 + 1):
pos = (y * width + x1) * len(color_bytes)
buf[pos:pos + row_size] = row
red = bytes((0, 0, 255, 255)) if bpp == 32 else bytes((0x00, 0xF8))
try:
while True:
# 1. Clear off-screen buffer
offscreen_buffer[:] = b'\x00' * screen_size
# 2. Draw shapes into off-screen buffer
fill_rectangle(offscreen_buffer, w, 100, 50, 300, 200, red)
# 3. Copy whole frame at once to visible framebuffer
fb[:] = offscreen_buffer
# 4. Cap framerate (e.g., ~60 FPS) to reduce CPU burn
time.sleep(1 / 60)
finally:
fb.close()
fbdev.close()
Solution 2: Hardware Double Buffering with Page Flipping
While an offscreen RAM buffer stops the visible blanking effect, horizontal screen tearing may still happen if the monitor reads a frame while fb[:] = offscreen_buffer is running.
The standard solution in graphics programming is Hardware Double Buffering. This involves allocating a virtual framebuffer twice the display height (yres_virtual = yres * 2) and alternating the display pointer (page flipping) using the FBIOPAN_DISPLAY ioctl.
How Double Buffering Works
- Allocate a buffer large enough for two frames (Buffer A at vertical offset
0, Buffer B at vertical offsetheight). - While the screen displays Buffer A, draw to Buffer B.
- Call
FBIOPAN_DISPLAYto switch the screen view to Buffer B. - Draw the next frame into Buffer A, and flip back.
import fcntl
import struct
# ioctl constants for Linux framebuffer
FBIOGET_VSCREENINFO = 0x4600
FBIOPUT_VSCREENINFO = 0x4601
FBIOPAN_DISPLAY = 0x4606
FBIO_WAITFORVSYNC = 0x40044620
# Struct format for fb_var_screeninfo
# (xres, yres, xres_virtual, yres_virtual, xoffset, yoffset, bits_per_pixel, ...)
FB_VAR_FORMAT = 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIII'
def pan_display(fbdev_fd, vinfo, y_offset):
# Update yoffset and trigger pan
vinfo_list = list(struct.unpack(FB_VAR_FORMAT, vinfo))
vinfo_list[5] = y_offset # Set yoffset
new_vinfo = struct.pack(FB_VAR_FORMAT, *vinfo_list)
fcntl.ioctl(fbdev_fd, FBIOPAN_DISPLAY, new_vinfo)
Coupled with ioctl(fbdev.fileno(), FBIO_WAITFORVSYNC, 0), page flipping ensures the screen flips exclusively during the vertical blank interval (VSync), eliminating all tearing and flickering.
Important Best Practices
- Do not redraw static screens: If your graphics do not change every frame, do not clear and redraw inside a continuous
while Trueloop. Render once and wait for user input or state updates. - Consider DRM/KMS: The legacy Linux framebuffer (
fbdev) is deprecated in modern Linux kernels in favor of DRM/KMS (Direct Rendering Manager / Kernel Mode Setting). For complex projects, consider libraries likepykmsor SDL2 (which can target KMS/DRM directly without X11 or Wayland) for hardware-accelerated, tear-free rendering.