Repository navigation
fix(eyegaze): hold zoom on gaze loss (crosshair park), surface degraded tracker mode, merge GazeFilter - #74
Merged
Conversation
…) + diagnostic logging Juan (Argentina, Tobii PCEye + TD Control): "When I look to a letter it takes some milliseconds till the eye tracker points toward the letter. Due to this, the experience is a disaster. I believe the problem is Dasher does not take into account where I am looking at until the eye tracker points where I intended." Root cause: OnEyeGazePositionChanged called dasher_mouse_move on every raw gaze sample with NO filtering — during the transit between targets, every intermediate position zoomed the canvas toward the wrong place. New GazeFilter with three configurable stages: 1. DEAD ZONE (default 8px): gaze must move at least N px from the last SENT position — reduces jitter noise. 2. VELOCITY GATE (default 3.0 px/ms): if smoothed gaze velocity exceeds the threshold, the sample is held (saccade/transit detection). Uses exponential smoothing (alpha=0.3) so single-sample spikes don't trigger. 3. SETTLE WINDOW (default 80ms): the gaze must dwell within the dead zone for N ms before the position is sent — ensures the user has ARRIVED at their target. Settings UI in Settings > Input > Eye Tracker (visible only when EyeGaze selected): enable/disable toggle + dead zone and settle time sliders, saved debounced to access.json and applied live. Diagnostic logging to %APPDATA%\Dasher\eyegaze.log: every 30th sample logs position, velocity, and filter statistics (total/sent/dead-zone/ velocity-blocked/settle-blocked counts) — enough to diagnose latency patterns remotely without flooding the log. Signed-off-by: will wade <willwade@gmail.com>
…mode
Field-session log (eyegaze.log, Tobii IS514, 2.5h) analysis:
1. GAZE LOSS NEVER STOPPED THE ZOOM. The tracker delivered 40-100%
invalid samples (blinks, dropouts, and one 100-MINUTE 0%-valid
stretch at 2Hz), and invalid samples were dropped at the tracker
- the canvas never learned, so the last gaze position kept
driving the zoom through every gap. For an eye-tracker user that
is a runaway crosshair the instant tracking degrades - the
"quite uncomfortable" experience reported.
2. THE DEVICE RAN IN DEGRADED MODE, SILENTLY. resume_device failed
with INSUFFICIENT_PERMISSIONS (OEM IS5 sensors need elevation);
the log shows a 33 samples/s ceiling (30Hz mode) at 53-62%
validity instead of the device rating, and engine_create also
failed - all logged, none surfaced to the user.
Fixes:
- Invalid samples are now forwarded (Tobii + Windows native) and the
canvas pauses the engine immediately via the stop-outside idiom;
the next valid sample resumes. A Render-cadence staleness backstop
catches trackers that stop delivering entirely (no invalid samples
either). Stop When Pointer Leaves is enabled automatically at gaze
init (required for the pause to hold; logged).
- Trackers raise Warning (new IEyeTrackerService event) for
degraded-mode conditions; resume_device is retried once, then the
user gets a toast ("try running Dasher as administrator") + log.
- The GazeFilter (dead zone / velocity gate / settle window, from
the eyegaze diagnostic work for Juan transit-zoom report) is
merged with settings, default on, plus a bug fix: the first
sample after activation/Reset was measured against the origin,
producing a phantom 8.8 px/ms saccade that blocked the first real
positions after every gaze-loss recovery (found while writing
GazeFilterTests). The filter state resets on each loss so
recovery starts clean.
Also: eyegaze.log statistics tooling (rate/validity/gap analysis)
drove the diagnosis; heartbeat counters remain for the next session.
Signed-off-by: will wade <willwade@gmail.com>
Rework of the pause mechanism after review (5/10): the stop-outside idiom performs a full engine STOP that needs a Primary_Input click to resume (LP_START_MODE=none) - a click gaze users cannot make, so every blink would have halted typing permanently. It also required force-enabling the PERSISTENT BP_STOP_OUTSIDE parameter (mutating mouse-mode behaviour forever) and sent -10000 unconditionally. New design: park the pointer on the crosshair - the engine zero-velocity point, via dasher_get_viewport + dasher_dasher_to_screen (new NativeBridge P/Invokes, cached in Render). The zoom freezes WITHOUT stopping; the next valid sample simply moves again. No resume path needed, no settings mutated, no stop-outside interaction. Fallback: canvas centre if the viewport query fails. Also (review round 1): - GazeFilter: settle candidate tolerance is its own (dead zone 0 + settle on previously never sent ANYTHING - 1px jitter restarted the dwell forever). New regression test. - GazeFilter class doc order corrected (velocity gate runs first). - Filter reference captured to a local in the gaze callback (ConfigureGazeFilter can swap it on the UI thread). - New test: first sample not measured against the origin (named for the phantom-saccade regression fixed in the previous commit). - Tobii connect now logs process elevation - the next field log self-interprets the administrator/resume_device experiment. 149/149 tests. Signed-off-by: will wade <willwade@gmail.com>
Review loop 2 (6/10): parking on the crosshair X alone froze the zoom but kept steering the VERTICAL pan - the engine pans toward the pointer Y at a constant rate (the root-independent screen->Dasher mapping never self-corrects), so the canvas slid during exactly the events this PR targets: ~4% of screen height per 250ms blink at quarter-screen offset and default speed, ~40% at high speeds. Parking at the full crosshair (dasher 2048,2048) gives m1=m2=0: zero zoom AND zero pan. - Park at (chX, chY); _lastGazeY removed (subsumed). - Crosshair cached at gaze INIT (hold-before-first-Render window empty), retried each Render until success, failure logged once. - The reverse-zooming canvas-centre fallback is GONE: if the cache is somehow still empty the hold is skipped once and logged (pre-PR behaviour for that instant, self-heals next Render). - Geometry assumption documented (default/squish exact; square_no_xhair would reverse-zoom - not reachable via Dasher UI). - GazeFilter now fully serialised (_gazeFilterLock): Process on the callback thread vs Reset/ConfigureGazeFilter on the UI thread. - Dead filterToggleRow removed (CS8625). 149/149 tests. Signed-off-by: will wade <willwade@gmail.com>
willwade
force-pushed
the
fix/eyegaze-loss-pause-and-filter
branch
from
October 30, 2026 21:23
b29822a to
4da6ada
Compare
Settings > Input (eye gaze): "Save eye-tracker diagnostics". Detailed per-sample lines (Gaze data flowing, RAW struct dumps, gaze filter stats) now only when enabled; the ALWAYS-ON layer is lifecycle events only (connect, resume result, elevation status, heartbeat counters, gaze lost/resumed, tracker warnings) - low volume, exactly what field triage needs first. Heartbeat notes where to enable detail. Persisted as GazeDiagnostics in access.json; applied at gaze init and live from the toggle. The eyegaze.log file is the bug-report attachment for tracker problems. 149/149 tests. Signed-off-by: will wade <willwade@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Field-log findings (Tobii IS514, 2.5h diagnostic session)
resume_device → INSUFFICIENT_PERMISSIONSWorking theory: the tester's other gaze apps work because they use a different access path (Tobii's own elevated stack, or OS Eye Control); Dasher's direct stream-engine connection runs as a second-class client on an unresumed device.
What this PR does (resilience + surfacing)
dasher_get_viewport+dasher_dasher_to_screen, newly P/Invoked) — so the zoom freezes without stopping the engine. The next valid sample simply moves again: no resume click (which gaze users can't make), no persistent settings mutated, no stop-outside interaction. A Render-cadence staleness backstop catches trackers that stop delivering entirely.Warningevent;resume_deviceis retried once, then the user gets a toast ("try running Dasher as administrator") + log. Connect now also logs process elevation — the next field log self-interprets the admin experiment.Next round of logs — what we will look for
process elevation = True/False+resume_deviceresult + achieved sample rate → confirms/refutes the elevation theoryGaze lost (…) — holding on crosshair/Gaze resumedpairs → hold cadence vs. blink rateReview-loop status
Loop 1 (5/10) caught the original stop-outside design flaw (no resume path, persistent setting mutation, unguarded off-canvas send) — all reworked into the crosshair hold. Filter regressions fixed with named tests.
149/149 tests, build clean.