Skip to content

Stop the extent clamp rewriting the ROI on every drag event (+ a jump detector) - #43

Merged
CSSFrancis merged 1 commit into
mainfrom
fix/roi-jump-trace
Jul 26, 2026
Merged

Stop the extent clamp rewriting the ROI on every drag event (+ a jump detector)#43
CSSFrancis merged 1 commit into
mainfrom
fix/roi-jump-trace

Conversation

@CSSFrancis

Copy link
Copy Markdown
Owner

Chasing "the ROI still jumps a bit" on a 4k in-situ movie. Adds an always-on detector for an integrating ROI that moves on its own, and fixes the first thing it found — within one test run.

The bug

_clamp_extent compared the span against the cap with a bare >. A span resting exactly on the cap computes fractionally over it in floating point:

1.95 - 1.15 = 0.8000000000000003  >  0.8

So the clamp rewrote the widget on nearly every pointer_move of a capped drag. Each rewrite pushes a python-sourced geometry echo back to the renderer mid-drag — precisely the feedback loop the widget-side max_extent cap exists to avoid. Now compared with a relative tolerance; same for the 2-D rectangle, which can hit it too.

Caught by the detector''s own ring buffer on its first run:

[ROI-JUMP] 1-D span: clamp rewrote geometry (widget cap did not hold)
  [1.15,1.95] -> clamp [1.15,1.95]  n=16 uniq=16 scale=0.05 cap=0.8

— before and after print identically at 4 s.f., which is the whole point.

The detector

The symptom is intermittent and the interesting state (what the renderer sent, what the clamp did to it, what indices came out) is gone by the time anyone looks. RoiTrace keeps a ring of the last 8 pointer events and dumps it at WARNING when the geometry moves in a way no single pointer gesture explains:

  • the fallback clamp rewrote geometry — with the widget-side cap this should be unreachable during a drag, so a hit means that assumption broke;
  • the ROI both moved and resized in one event — dragging an edge moves one end, dragging the body moves both by the same amount, so both ends moving by different amounts didn''t come from one gesture;
  • every index collapsed onto one nav position while the ROI still covers more than one — the "span pinned at the last frame" shape.

Design choices worth stating:

  • Always on, silent until something is wrong. An env var would mean reproducing the bug twice — once to notice, once to capture.
  • Distance is deliberately not a rule. A fast mouse move legitimately translates the ROI a long way in one event; a threshold there would cry wolf on every flick and bury the real hit.
  • The ring is the point. One anomalous event says little; the run-up says what the user was doing.
  • SPYDE_ROI_TRACE=1 logs every event for a deliberate reproduction session.

Costs a few float comparisons and a deque append per pointer event.

What this rules out

Measured first, on a calibrated 0.05 s/frame movie axis, because the documented suspect was _clamp_extent''s lower-edge anchoring:

  • cap at construction: 0.8 (= 16 frames) — correct, not stale
  • assigning a 6 s span: the widget clamped it, before _clamp_extent ran
  • the lower-edge-anchoring fallback: never fired

So the remaining jumping is not the documented phantom-movement. The detector is there to identify what it actually is.

Tests

26 new, mostly about what must not fire — pure translation, dragging either edge, a fast flick, a single-point region, the first event. Plus the epsilon regression pinned directly (_clamp_extent must not rewrite a span sitting exactly on the cap), and a real Session drag asserting silence. 367 selector/nav/region/movie tests pass.

… event

Adds RoiTrace, an always-on detector for an integrating ROI that moves on its
own, and fixes the first thing it found.

THE BUG. _clamp_extent compared the span against the cap with a bare `>`. A
span resting EXACTLY on the cap computes fractionally OVER it in floating
point -- 1.95 - 1.15 = 0.8000000000000003 > 0.8 -- so the clamp rewrote the
widget on nearly every pointer_move of a capped drag. Each rewrite pushes a
python-sourced geometry echo back to the renderer mid-drag, which is exactly
the feedback loop the widget-side max_extent cap exists to avoid. Now compared
with a relative tolerance; same for the 2-D rectangle, which can hit it too.

THE DETECTOR. The symptom is intermittent and the interesting state (what the
renderer sent, what the clamp did to it, what indices came out) is gone by the
time anyone looks. RoiTrace keeps a ring of the last 8 pointer events and dumps
it at WARNING when the geometry moves in a way no single pointer gesture
explains:

  * the fallback clamp rewrote geometry -- with the widget-side cap this should
    be unreachable during a drag, so a hit means that assumption broke;
  * the ROI both MOVED and RESIZED in one event -- dragging an edge moves one
    end, dragging the body moves both by the SAME amount, so both ends moving
    by DIFFERENT amounts did not come from one gesture;
  * every index collapsed onto one nav position while the ROI still covers more
    than one -- the "span pinned at the last frame" shape.

It is always on and silent until one of those fires. Requiring an env var would
mean reproducing the bug twice: once to notice, once to capture. The rules are
deterministic -- distance is deliberately NOT one of them, because a fast mouse
move legitimately translates the ROI a long way in a single event and a
threshold there would cry wolf on every flick. SPYDE_ROI_TRACE=1 logs every
event for a deliberate reproduction session.

Measured beforehand and worth recording, since it rules out the obvious
suspects on a calibrated 0.05 s/frame movie axis: the cap is CORRECT at
construction (0.8 = 16 frames, not stale), the widget clamps the drag itself,
and the lower-edge-anchoring fallback never fires. So the remaining jumping is
not the documented _clamp_extent phantom-movement.
@CSSFrancis
CSSFrancis merged commit 7f6cc7e into main Jul 26, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant