Skip to content

Blank screen fix for GoodTFT 3.5" SPI LCD (Ubuntu 20.04 ILI9486 controller, /dev/fb2) #30

Description

@RowanUnderwood

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions