mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-10 09:06:36 +00:00
feat(vegas): prefetch_gate on by default, after an A/B/C soak on hdpi
Two runs per arm, about 81,000 frames each, order A B C C B A: A step 1 as is 0.90% late, 20.1 per 10k two+ refreshes late B switch_interval_ms 1 0.78% late, 15.8 per 10k C prefetch_gate 0.60% late, 2.5 per 10k No freezes in any arm, and the next group was ready at every strip extension, so parking the prefetch thread (3-6s per 8-minute run) cost nothing visible. The gate is now on unless vegas_scroll.prefetch_gate is false; on a stock binding it cannot work and says so at INFO once a run rather than warning on every install. switch_interval_ms stays off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -128,6 +128,8 @@ Read by `src/vegas_mode/config.py` (`VegasScrollConfig.from_config`). See
|
||||
| `min_cut_gap` | int, `6` |
|
||||
| `continuous_scroll` | bool, `true` |
|
||||
| `offscreen_prefetch` | bool, `true` — render every plugin's ticker content on the background thread, each on its own canvas. `false` restores handing canvas-bound plugins to the render thread, one pause at a time. Temporary; see [OFFSCREEN_RENDERING.md](OFFSCREEN_RENDERING.md) |
|
||||
| `prefetch_gate` | bool, `true` — let that background thread run Python only while the render thread is waiting for the panel, so the render thread never waits for the GIL when a refresh comes round. Only takes effect with the rebuilt rgbmatrix binding (`scripts/build_rgbmatrix_nogil.sh`). See [OFFSCREEN_RENDERING.md](OFFSCREEN_RENDERING.md) |
|
||||
| `switch_interval_ms` | float, `0` — experimental: shorten Python's GIL switch interval to this many ms while Vegas runs. `0` leaves the default (5 ms) alone |
|
||||
| `smooth_scroll` | bool, `true` — move a whole number of pixels per panel refresh, locked to vsync. `scroll_speed` is snapped to the nearest speed the panel can show that way (at 95Hz: 95, 47.5, 31.7 px/s…), measured against the panel's real refresh rate once scrolling starts |
|
||||
| `sub_pixel_blend` | bool, `false` — the older smoothing: advance by elapsed time and blend neighbouring pixel columns. Looks anti-aliased in the web preview but shimmers on the panel and is not locked to the refresh. Overrides `smooth_scroll` when on |
|
||||
| `extend_threshold_screens` | float, `2.0` |
|
||||
|
||||
@@ -21,6 +21,28 @@ moved to the prefetch thread still needs the GIL, and the render thread waits
|
||||
for it (risk 5 below). The late rate did not improve overall. The 1–2 s
|
||||
freezes appear in both builds and have a separate, not yet identified cause.
|
||||
|
||||
The GIL fix, measured on hdpi (90 px/s, `pwm_bits` 8, preview open, 8-minute
|
||||
runs after a 2-minute warm-up, order A B C C B A, 2026-09-24). Each arm pools
|
||||
two runs, about 81,000 frames:
|
||||
|
||||
| arm | late | by 1 | 2 | 3–5 | 6+ | 2+ late per 10k frames | freezes |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| A: step 1 as is | 0.90% | 575 | 64 | 91 | 9 | 20.1 | 0 |
|
||||
| B: `switch_interval_ms` 1 | 0.78% | 510 | 105 | 23 | 2 | 15.8 | 0 |
|
||||
| C: `prefetch_gate` | **0.60%** | 471 | 11 | 7 | 2 | **2.5** | 0 |
|
||||
|
||||
The gate removes the frames the render thread spent waiting for the GIL, and
|
||||
it costs the prefetch nothing that shows: it parked the thread for 3–6 s per
|
||||
run, and the next group was ready at every strip extension in every arm.
|
||||
`prefetch_gate` is therefore on by default; `switch_interval_ms` stays an
|
||||
off-by-default experiment. What is left is almost all one refresh late, which
|
||||
is the per-frame budget (a 6.75 ms p50 blit in a refresh the panel holds at
|
||||
83–85 Hz while rendering), not contention.
|
||||
|
||||
The runs restart the service, so the hourly sports refresh never fell inside
|
||||
one. That refresh is its own case: about twenty ESPN chunk-fetch threads at
|
||||
once, which the gate does not cover (it gates only the prefetch thread).
|
||||
|
||||
## The problem
|
||||
|
||||
Vegas mode builds its ticker from every plugin's content. Most of that work
|
||||
@@ -285,9 +307,8 @@ updates are disabled while sync is active.
|
||||
it, and a waiting thread only gets it back after the switch interval
|
||||
(default 5 ms). Expect some single-refresh late frames while a prefetch
|
||||
runs. Measure with the soak. A render process separate from plugin work
|
||||
is the structural answer (the "native presenter" step). Two opt-in
|
||||
experiments try to get most of the way first, both off by default until
|
||||
the soak says otherwise:
|
||||
is the structural answer (the "native presenter" step). Two experiments
|
||||
get most of the way first (results under Status, above):
|
||||
- `vegas_scroll.switch_interval_ms` lowers the switch interval for a Vegas
|
||||
run (1 ms is the obvious try), so the render thread waits at most that
|
||||
long behind bytecode. It does nothing for a C call that keeps the GIL.
|
||||
@@ -297,7 +318,8 @@ updates are disabled while sync is active.
|
||||
parks it the rest of the time. That covers C calls too, since the gate is
|
||||
checked before each one starts. It never parks the thread while it holds
|
||||
a lock the render thread takes, and never for more than 50 ms. It needs
|
||||
the rebuilt binding, which releases the GIL during the swap.
|
||||
the rebuilt binding, which releases the GIL during the swap. On by
|
||||
default.
|
||||
|
||||
## What this does not fix
|
||||
|
||||
|
||||
@@ -87,16 +87,17 @@ class VegasModeConfig:
|
||||
# 5ms. Plugin rendering on the prefetch thread and plugin updates hold the
|
||||
# GIL in Pillow and Python code, and a frame waiting its turn for 5ms at a
|
||||
# time misses its refresh. 0 leaves the interpreter default alone.
|
||||
# Experimental: measured with scripts/frame_soak.py before it gets a default.
|
||||
# Experimental. On hdpi it did less than prefetch_gate (0.90% -> 0.78% late
|
||||
# against 0.60%; see docs/OFFSCREEN_RENDERING.md), so it stays off.
|
||||
switch_interval_ms: float = 0.0
|
||||
|
||||
# Let the prefetch thread run Python only while the render thread is
|
||||
# blocked waiting for vsync, and park it the rest of the time, so the
|
||||
# render thread never waits for the GIL when its refresh comes round. Needs
|
||||
# a binding that releases the GIL in SwapOnVSync; ignored otherwise.
|
||||
# Experimental: see src/common/render_gate.py and measure with
|
||||
# scripts/frame_soak.py before it gets a default.
|
||||
prefetch_gate: bool = False
|
||||
# a binding that releases the GIL in SwapOnVSync; off otherwise. On hdpi
|
||||
# it cut frames two or more refreshes late eightfold, and late frames
|
||||
# overall from 0.90% to 0.60%. See src/common/render_gate.py.
|
||||
prefetch_gate: bool = True
|
||||
|
||||
# Keep one continuous strip, extending it with the next group of plugins as
|
||||
# the scroll approaches the end, instead of composing a fresh strip and
|
||||
@@ -229,7 +230,7 @@ class VegasModeConfig:
|
||||
continuous_scroll=vegas_config.get('continuous_scroll', True),
|
||||
offscreen_prefetch=bool(vegas_config.get('offscreen_prefetch', True)),
|
||||
switch_interval_ms=float(vegas_config.get('switch_interval_ms', 0.0) or 0.0),
|
||||
prefetch_gate=bool(vegas_config.get('prefetch_gate', False)),
|
||||
prefetch_gate=bool(vegas_config.get('prefetch_gate', True)),
|
||||
extend_threshold_screens=float(
|
||||
vegas_config.get('extend_threshold_screens', 2.0)),
|
||||
auto_trim=vegas_config.get('auto_trim', True),
|
||||
|
||||
@@ -340,12 +340,14 @@ class VegasModeCoordinator:
|
||||
if getattr(self.display_manager, 'render_gate', None) is not None:
|
||||
return
|
||||
releases = render_gate.swap_releases_gil()
|
||||
if releases is None:
|
||||
logger.debug("Vegas: no prefetch gate -- no hardware binding loaded")
|
||||
return
|
||||
if not releases:
|
||||
logger.warning(
|
||||
"Vegas: prefetch_gate ignored -- %s",
|
||||
"this rgbmatrix binding keeps the GIL in SwapOnVSync "
|
||||
"(scripts/build_rgbmatrix_nogil.sh)" if releases is False
|
||||
else "no hardware binding loaded")
|
||||
# On by default, so this is every stock install: say so once per
|
||||
# run, not as a warning.
|
||||
logger.info("Vegas: no prefetch gate -- this rgbmatrix binding keeps "
|
||||
"the GIL in SwapOnVSync (scripts/build_rgbmatrix_nogil.sh)")
|
||||
return
|
||||
gate = render_gate.RenderGate()
|
||||
# Locks the render thread takes too: never park the prefetch holding one.
|
||||
|
||||
@@ -311,10 +311,16 @@ class TestVegasWiring:
|
||||
c.display_manager = type("DM", (), {"render_gate": None})()
|
||||
return c
|
||||
|
||||
def test_off_by_default(self, monkeypatch):
|
||||
def test_on_by_default(self, monkeypatch):
|
||||
monkeypatch.setattr(render_gate, "swap_releases_gil", lambda: True)
|
||||
c = self._coordinator()
|
||||
c._install_render_gate()
|
||||
assert isinstance(c.display_manager.render_gate, RenderGate)
|
||||
|
||||
def test_can_be_turned_off(self, monkeypatch):
|
||||
monkeypatch.setattr(render_gate, "swap_releases_gil", lambda: True)
|
||||
c = self._coordinator(prefetch_gate=False)
|
||||
c._install_render_gate()
|
||||
assert c.display_manager.render_gate is None
|
||||
|
||||
def test_installed_for_the_run_and_removed_after(self, monkeypatch):
|
||||
@@ -338,12 +344,12 @@ class TestVegasWiring:
|
||||
|
||||
def test_read_from_config(self):
|
||||
from src.vegas_mode.config import VegasModeConfig
|
||||
on = VegasModeConfig.from_config(
|
||||
{"display": {"vegas_scroll": {"prefetch_gate": True}}})
|
||||
assert on.prefetch_gate is True
|
||||
assert on.to_dict()["prefetch_gate"] is True
|
||||
off = VegasModeConfig.from_config(
|
||||
{"display": {"vegas_scroll": {"prefetch_gate": False}}})
|
||||
assert off.prefetch_gate is False
|
||||
assert off.to_dict()["prefetch_gate"] is False
|
||||
assert VegasModeConfig.from_config(
|
||||
{"display": {"vegas_scroll": {}}}).prefetch_gate is False
|
||||
{"display": {"vegas_scroll": {}}}).prefetch_gate is True
|
||||
|
||||
def test_the_prefetch_runs_inside_the_gate(self):
|
||||
from src.vegas_mode.render_pipeline import RenderPipeline
|
||||
|
||||
Reference in New Issue
Block a user