diff --git a/CHANGELOG.md b/CHANGELOG.md index d4620e98..4fdaa3a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -194,6 +194,12 @@ floor on the release that ships them): `api_extractors`). No known plugin imports it. A plugin that does must use `src.common` or its own copy of the code. +- `display.scan_order_compensation` (`"auto"` by default): while something + scrolls at one pixel per refresh, one half of each panel is shown a refresh + behind the other, which removes the 1px step a 1:N-scan panel shows across + its middle. Only for layouts whose row order is known; `"off"` disables it. + See `docs/SCROLL_PERFORMANCE.md`, "A tear across the middle on fast scrolls". + ## 3.5.0 New modules a plugin may import via `src.*` (floor on 3.5.0): diff --git a/docs/SCROLL_PERFORMANCE.md b/docs/SCROLL_PERFORMANCE.md index b588adfc..06938493 100644 --- a/docs/SCROLL_PERFORMANCE.md +++ b/docs/SCROLL_PERFORMANCE.md @@ -325,10 +325,9 @@ of roughly offset ≈ scroll speed × refresh period ``` -Each frame already reaches the panel whole (`SwapOnVSync` swaps complete frames -between refreshes), so there is nothing to fix in the render path; the shift is -created inside a single refresh. Other panel heights show it too, at the point -where their two scan halves meet. +Each frame reaches the panel whole (`SwapOnVSync` swaps complete frames between +refreshes); the shift is created inside a single refresh. Other panel heights +show it too, at the point where their two scan halves meet. On the 2×128×64 chain above, which refreshes at about 130 Hz flat out (7.7 ms per pass): @@ -339,7 +338,37 @@ On the 2×128×64 chain above, which refreshes at about 130 Hz flat out | 100 px/s | ~0.8 px | | 150 px/s | ~1.2 px, plainly visible | -### What changes it +### What the display does about it + +At one pixel per refresh, the fastest crisp speed, the step is exactly one +refresh's worth of motion, so it can be cancelled: show one half of the panel +a refresh behind the other -- the half whose row at the seam lights at the +start of each refresh. The two rows either side of the seam then show the same +moment again. What is left is a +lean of one pixel per half from top to bottom, continuous across the panel, +which reads as nothing where the step read as a tear. `DisplayManager` does +this while something scrolls at one frame per refresh +(`display.scan_order_compensation`, `"auto"` by default, `"off"` to disable; +the geometry is in `src/scan_order.py`). The lagging rows come from the +previous frame the display presented, so it works for Vegas and every plugin +ticker without knowing how they scroll. + +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 +frame. With the compensation the step is gone at 90 px/s. + +It is left off where the row order is unknown or the maths does not hold: + +- **Slower speeds**, where each frame is held for two or more refreshes. The + offset there is half a pixel or less, and cancelling it would need a lag of + a fraction of a frame. +- **Other layouts:** pixel mappers other than a 0 or 180 degree rotation + (U-mapper, 90/270), non-zero `multiplexing`, interlaced `scan_mode`, and a + canvas remapped to another height (double-sided mode). +- **The emulator,** which has no scan order. + +### When it cannot apply Only a shorter scan period (a faster refresh) or a slower scroll. Measure what the panel actually achieves first. The library prints the rate with a carriage @@ -370,7 +399,7 @@ on its own output and set `parallel` to the number of outputs used and should roughly double the refresh rate and halve the offset. That is a cable change, so measure again afterwards. -Short of rewiring, keep fast scrolls moderate: at the default 50 px/s the +Short of rewiring, keep fast scrolls moderate on those layouts: at 50 px/s the offset is under half a pixel. ## Rebuilding the binding