mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-05 06:45:09 +00:00
fix(display): end the scroll state at scroller-to-static handovers (#716)
A static plugin screen that follows a scroller no longer starts with the ticker's lagging rows on scan-compensated panels, and the 1 Hz loop's second frame is no longer recorded as a ~1 s mid-scroll freeze / Render stall. The display controller calls DisplayManager.end_scroll_for_static_screen() before a static screen's first display() (clears the scan history; _scan_segments passes its frames through in one swap) and set_scrolling_state(False) after it; the scroller's hold stays until then, so late-frame counts are unchanged. A screen's first frame is tagged 'handover': gaps of 250 ms or more before it go to the additive handover_freezes (frame_soak prints 'Handover gaps'), not freezes. The display thread is named display-<plugin id>. The WiFi notice and the schedule-off blank are not covered yet (docs list them as a follow-up). ledpi A B B A soak (20 min each, --preview): main 0.118% / 0.113% late with 6 / 3 freezes; with this and #717 0.107% / 0.104% late, 0 freezes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -232,7 +232,11 @@ its prefetch inline. Verify with a Vegas soak on ledpi, A/B.
|
||||
Plugins declare `frame_policy` (STATIC, PERIODIC(hz), ANIMATED(fps),
|
||||
SCROLL). `_needs_high_fps` becomes the mapping for legacy plugins
|
||||
(`needs_high_fps`, the `static-image` special case, `enable_scrolling`),
|
||||
and its per-screen INFO line drops to DEBUG.
|
||||
and its per-screen INFO line drops to DEBUG. It is already read twice per
|
||||
screen: once quietly before the first frame, so `_dispatch_first_frame` can
|
||||
end the previous scroll for a screen that runs the 1 Hz loop
|
||||
(`_start_screen_handover`), and once after it to pick the loop. A declared
|
||||
policy answers both.
|
||||
|
||||
## How each stage is verified
|
||||
|
||||
|
||||
@@ -341,12 +341,13 @@ service's user.
|
||||
| line | what it tells you |
|
||||
|---|---|
|
||||
| **Late frames** | Frames presented one or more refreshes after they were due: the panel showed the previous frame again, a visible hitch. **The pass/fail number**, 0.1% by default (`--max-late-pct`). Only intervals between two scrolling frames count, and a frame held for `frame_hold` refreshes is due `frame_hold` refreshes after the last. |
|
||||
| **Freezes** | Gaps of 250 ms or more inside a scroll: recomposes, plugin handovers, blocking calls on the render thread. Reported but not failed on, because some are handovers between plugins rather than faults. A gap still counts when the display's scroll state went missing for one frame across it, as long as scrolling resumes within 1 s: both of that frame's intervals count. Two static frames in a row end the scroll. (The state expires after 2 s without scroll activity, and plugins can clear it from their own `display()`.) The late and early rates are over frames judged against a known refresh period, which the recorder adopts once two windows in a row agree on it. |
|
||||
| **Freezes** | Gaps of 250 ms or more inside a scroll: recomposes, plugin handovers the display controller does not tag (see *Handover gaps*), blocking calls on the render thread. Reported but not failed on, because some are handovers between plugins rather than faults. A gap still counts when the display's scroll state went missing for one frame across it, as long as scrolling resumes within 1 s: both of that frame's intervals count. Two static frames in a row end the scroll. (The state expires after 2 s without scroll activity, and plugins can clear it from their own `display()`.) The late and early rates are over frames judged against a known refresh period, which the recorder adopts once two windows in a row agree on it. Handovers to a static screen no longer show up here: the display controller ends the scroll state after a static screen's first frame, where it used to linger for 2 s and turn the 1 Hz loop's second frame into a ~1 s "freeze" (17 of 31 `Render stall over` lines on ledpi, 2026-09-15 to 10-01). **Freeze counts from before and after that change are not comparable.** |
|
||||
| **Handover gaps** | Gaps of 250 ms or more from a scroll's last frame to the next screen's first: the next plugin drawing, not a scroll stalling. The display controller tags that first frame `handover`, and these gaps are counted here instead of under Freezes (`handover_freezes` in the stats; the `handover` row under *after work* counts the same ones in its freezes column). Every turn's first frame is tagged, also when the rotation comes back to the same mode (a one-mode rotation, a pinned on-demand mode, live priority holding a screen), so a scroller rebuilding its content at the start of a turn is counted here; measure work on that rebuild with this line, not Freezes. Missing from stats written by an older service, whose freezes include them. |
|
||||
| **blit** | Copying the frame into the matrix canvas (`SetImage`). It grows with width × height × `pwm_bits`: ~5.5 ms at 512×64 with 8 bits on a Pi 4. It is the biggest fixed cost, and it sets the refresh rates a rig can hold one pixel per refresh at. |
|
||||
| **wait** | Time blocked in `SwapOnVSync`, i.e. the slack left in each refresh. A p50 near zero means the rig has no headroom and anything extra lands a frame late. |
|
||||
| **work** | Everything else between two frames: drawing, scrolling, and waiting for the GIL. A wide gap between its p50 and p99 is another thread getting in the way. |
|
||||
| **Binding** | `STOCK` means the rgbmatrix binding holds the GIL through the vsync wait, which starves every other thread. See *Rebuilding the binding*. |
|
||||
| **after work** | Frames presented straight after tagged render-thread work, with their own late rate: `extend` and `compose` (Vegas building its strip), `patch` (live elements, once they land). A kind whose late rate sits well above the overall one is the work making frames late. Shown only when something tagged its work. |
|
||||
| **after work** | Frames presented straight after tagged render-thread work, with their own late rate: `extend` and `compose` (Vegas building its strip), `patch` (live elements, once they land), `handover` (a new screen's first frame). A kind whose late rate sits well above the overall one is the work making frames late. Shown only when something tagged its work. |
|
||||
|
||||
The refresh rate is estimated from the frames themselves (swaps that block on
|
||||
vsync can only land on refresh boundaries). Cross-check it with
|
||||
@@ -360,10 +361,13 @@ A/B two of them. A live-API workload drifts over time.
|
||||
The soak says how often; the service's log says why. A scroll that presents no
|
||||
frame for 250 ms logs `Render stall:` with the stack of the render thread and
|
||||
the top of every other thread's, and whether the whole interpreter was blocked
|
||||
(C code holding the GIL) rather than one thread. To see what is behind the
|
||||
shorter hitches, run the service with `LEDMATRIX_STALL_WATCHDOG_MS=30`, which
|
||||
dumps at three refreshes late instead: its extra polling costs a little GIL
|
||||
time of its own, so do that on a diagnostic run, not a soak you are grading.
|
||||
(C code holding the GIL) rather than one thread. A stall while the next
|
||||
screen's first `display()` is still drawing says `in a handover gap` instead of
|
||||
`mid-scroll`; that call runs on a thread named `display-<plugin id>`. To see
|
||||
what is behind the shorter hitches, run the service with
|
||||
`LEDMATRIX_STALL_WATCHDOG_MS=30`, which dumps at three refreshes late instead:
|
||||
its extra polling costs a little GIL time of its own, so do that on a
|
||||
diagnostic run, not a soak you are grading.
|
||||
`LEDMATRIX_STALL_WATCHDOG=0` turns it off.
|
||||
|
||||
### Results: hdpi, 2026-09-24
|
||||
@@ -540,6 +544,31 @@ for the rest, so it steps one refresh after the rest rather than one frame.
|
||||
That costs a second blit inside the refresh after the first swap, so it is
|
||||
skipped when a blit takes more than half a refresh.
|
||||
|
||||
A plugin screen that runs the 1 Hz loop after a scroll is not composed: the
|
||||
display controller calls `DisplayManager.end_scroll_for_static_screen()` before
|
||||
its first `display()`, so the frames that call presents go out as drawn, in one
|
||||
swap each, instead of with the lagging half taken from the scroller's last
|
||||
frame.
|
||||
|
||||
Other screens that follow a scroll still are, while the scroll state lasts (it
|
||||
expires 2 s after the scroller's last frame). Their first frame takes its
|
||||
lagging half from the scroller's last frame: for one refresh after a held
|
||||
scroll, and after a scroll at one frame per refresh until the next frame
|
||||
replaces it. They are:
|
||||
|
||||
- the blank shown when the schedule turns the panel off. It is redrawn once a
|
||||
minute while the panel is off, so half of the scroller's last frame can stay
|
||||
lit for up to 60 s;
|
||||
- the WiFi status message, until the next pass half a second later;
|
||||
- a screen that runs the high-FPS loop without scrolling (an older
|
||||
`static-image`, which is forced into it), until its next frame.
|
||||
|
||||
The frame stats still time those two as frames of the old scroll: a WiFi
|
||||
notice that preempts a scroller records up to three 0.5-1 s freezes, and the
|
||||
schedule-off blank a `Render stall ... mid-scroll`. Ending the scroll state
|
||||
before the schedule-off blank and the WiFi message is a follow-up, the
|
||||
schedule-off blank first.
|
||||
|
||||
Checked on hdpi (4×128×64 on one chain, rotated 180, 2026-09-24) before it was
|
||||
written: `scan_mode: 1` (interlaced) made the step vanish but turned moving
|
||||
edges grainy, and halving the speed halved it, so it is the scan and not a torn
|
||||
|
||||
Reference in New Issue
Block a user