Bug fix: SPI TFT display (ILI9486) shows nothing with pygame 1.9.6 + SDL_VIDEODRIVER=dummy
Setup: Raspberry Pi running TinyProgrammer with a GoodTFT 3.5" SPI LCD (ILI9486 controller,
/dev/fb2), using DISPLAY_PROFILE=pizero-spi (480x320).
SYMPTOM
The service runs normally (systemctl status: active, web dashboard at :5000 works, BBS
breaks and LLM code generation all function). Writing random noise to /dev/fb2 via dd
shows correctly on the screen. But TinyProgrammer itself never displays anything — the
screen stays black. Reading the first bytes of /dev/fb2 shows all 0x00.
ROOT CAUSE
pygame 1.9.6 initialises pygame.Surface objects in an 8-bit palette format when
SDL_VIDEODRIVER=dummy is set and pygame.display.set_mode() has not been called first.
In this mode all color channel masks are 0 and all loss values are 8, meaning the surface
has no usable RGB channels. Every fill(), blit(), and image.load() silently produces black
pixels regardless of what color is requested. The FramebufferWriter in display/framebuffer.py
was working correctly — it was faithfully writing 307,200 bytes of pure black to /dev/fb2
on every frame with no errors.
Confirmed by inspecting a live surface:
bit size: 8
masks: (0, 0, 0, 0)
losses: (8, 8, 8, 8)
fill((0, 255, 0)) -> get_at((0,0)): (0, 0, 0, 255) <-- all black
FIX
In display/terminal.py, inside the _init_display() method, add one line immediately
before the pygame.Surface creation:
# Before (broken):
pygame.mouse.set_visible(False)
self.screen = pygame.Surface((self.width, self.height))
# After (fixed):
pygame.mouse.set_visible(False)
pygame.display.set_mode((self.width, self.height), 0, 32) # <-- add this
self.screen = pygame.Surface((self.width, self.height))
Calling pygame.display.set_mode() with depth=32 forces pygame to initialise its internal
pixel format to 32-bit RGBA before any Surface objects are created. Subsequent Surface()
calls inherit this format and have correct RGB masks, making fill(), blit(), surfarray,
and image.load() all work as expected.
After the fix:
bit size: 32
masks: (16711680, 65280, 255, 0)
fill((0, 255, 0)) -> get_at((0,0)): (0, 255, 0, 255) <-- correct
The dummy driver still handles all display output via FramebufferWriter writing to
/dev/fb2 directly — pygame.display.flip() is never called. The set_mode() call here
is used only to establish the correct pixel format, not to open a real display window.
AFFECTED VERSIONS
Confirmed on pygame 1.9.6 (Ubuntu 20.04 / Raspberry Pi). May also affect other old
pygame versions on headless/framebuffer-only setups. Newer pygame versions (2.x) may
handle this differently.
Bug fix: SPI TFT display (ILI9486) shows nothing with pygame 1.9.6 + SDL_VIDEODRIVER=dummy
Setup: Raspberry Pi running TinyProgrammer with a GoodTFT 3.5" SPI LCD (ILI9486 controller,
/dev/fb2), using DISPLAY_PROFILE=pizero-spi (480x320).
SYMPTOM
The service runs normally (systemctl status: active, web dashboard at :5000 works, BBS
breaks and LLM code generation all function). Writing random noise to /dev/fb2 via dd
shows correctly on the screen. But TinyProgrammer itself never displays anything — the
screen stays black. Reading the first bytes of /dev/fb2 shows all 0x00.
ROOT CAUSE
pygame 1.9.6 initialises pygame.Surface objects in an 8-bit palette format when
SDL_VIDEODRIVER=dummy is set and pygame.display.set_mode() has not been called first.
In this mode all color channel masks are 0 and all loss values are 8, meaning the surface
has no usable RGB channels. Every fill(), blit(), and image.load() silently produces black
pixels regardless of what color is requested. The FramebufferWriter in display/framebuffer.py
was working correctly — it was faithfully writing 307,200 bytes of pure black to /dev/fb2
on every frame with no errors.
Confirmed by inspecting a live surface:
bit size: 8
masks: (0, 0, 0, 0)
losses: (8, 8, 8, 8)
fill((0, 255, 0)) -> get_at((0,0)): (0, 0, 0, 255) <-- all black
FIX
In display/terminal.py, inside the _init_display() method, add one line immediately
before the pygame.Surface creation:
Calling pygame.display.set_mode() with depth=32 forces pygame to initialise its internal
pixel format to 32-bit RGBA before any Surface objects are created. Subsequent Surface()
calls inherit this format and have correct RGB masks, making fill(), blit(), surfarray,
and image.load() all work as expected.
After the fix:
bit size: 32
masks: (16711680, 65280, 255, 0)
fill((0, 255, 0)) -> get_at((0,0)): (0, 255, 0, 255) <-- correct
The dummy driver still handles all display output via FramebufferWriter writing to
/dev/fb2 directly — pygame.display.flip() is never called. The set_mode() call here
is used only to establish the correct pixel format, not to open a real display window.
AFFECTED VERSIONS
Confirmed on pygame 1.9.6 (Ubuntu 20.04 / Raspberry Pi). May also affect other old
pygame versions on headless/framebuffer-only setups. Newer pygame versions (2.x) may
handle this differently.