fix(display): a frame the preview throttle skipped still reaches the snapshot

The preview snapshot (/api/v3/display/current, the web UI's live preview)
is written only from update_display(), at most once per write interval.
A changed frame pushed inside that interval was skipped and left for the
next update_display() -- which a screen that draws its card once and then
holds it never makes.

Soccer's recent/upcoming cards skip redundant redraws. After an on-demand
start, the card is pushed a few milliseconds after the start's clear wrote
a black frame, so the throttle skipped it and the preview stayed black for
the whole 15 s screen while the panel showed the card. The next screen
looked fine because its first push came after the interval had passed.

DisplayManager now records a skipped changed frame as owed, and the render
loop (_display_once) calls write_owed_snapshot() after each frame, which
writes it once the interval has passed. The cadence is unchanged, an
unchanged frame is never re-encoded, and nothing runs when no frame is owed.
Golden run-loop traces are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-10-03 22:30:01 -04:00
co-authored by Claude Opus 5.5
parent 41192b9588
commit 9b7217efb3
4 changed files with 230 additions and 2 deletions
+15
View File
@@ -19,6 +19,21 @@ accepts both, but the store flags the old spelling as deprecated
## Unreleased
### Fixed
- The web preview and `/api/v3/display/current` no longer stay black for a
whole screen that draws its card once and then holds it. The snapshot is
written from `update_display()` at most once per write interval, so a frame
pushed inside that interval was skipped and left for the next
`update_display()` -- which such a screen never makes. Soccer's
recent/upcoming cards skip redundant redraws, and the first one after an
on-demand start lands a few milliseconds after the start's clear wrote a
black frame: on ledpi the preview showed 0 lit pixels for the whole 15 s
while the panel showed the card. `DisplayManager` now remembers a skipped
changed frame, and the render loop writes it (`write_owed_snapshot()`)
once the interval has passed. The cadence is unchanged, and nothing extra
runs when no frame is owed.
### Cheap per-frame and per-fetch savings
- `BaseOddsManager.get_odds()` no longer pretty-prints every odds response