evidence · 13.1 KiB

hypotheses.md

Committed at docs/evidence/card-shell/bottom-band-flicker/hypotheses.md. This is what grounds the requirements that cite it.

Open raw source

Bottom-~50px flicker during animation: hypothesis list for board experiments

User report (real glass, 568x1232 RM69A10 @ 52.19 Hz): the bottom ~50 px band of the panel flickers while the screen animates — during bottom-edge app-switch dragging and while the overview/app-switcher is shown. The user is explicit the band is not shaped like app icons; it reads as a banding/ tearing artifact, not mis-rendered content.

While investigating this, a genuine but separate compositor-scene bug turned up and was fixed on this same branch: home_layer_sync (nix/ card-shell/adapter.c) only ran from card_shell’s own state-machine transitions, so an unrelated Sway focus change while the overview stayed open could re-enable Home’s layer without shell.active ever changing — see openspec/changes/the-overview-hides-the-home-screen/tasks.md section 6 and tests/test_card_shell_home_layer_self_heal.py. That is a real, independent defect (content-shaped, would show Home’s own grid/dock), not the hardware-level banding this document investigates, and it does not change any hypothesis below.

What the compositor side already rules out

grim bursts of the bottom 80 px strip taken while the overview is held open via IPC (swaymsg card_shell enter, no further input) are pixel-identical frame to frame — see the coordinator’s prior capture. That is a static scene: nothing re-renders once settled, so it cannot exercise a race that only exists while the compositor is actually producing a new frame every ~19.2 ms. It does not clear the render or scanout path during animation.

Relevant grounding already in this repo

Prioritised hypotheses and board-side experiments

Ordered by (a) how directly each is supported by the grounding above, and (b) how cheap the experiment is. None of steps 1-2 need a rebuild.

1. Confirm with the existing telemetry pipeline first (no rebuild, ~5 min)

This compositor already has a mature, reviewed benchmark/telemetry path built exactly for “how long does rendering take, and were frames presented on the expected cadence” (nix/card-shell/telemetry.c, tools/card-shell-benchmark.py, documented in nix/card-shell/README.md’s “Benchmark IPC” section). Run it against the real reported gesture before touching anything else:

swaymsg card_shell benchmark physical
# perform the exact reported gesture: a bottom-edge app switch, and
# separately, opening the app switcher by a real swipe; wait 3s before/after
swaymsg card_shell benchmark-stop
journalctl -u <sway-service> | grep K230_CARD_BENCH > telemetry.log
python3 tools/card-shell-benchmark.py --input telemetry.log --renderer pixman

Read tracking_present_interval_ms (spacing between successive presented frames while dragging — this is DRM/vblank feedback, not a guess) and frame_update_cpu_ms (Pixman scene-build + commit-submit CPU time per frame) in the JSON report:

This single run should reorder the rest of this list with real numbers before any hardware toggle is flipped.

2. Host render occasionally overruns the ~19.2 ms budget (no rebuild)

The panel refreshes at 52.19 Hz (~19.2 ms/frame). This is a CPU/Pixman software-rendering compositor, and this project already has a whole set of in-flight branches (perf/card-scaled-cache, measure/card-render-cost, perf/card-mirror-order, perf/theme-background-decode, …) tracking that full-screen card-shell animation frames are expensive to render. If a frame’s build+commit occasionally exceeds one refresh period, the next commit lands after scanout has already started reading the previous buffer for that period.

Experimental toggle: swaymsg output DSI-1 max_render_time 12 (render each frame starting ~12 ms before wlroots’ predicted deadline, rather than sway’s default “start as late as possible” latency-minimising schedule), then repeat the gesture and watch the panel. If the band goes away or gets markedly rarer, this supports “occasional CPU overrun, no headroom” as (part of) the cause; revert with swaymsg output DSI-1 max_render_time off. Cross-check against hypothesis 1’s frame_update_cpu_ms numbers, since max_render_time only changes when rendering starts, not whether a genuinely slow frame finishes in time.

3. K230 DRM driver’s flip-done/vblank signalling is not trustworthy (no rebuild for the toggle; needs a session restart, not a package rebuild)

