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
docs/evidence/drm-info.txt: drivercanaan-drm(“Canaan K230 DRM driver”) 1.0.0 20230501, dumb buffers only (/dev/dri/renderD*absent), imported via PRIME as usual for wlroots’ DRM backend.DRM_CAP_CRTC_IN_VBLANK_EVENT = 1,DRM_CAP_ATOMIC_ASYNC_PAGE_FLIPnot supported,DRM_CAP_ASYNC_PAGE_FLIP = 0. Primary plane format list omitsXRGB8888(wlroots’ default); the system runs Pixman software rendering atrender_bit_depth 6-> RGB565 (nix/shell.nix,output DSI-1 ... render_bit_depth 6).docs/evidence/flicker-after-headroom-revert.md: a different, already-investigated flicker symptom (whole-frame vertical roll, steady state, no animation) was traced to the RM69A10 being a DSI command-mode panel with its own GRAM: the init sequence issues35 00(SET_TEAR_ON) but nothing in this driver stack consumes the Tearing-Effect (TE) signal. That investigation’s conclusion was explicitly “not tearing” for that symptom (a uniform modulo-height roll), but it establishes as fact that this panel’s host-side write and the panel’s own internal scan are not synchronized by TE today. A different symptom (a torn/unstable band rather than a clean roll) from the same unsynchronized-write root cause is plausible under different timing conditions (e.g. CPU render duration varying frame to frame during an animation, rather than the steady low-load case that investigation measured).- No kernel or DRM patch in this repo wires TE (
grep -rl "tear\|te-gpio" nix/kernel* nix/patches/*.patchmatches only the Sway-side frame-timing and splash patches, none of which touch TE). nix/patches/wlroots-vglite-dumb-stride.patchshows this exact vendor driver has already had one dumb-buffer stride/alignment surprise, but it is gated onWLR_RENDERER=vgliteand does not apply to the defaultpixmanrenderer path this system actually ships.
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:
- If
tracking_present_interval_msstays close to 19.2 ms (budget p95 33.334 ms / max 100 ms passes cleanly) andframe_update_cpu_msis well under 16.7 ms: the compositor is submitting frames on a clean, on-time cadence. That rules out “the CPU render is slow and racing scanout” and points squarely at the panel/driver side (hypotheses 3-4 below) — exactly what the pixel-identical IPC capture already suggested, now confirmed for the animating case too. - If
frame_update_cpu_msis at or above ~16-19 ms (i.e. Pixman sometimes takes longer than one refresh period to build+submit a frame): that supports hypothesis 2 (host-side render occasionally overruns the frame budget). Note the caveat in the tool’s ownlimits: presentation timestamps are DRM backend feedback, not proof of what the panel actually displayed — this stage only tells you whether the compositor missed its own deadline, not whether the glass tore.
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
allow_tearing: Sway’s own tearing path (output_can_tearinsway/desktop/output.c) only applies to awlrfullscreen container withoutput->allow_tearingset and the view itself opted in via the tearing-control protocol. Neither is set anywhere in this repo (nix/shell.nix’sswayConfighas noallow_tearingline), and card-shell’s own “ordinary maximized” cards are floating, notwlrfullscreen. This mechanism is very unlikely to be engaged at all today; worth a one-line confirmation (swaymsg output DSI-1 allow_tearing no, which should already be a no-op) rather than a real suspect.WLR_SCENE_DISABLE_DIRECT_SCANOUT=1: worth trying if 1-4 are inconclusive. Direct scanout (handing a single eligible client buffer straight to the KMS primary plane, bypassing Pixman composition) is a different code path from full composition, and eligibility flips on/off frame to frame exactly at the boundaries of an animation (a maximized card is eligible when static, not while multiple scene layers are animating over it), which is a classic source of visible hitches if one of the two paths has a driver-side bug the other doesn’t. No rebuild; session restart only.WLR_DRM_NO_MODIFIERS=1: worth trying only if 1-4 point at a structured/banded corruption consistent with a stride or modifier mismatch specifically (thewlroots-vglite-dumb-stride.patchhistory shows this vendor driver has had at least one such surprise, though on a different, currently-inactive renderer path). No rebuild; session restart only.
What each experiment would mean, summarised
| Toggle | If band disappears/improves | If band unchanged |
|---|---|---|
Read tracking_present_interval_ms/frame_update_cpu_ms from a physical benchmark run | n/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=1 | Bug 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=1 | Direct-scanout/composited path transition was the trigger | Not a direct-scanout artifact |
WLR_DRM_NO_MODIFIERS=1 | Modifier/stride mismatch on this driver | Not a modifier issue |
(no toggle) optical correlation with frame_update_cpu_ms per-frame | Band tracks render-time variance -> hyp. 2/3 | Band 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.