From 785e16ab0fd69bf7219ac3550c47d5c282bf6ba3 Mon Sep 17 00:00:00 2001 From: Viktor Petersson Date: Fri, 12 Jun 2026 12:42:10 +0000 Subject: [PATCH] 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) --- bin/start_viewer.sh | 98 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/bin/start_viewer.sh b/bin/start_viewer.sh index e2a082421..a49d36a71 100755 --- a/bin/start_viewer.sh +++ b/bin/start_viewer.sh @@ -476,6 +476,99 @@ wait_for_framebuffer() { } wait_for_framebuffer +# === HDMI hotplug resolution recovery (linuxfb / Pi 1-3) === +# +# Under dtoverlay=vc4-kms-v3d the kernel's drm_fb_helper owns the +# display mode on the linuxfb boards: Qt's linuxfb plugin only paints +# /dev/fb0 and never takes DRM master, so nothing in userspace re-runs a +# modeset. When an HDMI sink on a power schedule (a TV that switches +# itself off and on) wakes up, 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 the framebuffer to +# its hard-coded 1024x768 fallback. Qt read the framebuffer geometry +# once at startup and can't follow the change, so the picture stays +# stuck at 1024x768 until someone power-cycles the Pi (issue #3052). +# +# eglfs boards (pi4 / pi5 / pi3-64 / arm64) are immune: Qt holds DRM +# master and keeps its own modeset committed across the hotplug, so they +# hold the resolution through a TV power-cycle (verified on a Pi 3-64 +# testbed: a real ~10 s HDMI unplug never left 1920x1080). The +# QT_QPA_PLATFORM guard below makes the watchdog a no-op for them. +# +# Recover by watching the HDMI connector for a disconnect->reconnect +# and, once its EDID is readable again, re-asserting the connector's +# *preferred* mode onto the framebuffer through the fbdev sysfs `mode` +# attribute. The target mode is read live from the connector, never +# hard-coded, so any panel resolution is honoured. The viewer is then +# restarted (the wait loop below exits, the container's restart policy +# brings it back) so Qt re-initialises cleanly against the restored +# mode. All reads/writes are under /sys — the viewer container is +# privileged and no DRM master is taken, so this never conflicts with +# Qt's fbdev use. +HOTPLUG_SETTLE_SECONDS="${ANTHIAS_HOTPLUG_SETTLE_SECONDS:-5}" + +find_connected_hdmi() { + # Echo the sysfs dir of the first connected HDMI connector, if any. + local status_file + for status_file in /sys/class/drm/card*-HDMI*/status; do + [ -r "$status_file" ] || continue + [ "$(cat "$status_file" 2>/dev/null)" = 'connected' ] || continue + dirname "$status_file" + return 0 + done + return 1 +} + +reassert_preferred_mode() { + # Re-apply the connector's EDID-preferred mode to /dev/fb0. + # $1 = connector sysfs dir. Returns non-zero (so the caller retries + # on the next reconnect tick) while the sink hasn't published EDID. + local conn="$1" preferred width height mode + preferred=$(head -n1 "$conn/modes" 2>/dev/null) + case "$preferred" in + *x*) ;; + *) return 1 ;; # no modes yet — EDID still negotiating + esac + width=${preferred%%x*} + height=${preferred#*x} + height=${height%%[!0-9]*} # drop a trailing 'i' on interlaced modes + # Prefer the matching fbcon mode string the kernel already + # registered; fall back to constructing one in the same format. + mode=$(grep -m1 -E "^U:${width}x${height}p" \ + /sys/class/graphics/fb0/modes 2>/dev/null) + [ -n "$mode" ] || mode="U:${width}x${height}p-0" + echo "$mode" > /sys/class/graphics/fb0/mode 2>/dev/null || return 1 + echo "start_viewer: HDMI reconnect — re-asserted ${width}x${height}" \ + "framebuffer mode; restarting viewer." + return 0 +} + +monitor_hdmi_resolution() { + # linuxfb only; eglfs/wayland boards recover on their own. + [ "${QT_QPA_PLATFORM:-}" = 'linuxfb' ] || return 0 + local viewer_pid="$1" conn previous current + conn=$(find_connected_hdmi) || return 0 + previous='connected' + while kill -0 "$viewer_pid" 2>/dev/null; do + sleep 3 + current=$(cat "$conn/status" 2>/dev/null) + if [ "$previous" != 'connected' ] && [ "$current" = 'connected' ]; then + # The sink just came back. Give EDID/DDC a moment to settle, + # then re-assert the preferred mode. On success the viewer is + # restarted so Qt re-reads the framebuffer at the right size; + # on failure (EDID not up yet) leave previous != connected so + # the next loop re-checks. + sleep "$HOTPLUG_SETTLE_SECONDS" + if reassert_preferred_mode "$conn"; then + kill "$viewer_pid" 2>/dev/null + return 0 + fi + continue + fi + [ -n "$current" ] && previous="$current" + done +} + # x86 / arm64 / pi5 run under `cage`, a kiosk wlroots compositor. # cage acquires DRM master as root, exports WAYLAND_DISPLAY for its # child, and exits when the child exits — so the existing kill -0 @@ -572,6 +665,11 @@ while true; do sleep 0.5 done +# Self-heal the linuxfb 1024x768 HDMI-hotplug latch (issue #3052): on a +# TV power-cycle, re-assert the connector's preferred mode and restart +# the viewer. No-op on eglfs/wayland boards (guarded inside). +monitor_hdmi_resolution "$PID" & + # If the viewer runs OOM, force the OOM killer to kill this script so the container restarts echo 1000 > /proc/$$/oom_score_adj