docs/evidence/drm-info.txt shows atomic KMS and DRM_CAP_CRTC_IN_VBLANK_EVENT are both advertised, but this is an early vendor driver (canaan-drm “1.0.0 20230501”) with no render node and dumb-buffer-only allocation — exactly the profile of a driver whose atomic commit / vblank-event path has not been exercised much. If the driver signals “flip complete” to wlroots before the hardware has actually finished scanning out the previous buffer (or before the next buffer is safely presentable), wlroots could legitimately believe it is safe to recycle/write a buffer that is still on-glass, producing exactly a band of stale/torn pixels that only shows up when frames are produced back-to-back (i.e. during animation, never in the static IPC capture).

Experimental toggle: WLR_DRM_NO_ATOMIC=1 in the compositor’s environment, then restart the Sway session (no rebuild) and repeat the gesture. This forces wlroots onto the legacy drmModePageFlip KMS path, whose page-flip completion semantics are a different code path in both wlroots and the vendor driver than the atomic commit path. If the band changes character or disappears, that isolates the bug to the atomic-commit/vblank-event path specifically (a very plausible immature-driver bug class). If it is identical, legacy and atomic share whatever is wrong (more likely a deeper issue such as hypothesis 4).

4. The command-mode panel’s own scan is not synchronized to host writes at all (TE never wired) — same root cause already found for the whole-frame-roll symptom

Per docs/evidence/flicker-after-headroom-revert.md, SET_TEAR_ON is sent but nothing consumes the panel’s TE line, so the panel’s internal GRAM scan and the host’s writes into that GRAM already run on independent, unsynced clocks. At idle/steady state that showed up as a slow, uniform roll. Under per-frame CPU render-time jitter (animation), the same lack of synchronization could plausibly show up instead as a locally unstable band near wherever the write “catches up to or is caught by” the panel’s own scan position for that instant — which would look like flicker rather than a clean roll, and would be worse exactly when render timing is least uniform (i.e. during animation), matching the report.

This is a real hardware/driver gap, not a toggle: fixing it needs the panel driver to actually wait for (or schedule around) the TE GPIO/interrupt, which is display/panel work, not something flippable from the compositor today. It can be supported or weakened by combining hypothesis 1’s present-interval numbers (clean cadence at the DRM-feedback level) with direct visual correlation: if the band’s position/character does not track frame_update_cpu_ms variance at all (i.e. it happens identically even during the cheapest, fastest-rendering frames of a gesture), that argues against hypothesis 2/3 and toward this one — a free-running panel-side scan that doesn’t care how fast the host writes.

5. Lower priority / weaker support

What each experiment would mean, summarised

ToggleIf band disappears/improvesIf band unchanged
Read tracking_present_interval_ms/frame_update_cpu_ms from a physical benchmark runn/a (diagnostic, not a fix)n/a
max_render_time 12 (then off)Host render sometimes has no headroom before its deadline (hyp. 2)CPU timing isn’t the (whole) story
WLR_DRM_NO_ATOMIC=1Bug is specific to the atomic commit/vblank-event path (hyp. 3)Legacy and atomic share the bug, or it’s not driver-flip-path-specific
WLR_SCENE_DISABLE_DIRECT_SCANOUT=1Direct-scanout/composited path transition was the triggerNot a direct-scanout artifact
WLR_DRM_NO_MODIFIERS=1Modifier/stride mismatch on this driverNot a modifier issue
(no toggle) optical correlation with frame_update_cpu_ms per-frameBand tracks render-time variance -> hyp. 2/3Band is present even on the cheapest frames -> hyp. 4 (TE/free-running panel scan), display/panel work, not a compositor toggle

Explicitly not done here

No DRM, KMS, panel, or wlroots render-path code was changed to “fix” this. Nothing here has been run on the board; every toggle above is proposed, not verified. This file records hypotheses and the commands to test them so the coordinator’s webcam-equipped board session can gather evidence efficiently, per .skills/k230-spec-change/SKILL.md’s grounding order: an observation on the board outranks everything in this file.