Hard-won per-board hardware knowledge for Anthias — display stack (linuxfb/eglfs/wayland-cage), video HW-vs-SW decode per chip (Pi 2/3/4/5, x86, Rock Pi 4), the Qt6 presentation bottleneck, rotation convention, WebGL, boot splash, audio, and the Pi 2 armhf toolchain SIGBUS. Read before touching viewer rendering, codec, rotation, display-power, or Qt build code.
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Une commande directe contourne le prompt de vérification. Examinez la source avant de l'exécuter.
Hard-won per-board hardware knowledge for Anthias — display stack (linuxfb/eglfs/wayland-cage), video HW-vs-SW decode per chip (Pi 2/3/4/5, x86, Rock Pi 4), the Qt6 presentation bottleneck, rotation convention, WebGL, boot splash, audio, and the Pi 2 armhf toolchain SIGBUS. Read before touching viewer rendering, codec, rotation, display-power, or Qt build code.
Anthias board / display / codec knowledge
Durable engineering findings from field debugging across the board fleet. Each
bullet carries the mechanism plus the fixing PR / CalVer release where known;
OPEN items are called out. Boards map to a rendering stack by userspace arch, so
read the display-stack section first.
Board → QPA plugin mapping. Pi 1/2/3-32 (armhf/Qt5) run QT_QPA_PLATFORM=linuxfb (needs /dev/fb0); Pi 3-64/Pi 4 (arm64/Qt6) run eglfs (DRM/GBM, holds DRM master, no fb0); Pi 5/x86/Rock Pi 4 (arm64/Qt6) run cage/wlroots wayland. Pi 4 switched linuxfb→eglfs in #2904 for HW video. Stale start_viewer.sh comments claiming "Pi 4 runs linuxfb" were corrected in #2962.
DRM master ownership determines what can blank/rotate/recover. On eglfs/linuxfb the Qt app (or drm_fb_helper) owns DRM master; on wayland the cage compositor owns the connector. This single fact drives blanking, hotplug recovery, and rotation mechanism per stack.
Board selection is by userspace arch (dpkg --print-architecture), NOT uname -m or model string. 32-bit Raspberry Pi OS ships a 64-bit kernel by default on Pi 3 (arm_64bit=1), so uname -m=aarch64 while docker/apt are armhf. Gating on kernel arch mispicked pi3-64 and pulled linux/arm/v8 (no matching manifest). Fixed in PR #3076 (2026-06-16) to gate on userspace arch. arm64 catch-all branch still uses uname -m (no armhf image for generic SBCs). No in-place arch switch — only reflash changes streams.
Display blanking (per-platform power-off)
Wayland boards can true-power-off; eglfs/linuxfb cannot.wlr-randr --output <NAME> --off DPMS-powers-off the monitor on wayland (run as viewer user with XDG_RUNTIME_DIR=/run/user/1000 + WAYLAND_DISPLAY=wayland-0). On eglfs/linuxfb the Qt app holds DRM master so external blank is rejected (echo 3 > /sys/class/drm/cardN-HDMI-A-1/dpms → EBUSY under atomic KMS; vcgencmd display_power 0 no-op under vc4-KMS) — the only blank without stopping the viewer is to paint the screen black. PR #3065 added first-class blank/unblank viewer commands (Redis anthias.viewer channel) that do wlr-randr off on wayland, paint BLACK_SCREEN (img/black.png) on eglfs/linuxfb.
Headless-boot / HDMI-hotplug recovery (differs sharply by stack)
linuxfb (32-bit Pi 1/2/3): headless boot = no /dev/fb0 = crash-loop. Full KMS (dtoverlay=vc4-kms-v3d) only creates /dev/fb0 when a display is connected at boot; headless → connector disconnected → no fb0 → linuxfb hard-requires it → Unable to figure out framebuffer device → no screens available → Qt heap-corrupts on the no-screen path (malloc(): unaligned tcache). Firmware hdmi_force_hotplug/hdmi_* knobs are a DEAD END (full KMS ignores them); only kernel cmdline video=<connector>:... forces a fb, unsafe for a fleet. Fix = graceful wait-and-retry in viewer (#2959 fb0-wait), self-heals when display hotplugs. /dev/dri/card0+renderD128 exist even headless; only fbdev emulation is absent.
eglfs (Pi 3-64/Pi 4): does NOT need the linuxfb stale-/dev exit-1 guard. Verified by clean power-cycle: /dev/dri/cardN is created at vc4 driver bind (kernel boot), present headless; open() routes by (major,minor)=226,1 to the live device even with a stale-timestamp node; connector status is read from sysfs (truthful). eglfs opens container card1 and modesets HDMI-A-1 after headless-boot+connect. But eglfs boards refuse to start the viewer while headless (wait_for_eglfs_display in start_viewer.sh from #2962 polls /sys/class/drm/card*-*/status for non-disconnected) — correct signage behavior but means a headless testbed renders nothing.
eglfs headless gap was also a crash-loop before #2962. Pi 4 eglfs headless → no screens available → binary exits before D-Bus handshake → container crash-loop. #2962 (wait_for_eglfs_display) mirrors the linuxfb fb0 wait. Separately #2969 added capped-backoff retry of the AnthiasViewer spawn (BROWSER_SPAWN_MAX_ATTEMPTS=30, BROWSER_SPAWN_BACKOFF_CAP_SECONDS=15).
wayland-cage (Pi 5/x86): headless boot binds ZERO wl_outputs and never recovers on hotplug (#3155, refactor #3161, shipped 2026-07-08). cage enumerates DRM connectors at startup; if none connected+enabled it binds 0 wl_outputs and runs forever (no exit → no restart → permanent black). It can't recover on hotplug because the viewer container has no udevd / no /run/udev, so wlroots' libudev hotplug monitor gets no uevent. Contrast: eglfs Qt exits non-zero on no-fb and restart: always self-heals. Symptom: viewer log spams ; → ; scheduler runs normally. Fix = in per asset_loop tick: if no bindable output past → container restart → cage re-enumerates.
Video decode (HW vs SW) by chip
Pi 4 (bcm2835, stateful v4l2m2m): ≤1080p H.264 HW-decodes; >1080p falls to SW.h264_v4l2m2m (the +rpt1 ffmpeg build) IS engaged by QtMultimedia for ≤1080p H.264 — /dev/video10 fd held open whole clip, ~120-140% CPU, ~0 drops. 4K30 / High10 / 4:2:2 / level>4.x → not opened, cores pegged, ~36% drops. QT_FFMPEG_DECODING_HW_DEVICE_TYPES only accepts AVHWDeviceType names (cuda/drm/vaapi), NOT v4l2m2m (a standalone AVCodec) — Qt reaches it empirically anyway on +rpt1. Probe = /dev/video10 open + CPU-delta.
Pi 5 (Hantro G2, v4l2-request): HEVC HW-decodes through QtMultimedia libavcodec. Verified via /proc/<pid>/fd: /dev/video19 (rpi-hevc-dec), /dev/dma_heap/linux,cma, /dev/media2, anon_inode:request, /dmabuf: frames; 4K30/4K60 HEVC both play at 1.0×. The chain QtMultimedia→libavcodec61→v4l2-request→/dev/video19 is load-bearing; don't strip device passthrough or the +rpt1 pin. Pi 5 has no H.264 HW decode block (upload gate rejects H.264, supported set = {hevc}).
Pi 3-64 (VideoCore IV, +rpt1 gate): was SW-only until #3110 added pi3-64 to the board tuple.docker/_rpt1-ffmpeg-pin.j2 gated the RPi +rpt1 ffmpeg on board in ('pi4-64','pi5','arm64') — pi3-64 was MISSING, so it shipped stock Debian ffmpeg and SW-decoded (~0.7× realtime). PR #3110 (shipped, HW-validated) → /dev/video10 held, 30fps realtime, dropped=0. H.264-only HW decode on VC IV (_HW_DECODE_VIDEO_CODECS['pi3-64'] = {'h264'}).
Rock Pi 4 (RK3399, stateless v4l2_request): both H.264 and HEVC SW-decode in practice (OPEN). No-hwaccel picks native SW — what QtMultimedia's libavcodec does. -hwaccel drm engages rkvdec for HEVC but produces Decode fail at 0.77× (broken on Armbian 6.18 kernel); H.264 has NO v4l2_request hwaccel binding in the +rpt1 7.1.3 build. h264_v4l2m2m/hevc_v4l2m2m = "Could not find a valid device" (stateful M2M wrappers don't bind the stateless RK3399 nodes). Real arm64 HW decode needs QtMultimedia to request -hwaccel drm against v4l2_request, and even then only HEVC works (with errors) on this kernel.
Video presentation bottleneck (Qt6 boards — decode ≠ what reaches screen)
Qt6 boards present video at ~8-12fps despite perfect HW decode (issue #2967). Root cause: QGraphicsVideoItem::paint → QVideoFrame::toImage → qImageFromVideoFrame — Qt 6.8 converts YUV→RGB on the GPU then reads it back to a QImage, CPU-blits into raster backing store, re-uploads to GPU. Two GPU/CPU crossings per frame. Measured: Pi 4 1080p30 H.264 8.3fps presented; Pi 5 11.7fps. PR #2975 fix = QML VideoOutput in a QQuickWidget (player->setVideoSink(item.videoSink)) → Pi 4 30.0fps/66% CPU. A QOpenGLWidget viewport is NOT sufficient (toImage stays); QVideoWidget = second native window, fails the eglfs single-window constraint.
#2975 fix does NOT hold at 1080p (OPEN, resolution-scaling). RC QA (forced 1080p60 headless): 720p H.264 ~28fps, but 1080p H.264 ~5.8fps (~40% pacer-dropped). Not a regression (Qt6 video byte-identical across releases). #3006 added a pacer that DROPS frames to match scene-render capacity. CONFOUND: measured on a no-EDID headless display; confirm on a real Pi 4 + monitor. Tracked in issue #2987.
Measure PRESENTED fps via the kernel DRM vblank tracepoint, never playback-stats.log. Count drm_vblank_event_delivered events over 10s (/sys/kernel/debug/tracing, count/10 = presented fps, crtc=N disambiguates outputs) — zero observer effect. playback-stats.log counts sink deliveries (full decode rate) and is blind to paints coalescing. frames-rendered (=QQuickWindow::afterRendering) is scene-render into an offscreen FBO, NOT scanout.
Architecture note (misnomer): Qt6 boards (pi4/pi5/x86/rockpi4) use MPVMediaPlayer — a misnomer, it drives IN-PROCESS QtMultimedia in the C++ webview over D-Bus, NO mpv binary anywhere post-#2904. pi1/2/3-32 use GstFbdevMediaPlayer (GStreamer HW → fbdevsink). pi3-64/pi4-64 routed via force_mpv env override in media_player.py.
issue #2987 video regressions (2026.6.1, four distinct defects)
2. pi3-32 freeze/cut clips — bash while true; gst-launch loop rebuilt the pipeline per EOS. Fix: playbin about-to-finish gapless loop in gst_fbdev_player.py (PR #3004). Helper MUST be spawned by path, not -m (package __init__ imports Django).
3. Pi 4 (Qt6) 1080p60 collapse — 22.6fps presented, clips killed by slot duration. Fix (PR #3006): intermediate QVideoSink gating on afterRendering + single-slot latest-frame mailbox. CEILING ~23fps for 60fps sources; full 60fps needs the bigger single-window rework. dropped= counter reads 0 in fast-forward mode — don't use it to detect this.
4. Backup timeout — sync tar gzip-9 = 98s for 355MB on Pi 4, browsers abort byte-less requests ~5min. Fix: StreamingHttpResponse over a pipe + compresslevel 1 (PR #3005). Pre-existing.
Pi 3-64 video BLACK on VideoCore IV (issue #3084 / forum 6720/6730) — RESOLVED via DRM overlay plane
Root cause: vc4 (VideoCore IV) GLES2 can composite RGB but CANNOT render QtMultimedia's VideoOutput scene-graph video node — for HW OR SW frames. Video plays fine on Pi 4 (V3D 4.2) and Pi 5 (V3D 7.x) but is pure black on Pi 3-64, while still images + webview composite fine. Decode is healthy. Pinned by a red-backdrop diagnostic (Rectangle{color:RED; VideoOutput{}} → JUST RED). The #2967 substrate swap (toImage CPU-raster → VideoOutput RHI-texture) traded slow-but-visible for fast-but-black on VC IV.
CPU-raster fallback works (unblacks) but caps ~1.5fps — QVideoFrame::toImage() on a 1080p HW SAND/DMABUF frame = ~600-800ms/frame on VC IV. SW decode makes toImage cheap but SW-decoding 1080p30 REBOOTS the 1GB Pi 3.
Two keys: (1) decoder-direct, NO v4l2convert — the bcm2835 ISP I420→NV12 convert caps ~15-18fps at 1080p; decoder alone does 30fps+. (2) capture-io-mode=mmap — default (exported dmabuf) lets kmssink zero-copy and PIN the decoder DPB buffers → pool starves → DEADLOCK (~frame 15). mmap forces kmssink to COPY each frame (~3MB, cheap) and release the decoder buffer immediately.
Audio runs a fully independent GStreamer pipeline → alsasink device=sysdefault:CARD=vc4hdmi DIRECT to HDMI (not pulsesink; pulse defaults to 3.5mm). Every shared-pipeline attempt froze video at frame 1. Added gstreamer1.0-libav for AC3/AAC/MP3.
In-process kmssink on eglfs's SHARED master fd is fragile: a crashed/SIGKILLed kmssink leaves the overlay plane bound → cascading DRM wedge until reboot — a supervised DRM-lease child would be more robust.
OPEN follow-up: presented fps << decode fps under memory pressure. On the ~787MB Pi 3-64 the merged build decodes 30fps but kmssink QoS-drops ~11fps as "late" → screen presents ~19-21fps under swap thrash. NOT a code regression. Next lever untried: .
screen_rotation is CLOCKWISE uniformly on every stack as of 2026.07.3 (PR #3202, 2026-07-16). Before 2026.07.3 linuxfb turned CW but eglfs/wayland turned CCW for the same setting. Canonical = clockwise (matches linuxfb paint path + GStreamer videoflip).
linuxfb (Pi 1/2/3-32): QPA ignores rotation=N, so content is turned by hand — images/GIFs in View::paintEvent (rotation.cpplinuxfbRotationOverride), web pages via injected CSS (webpageRotationScript), video via videoflip (90→CW).
eglfs (Pi 3-64/Pi 4, Rock Pi 4):QT_QPA_EGLFS_ROTATION NEGATED — {90:-90, 180:180, 270:90}. A literal 270 rotates content but skips Qt's width/height swap (viewport stays 1920×1080 → stretched); emit -90 instead (PR #2973, #2970).
pi3-64 video: vc4 overlay plane rotate-180 for 0/180 (HW, #3189); 90/270 drop the overlay → eglfs raster fallback inherits the compositor turn.
_is_wayland_board() rotation dispatch bug (RESOLVED #3047, issue #3044). Returned DEVICE_TYPE == 'x86' only, so pi5/arm64 never reached the wlr-randr transform path. Fixed to os.environ.get('QT_QPA_PLATFORM','').startswith('wayland').
Pi 4 rotation was a no-op on eglfs (RESOLVED). #2882 appended rotation=N to QT_QPA_PLATFORM, but that's a linuxfb-plugin option eglfs ignores. Fix: set QT_QPA_EGLFS_ROTATION=<deg> in _build_webview_env for eglfs AND remove the app-level video-rotate branch in media_player.py (else video double-rotates). Firmware display_rotate is ignored under vc4-kms-v3d + eglfs_kms.
pi2/pi3-32 linuxfb: only video rotated pre-fix, images + web pages upright (RESOLVED PR #3197, 2026.07.3). Fix: images/GIFs rotated by hand in View::paintEvent (linuxfbRotationOverride()); web pages via injected CSS through imperative runJavaScript. Gated linuxfb-only (eglfs/wayland rotate the whole compositor output).
— even with +. Use imperative from a PERSISTENT handler (re-applies on every auto-refresh reload; keep a MutationObserver for SPA rewrites). Debug without rebuild: + a stdlib CDP WebSocket client.
GPU / WebGL
WebGL 1+2 is hardware-accelerated out of the box on every Qt6 board (verified 2026-06-07): Pi 5 = V3D 7.1.7.0 (wayland), Pi 4 = V3D 4.2.14.0 (eglfs), Rock Pi 4 = Mali-T860/Panfrost (wayland). No QTWEBENGINE_CHROMIUM_FLAGS set; WebGLEnabled default true. Qt5 boards (Pi 1-3 32-bit, linuxfb) have NO GL — Qt built without -opengl es2 (enabling it breaks QtWebEngine); WebGL cannot work there. If a WebGL asset misbehaves on a Qt6 board, suspect page JS or memory pressure, not the GPU context.
Webpage→webpage transition flashed the (n-2 mod N) page for ~1 frame. The 2-buffer ping-pong revealed a buffer whose last-composited GPU surface still held the page from two rotations ago. loadFinished ≠ first-paint, and an occluded QWebEngineView does NOT composite (Chromium frame-callback-throttles occluded WebContents). Fix (2026-07-08, live-verified Pi 5): collapse to the single-QWebEngineView path for ALL boards — an in-place load() on the visible view keeps the old page composited until the new one paints. Removed the ANTHIAS_LOW_RAM viewer flag. Residual: x86 wayland logs periodic Atomic commit failed: Device or resource busy (benign).
Boot splash (Plymouth)
x86: Plymouth boot splash holds DRM master → cage wedges (RESOLVED #2955, shipped 2026.05.2). balenaOS plymouthd on x86 opens /dev/dri/card0 as DRM MASTER (efifb→i915 early KMS). cage then runs as non-master and every atomic commit is rejected — screen frozen on splash while Swapchain for output 'DP-1' failed test spams. Confirm via /sys/kernel/debug/dri/0/clients. Fix = start_viewer.sh asks host systemd (over the host system bus) to run plymouth-quit.service before launching cage. x86-specific: Pi plymouthd opens /dev/fb0, never becomes DRM master.
"Splash broken" on a fresh Pi is usually a pending-reboot artifact, NOT a bug. The running kernel still holds the stock rpi-imager cmdline (no splash); Ansible writes the Anthias cmdline.txt but it only takes effect next reboot. First diagnostic: diff cat /proc/cmdline vs cat /boot/firmware/cmdline.txt. Use dpkg-query -W (not dpkg -l | grep) to check the package.
Pi 2 splash rendered the TEXT theme, not Anthias (RESOLVED PR #3199, 2026.07.3). plymouthd waits DeviceTimeout for a graphics device (distro default 8s); on a Pi 2 (A7 @900MHz) vc4's /dev/dri/card0 only appears at ~14.3s → plymouthd commits to text at 12.65s. Fix: DeviceTimeout=30 in the plymouthd default + a Regenerate-initramfs handler. plymouthd runs from the initramfs and reads its config from the copy baked there — a config-only on-disk change is inert; verify with unmkinitramfs. A disconnected HDMI fakes this exact bug.
Audio
Qt6 Multimedia (Debian trixie) has ONLY a PulseAudio backend — links libpulse, zero ALSA code, no QT_AUDIO_BACKEND escape. No pulse server → QMediaDevices::audioOutputs() empty → silent video (issue #3000, all Qt6 boards after the #2905 mpv→QtMultimedia migration). Fix (PR #3001): start_pulseaudio() in start_viewer.sh runs a per-container pulse daemon as the viewer user. Sink-naming trick keeps routing: load-module module-alsa-card ... name=<alsa card id> → sink names embed the card name (alsa_output.vc4hdmi0.hdmi-stereo). Verify via /proc/asound/cardN/pcm0p/sub0/status=RUNNING.
HDMI/3.5mm audio_output selector is a no-op on x86 + arm64 (RESOLVED issue 3208 / PR 3209).get_alsa_audio_device collapsed to 'default' on non-Pi boards. Fix: non-Pi boards discover sinks at runtime via pactl list short sinks and pick by the profile token in the sink name (hdmi-* vs analog-*), not the card name; return the bare sink name, matched against QAudioDevice::id(); fall back to default if pulse unreachable. Pi paths unchanged. A headless x86 has no HDMI pulse sink, so it correctly falls back to default.
Pi 2 (and 32-bit Pi 3/armhf) blanks: deterministic ARMv7 alignment SIGBUS inside libQt5WebEngineCore (forum thread 6732). AnthiasViewer handshakes on D-Bus then SIGBUSes post-handshake: Alignment trap ... f9400adf. A 64-bit NEON doubleword store (vst1.8 {q8},[rN:64]) stamped :64 alignment lands on a 4-byte-aligned Chromium address; Cortex-A7 faults, kernel can't fix up NEON → SIGBUS. 100% deterministic even with empty playlist.
Root cause is the Debian gcc-14 armhf TOOLCHAIN, not the Qt version. Bisection on real Pi 2: 5.15.19 crashes; prebuilt 5.15.14 (built with Linaro gcc-7) is clean; 5.15.18 AND 5.15.14 rebuilt with gcc-14 BOTH crash. The culprit is the toolchain switch (~PR #3070 "replace retired Linaro armhf toolchain with Debian's"). The faulting store comes from gcc's ARM backend RTL block-move expander (arm_block_set_aligned_vect), NOT the SLP vectorizer (so -fno-tree-vectorize does nothing). GCC WONTFIX by policy; only Linaro gcc-7 doesn't emit it.
Fix VALIDATED on real Pi 2 (2026-07-10), 0 alignment traps — requires BOTH: (1) arm_use_neon=false in qtwebengine src/core/config/common.pri; (2) global -mno-unaligned-access via a compiler wrapper shim on /usr/bin/arm-linux-gnueabihf-{gcc,g++}. HW SIMD video decode preserved (libvpx/Skia re-add -mfpu=neon per-file). Metric truth = runtime dmesg | grep -c "alignment exception", NOT the vst1:64 count in the .so and NOT the launch harness (post-handshake, reports OK while the screen still blanks).
Qt5 toolchain rebuild is broken on trixie (Python 3.13 removed stdlib modules Chromium 87's build needs). Fixes: imp/pipes/cgi shims via PYTHONPATH, overwrite Chromium's vendored six with distro six 1.17; the re2 API break — drop libre2-dev so use_system_re2=false (also clear config.cache's webengine-re2.result=true). QtWebEngine ninja OOMs the build box regardless of MAKE_CORES (bundled ninja self-detects nproc); fix: export NINJAFLAGS="-j${MAKE_CORES}".
Qt5 5.15.14→5.15.19 toolchain bump: MERGED PR #3121, WebView-v2026.07.0. Qt6 boards untouched (track trixie apt 6.8.2; qt_version='6.4.2' in utils.py is dead, only names the Qt5 tarball). The viewer image downloads from the WebView release, so tarballs must be built + uploaded BEFORE the utils.py bump merges or pi2/pi3 image builds 404 on the sha256.
Pi 2/3-32 armhf webview crash-loop (headless AND display-present)
Two distinct malloc-unaligned crashes on arm32 — don't conflate. (a) malloc(): unaligned tcache on the NO-SCREEN/headless path (see headless-hotplug). (b) On a device WITH a valid /dev/fb0, the Qt5 viewer heap-corrupts while building the font database (deterministically at the synthetic-oblique Monospace style) → crash-loop. arm32-only. The real corruptor is Qt5 QtWebEngine/Chromium init on armv7 (only surfaces with --no-sandbox, which the fleet uses); the font DB is just the victim. Stop-gap = #2969 capped-backoff spawn retry. Real fix = the arm64/Qt6 pi3-64 migration (Qt6 WebEngine inits clean on Pi 3 silicon).
Board / fleet mapping facts
pi3-64 (arm64/Qt6/eglfs_kms) added alongside legacy 32-bit armhf/Qt5 pi3; mirrors the pi4-64 path (in-process QtMultimedia, no cage, no mpv); H.264-only HW decode on VC IV. get_device_type() still returns 'pi3'; routing relies on force_mpv. docker/eglfs-kms.json shared pi4-64/pi3-64, rewritten at runtime by start_viewer.sh DRM-card detection.
Rock Pi 4 (rockpi-4b-rk3399) runs the GENERIC arm64 images (no rockpi4 board build; deploy scripts rewrite BOARD→arm64 and strip /dev/vchiq). board.get_board_subtype falls back to /proc/device-tree/model in-container when redis is empty. Rock Pi 4 is 1GB and OOM-wedges even idle on the arm64 viewer.
Pi 5 runs a 16KB-page kernel (getconf PAGESIZE=16384). 32-bit armhf userspace (built for 4KB pages) fails to load (ELF load command ... not page-aligned) even under --platform linux/arm/v7; no qemu binfmt on these boards. For native armv7/armhf validation use the Pi 4 (arm64 kernel, 4KB pages). Pi 5 outputs 4K@30 → presented-fps ceiling is 30, not 60.
Diagnostic / harness techniques
Webview viewport probe via D-Bus (rotation/geometry without a screen capture): read DBUS_SESSION_BUS_ADDRESS from /proc/<AnthiasViewer pid>/environ, docker exec -u viewer pydbus bus.get('anthias.viewer','/Anthias').loadPage(url) (root gets "connection is closed"), load a data: page that fetch()es innerWidth/innerHeight/screen.width to the server, read dims from the uvicorn access log.
Offscreen D-Bus repro harness (validate AnthiasWebview on the dev host, no testbed): x86 viewer image + build src/anthias_webview with qmake6 && make inside, run under dbus-run-session with QT_QPA_PLATFORM=offscreen + QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --disable-gpu", drive via gdbus call. D-Bus interface casing differs Qt5 vs Qt6: Qt6 = local.anthiasviewer.MainWindow (lowercase), Qt5 (armhf) = local.AnthiasViewer.MainWindow (CamelCase); introspect /Anthias rather than hard-coding. C++ stderr is NOT in docker logs (bounded deque, issue-3138) — the playback-stats file is the visible signal.
pi3-64 eglfs capture/fps (fb0 is dead/all-zero on eglfs — DRM/GBM not fbdev): DRM plane structure via sudo modetest -M vc4 -p (active = crtc!=0 && fb!=0; overlay path = 2 planes; rotation: value 1=rotate-0, 4=rotate-180, vc4 has no 90/270). Pixel grab via ffmpeg -f kmsgrab (primary plane is T-tiled → scrambled but colors intact; SAND-tiled video plane can't be grabbed). Watch vcgencmd measure_temp/get_throttled (SW rotate heats the passively-cooled Pi 3 into throttle).
Content-diff fps measurement needs a full-frame-motion source (ffmpeg -f lavfi -i mandelbrot); a color-bar clip with a thin moving sweep under-counts to ~1fps even when playing fine. Prefer fpsdisplaysink over fb0 mmap sampling.
wlr-randr could not apply rotation N
grim
no wl_output
_wayland_output_watchdog()
__init__.py
sys.exit(1)
WAYLAND_OUTPUT_GRACE_S=60
Gate on BINDABLE, not just connected. A marginally-seated HDMI asserts HPD (status=connected) but EDID never read → /sys/class/drm/<c>/modes EMPTY → cage has no mode to bind → restart-loops forever. _kernel_has_bindable_display() requires connected AND non-empty modes.
Second bug: restart recovery is racy. The restart-policy relaunch is nearly instantaneous; if the old cage's DRM-master release + vc4 connector reset haven't drained, the fresh cage races a half-reset connector → EDID not re-read → empty modes → headless again (~50% wedge). docker restart recovers reliably (its stop→start gap lets vc4 reset). Fix in start_viewer.sh cage branch: on a connected connector, sleep 4 then force echo detect > status + bounded-wait for modes to repopulate before launching cage.
HDMI hotplug resolution drop to 1024×768 (issue #3052, fixed PR #3075).1024×768 is the kernel drm_fb_helper hard-coded fallback mode, triggered when a vc4-kms-v3d re-probe races the sink's EDID return (slow TVs). linuxfb boards vulnerable (no process holds DRM master; Qt reads /dev/fb0 once, can't follow a later mode change). eglfs boards immune (Qt holds DRM master, keeps its modeset across hotplug). Recover in-container without fbset: echo "U:1920x1080p-0" > /sys/class/graphics/fb0/mode (the -0 refresh field is CORRECT — vc4-kms-v3d fbdev emulation registers modes as U:WxHp-0). True active geometry = fbset -i; virtual_size is the static allocated buffer, /modes head is a sticky unreliable list.
Container /dev is a boot-time snapshot. If the privileged viewer container started headless and a display is plugged in later, the host gets /dev/fb0 but the container does NOT (only /sys/class/graphics/fb0 shows through) → wait_for_framebuffer parks forever. docker restart re-snapshots /dev. Only bites hotplug-after-boot. The celery container can NEVER do display recovery (not privileged, no /dev/fb0 or /dev/dri, /sys read-only) — only the privileged viewer container.
x86 (Intel UHD 610, iHD/VAAPI): HEVC HW decode fails inside cage's GL context (OPEN, worked around).vainfo lists HEVC VLD, container ffmpeg + an offscreen AnthiasViewer both VAAPI-decode HEVC cleanly, but the real display-up viewer emits FormatError code=2 / 0 frames on HEVC RTSP (H.264 in the same context is fine). VAAPI-HW × GL-context × HEVC interaction; QT_DISABLE_HW_TEXTURES_CONVERSION=1 did NOT fix it. Workaround: QT_FFMPEG_DECODING_HW_DEVICE_TYPES=, (empty → disable HW decode) → SW decode plays. Tradeoff: disables HW decode for ALL codecs on x86; a board-conditional list is better than blanket-off.
kmssink qos=false
qt5pi declarative QWebEngineScript silently never fires
Rotated video is ~1-2fps on Pi 1/2/3 linuxfb (issue #3198). Software videoflip rotates full-res frames single-threaded on A7/A53 → 100% of one core, ~1fps (vs 26fps unrotated). Pre-existing from #3004. Split verdict (PR #3207): 180° fixable for FREE via the bcm2835 ISP — v4l2convert extra-controls=c,horizontal_flip=1,vertical_flip=1 runs the flip in HW → full baseline fps, pixel-exact. 90/270 INFEASIBLE on VideoCore IV — no rotate control on any bcm2835 v4l2 device (only hflip/vflip). Scale-before-flip is a dead end. Measure fps with fpsdisplaysink, NOT fb0 mmap sampling.