Skip to content

fix(viewer): wait for /dev/fb0 instead of crash-looping when headless - #2959

Merged
vpetersson merged 1 commit into
masterfrom
fix/viewer-wait-for-framebuffer-headless
May 31, 2026
Merged

fix(viewer): wait for /dev/fb0 instead of crash-looping when headless#2959
vpetersson merged 1 commit into
masterfrom
fix/viewer-wait-for-framebuffer-headless

Conversation

@vpetersson

@vpetersson vpetersson commented May 31, 2026

Copy link
Copy Markdown
Contributor

Draft — logic + behaviour validated on-device (see Validation);
leaving as draft pending a build/deploy of the pi3 viewer image with
this change.

Issues Fixed

Headless / display-off 32-bit Pi (pi2/pi3) devices crash-loop the viewer
and show nothing. Diagnosed live on the pi3 fleet.

Qt's linuxfb platform opens /dev/fb0 at startup and can't recover if
it's absent. Under full KMS (dtoverlay=vc4-kms-v3d) the framebuffer
only exists while a display is connected, so a device that boots with no
panel attached — display powered off, TV switched on after the Pi, slow
HDMI handshake, panel swapped — has no /dev/fb0. Qt then doesn't fail
cleanly: it logs Unable to figure out framebuffer device /
no screens available and aborts with heap corruption (malloc(): unaligned tcache chunk detected), so the container crash-loops.

This is not a boot-time race. When a display is present at boot,
/dev/fb0 is created by the kernel at driver-bind (~t=23 s), ~30 s
before the container engine even starts — it's always there in time. The
problem is purely the display-absent-at-boot case, and crucially the
framebuffer is dynamic: the vc4 fbdev layer creates /dev/fb0 when a
connector transitions to connected after boot (verified — see below).

Description

Add wait_for_framebuffer() to bin/start_viewer.sh: on the linuxfb
path, if /dev/fb0 is missing, wait for it instead of launching into a
guaranteed heap-corruption crash. When a display is attached/powered on,
the KMS driver creates /dev/fb0 and the viewer starts immediately —
replacing a noisy crash-loop (which only recovers on a later restart that
happens to land after the display appears) with a quiet idle that
recovers the instant the framebuffer exists.

Makes no assumptions about which connector the display is on or its
resolution. Guarded on QT_QPA_PLATFORM=linuxfb, so it's a no-op on the
cage (wayland) and eglfs (KMS) boards, which render straight to
/dev/dri and have no /dev/fb0 dependency.

Companion to the UID/GID-pinning fix (independent). The analogous
"headless" behaviour on the eglfs/cage boards is a separate concern, out
of scope here.

Validation (on live pi3 hardware)

  • No boot race: on a device with a display, dmesg shows
    vc4drmfb fb0 created at monotonic t≈23 s; balena.service (the
    container engine) starts at t≈53 s. fb0 is present ~30 s before any
    container.
  • fb0 is dynamic on connect: on a headless device (connector
    disconnected, no /dev/fb0), forcing the connector connected
    (echo on > /sys/class/drm/card0-HDMI-A-1/status) created /dev/fb0
    at t≈1074 s post-boot (dmesg: vc4drmfb fb0). So a display
    attached after boot does make the framebuffer appear — which is exactly
    what the wait recovers on.
  • TODO before un-drafting: build the pi3 viewer image on this branch,
    deploy to a headless device, confirm it idles on the new log line and
    starts the viewer when a display is connected.

Checklist

  • I have performed a self-review of my own code.
  • New and existing unit tests pass locally and on CI with my changes.
  • I have done an end-to-end test for Raspberry Pi devices.
  • I have tested my changes for x86 devices.
  • I added a documentation for the changes I have made (when necessary).

- On the linuxfb path (pi2/pi3) Qt aborts with heap corruption
  ("malloc(): unaligned tcache chunk detected") when /dev/fb0 is absent,
  crash-looping the container and spamming the logs.
- Under full KMS (vc4-kms-v3d) the framebuffer only exists while a
  display is connected, so headless / powered-off / slow-HDMI boards
  have none.
- Add wait_for_framebuffer() to start_viewer.sh: on the linuxfb path,
  wait for /dev/fb0 before launching the viewer, self-healing when a
  display is (re)connected. Guarded on QT_QPA_PLATFORM=linuxfb, so it's
  a no-op on the cage (wayland) and eglfs (KMS) boards.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@vpetersson vpetersson self-assigned this May 31, 2026
@sonarqubecloud

Copy link
Copy Markdown

@vpetersson
vpetersson marked this pull request as ready for review May 31, 2026 11:35
@vpetersson
vpetersson requested a review from a team as a code owner May 31, 2026 11:35
@vpetersson
vpetersson merged commit 20008b7 into master May 31, 2026
7 checks passed
vpetersson added a commit that referenced this pull request Jun 1, 2026
…esn't crash-loop (#2962)

Pi 4 renders under QT_QPA_PLATFORM=eglfs (since #2904), but the
headless-display guard added in #2959 only covers the linuxfb /dev/fb0
path (pi2/pi3). On a headless eglfs board Qt's eglfs plugin finds no
connected DRM connector, logs "no screens available", and exits before
the viewer emits its D-Bus handshake — so load_browser() raises and the
viewer container crash-loops. Confirmed on 4 live pi4 devices running
2026.5.2.

- Add wait_for_eglfs_display(): before the KMS-card detection, wait
  until a DRM connector reads "connected" (or "unknown"); a genuinely
  headless board idles quietly and self-heals on hotplug, mirroring the
  linuxfb /dev/fb0 wait.
- Correct stale comments that still claimed Pi 4 runs under linuxfb.

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
vpetersson added a commit that referenced this pull request Jul 7, 2026
- exit wait_for_framebuffer when the host has fb0 but the container's
  stale /dev doesn't, so `restart: always` re-snapshots /dev
- keep waiting quietly when genuinely headless (no sysfs fb0) to avoid
  the #2959 crash-loop
- fixes the "self-heals on hotplug" case that a running container's
  devtmpfs snapshot silently broke

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
vpetersson added a commit that referenced this pull request Jul 7, 2026
* fix(viewer): re-assert linuxfb display mode after HDMI hotplug

On the 32-bit Pi 1-3 (linuxfb/Qt5) viewer, a TV on a power schedule
that switches itself off and on can leave the display stuck at
1024x768 instead of its native resolution (issue #3052).

Root cause is kernel-side, not anything Anthias writes: under
dtoverlay=vc4-kms-v3d the linuxfb boards never take DRM master, so the
kernel's drm_fb_helper owns the display mode. When the HDMI sink wakes,
the connector re-probe can win the race against the sink's EDID/DDC
coming back; the connector momentarily reports no valid modes and
drm_fb_helper latches its hard-coded 1024x768 default. Qt's linuxfb
plugin reads the framebuffer geometry once at startup and can't follow
the change, so the picture stays stuck until the Pi is power-cycled.

eglfs boards (pi4 / pi5 / pi3-64 / arm64) are immune: Qt holds DRM
master and keeps its own modeset committed across the hotplug. Verified
on a Pi 3-64 testbed that a real ~10 s HDMI unplug never left 1920x1080.

Fix: a linuxfb-only watchdog in start_viewer.sh watches the HDMI
connector for a disconnect->reconnect and, once EDID is readable again,
re-asserts the connector's *preferred* mode (read live from the
connector, never hard-coded, so any panel resolution is honoured) onto
the framebuffer via the fbdev sysfs `mode` attribute, then restarts the
viewer so Qt re-initialises against the restored mode. All access is
under /sys in the already-privileged viewer container; no DRM master is
taken, so it never conflicts with Qt's fbdev use. The QT_QPA_PLATFORM
guard makes it a no-op on eglfs/wayland boards.

Validated on a real Pi 3: forcing the connector through a
disconnect -> 1024x768 -> reconnect cycle, the watchdog re-asserted
1920x1080 and restarted the viewer. (The 1024x768 latch was simulated
with fbset because the testbed monitor presents EDID instantly and
can't reproduce the real no-EDID race; detection and recovery are
identical regardless of how the mode was latched.)

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix(viewer): restart to recover /dev/fb0 hotplugged after headless boot

- exit wait_for_framebuffer when the host has fb0 but the container's
  stale /dev doesn't, so `restart: always` re-snapshots /dev
- keep waiting quietly when genuinely headless (no sysfs fb0) to avoid
  the #2959 crash-loop
- fixes the "self-heals on hotplug" case that a running container's
  devtmpfs snapshot silently broke

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix(viewer): harden HDMI-hotplug resolution recovery (review follow-ups)

- preserve interlaced scan type instead of forcing a progressive mode
  string (interlaced-native sinks were never matched)
- log rejected /sys .../fb0/mode writes instead of returning silently,
  so a screen stuck at 1024x768 is diagnosable
- watch every HDMI connector, seeded from current status, instead of
  binding one at startup (second micro-HDMI port / late connect)
- normalize `pidof python` to a single PID so a multi-PID result can't
  disable the watchdog (also fixes the pre-existing kill -0 loop)

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix(viewer): address second-pass review nits on HDMI-hotplug recovery

- wait_for_framebuffer: sleep+re-check before the stale-devtmpfs exit so
  a /dev/fb0 node that lags its sysfs entry on a fresh start can't cause
  restart churn
- monitor: default an unseeded connector to 'connected' so an
  unreadable-status seed race can't look like a reconnect edge and
  spuriously restart a display that never dropped
- PID: select the oldest pidof result (the viewer, launched before any
  gst_fbdev_player.py helper) instead of the newest

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant