Gaze panel: shared buffers, no flicker; calibration dots wait for a click

The panel drew each picture with SetOverlayRaw, a new texture upload every
time: the full calibration's 1024x768 picture flickered on every change, and
in a live test the headset kept showing the last calibration picture after the
panel had drawn the fit check (2026-10-01). Now, as screens/keyboard.cpp does,
it writes into three linear DMA-BUFs SteamVR imported once (the size of the
biggest panel; texture bounds show the part in use) and switches between them.
A show makes the overlay visible once its first picture is in.

The full calibration's and the five-dot check's dots wait for a left click or
Meta+J while you look at the dot, in place of capturing any steady gaze (which
can be a look somewhere else); no capture ring, no 8 s skip, and the check
closes after 2 minutes without a click. The quick check still captures itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DeeJanuzandClaude Opus 5.5 committed 2026-10-01 11:27:30 -06:00
1 parent 45b1b06cdb
commit da3e3d66a7
4 files changed
+130 -23

No files matched your search

+17 -15
View File
@@ -27,11 +27,14 @@ directions: look at each one.
adjust the headset. A left click or Meta+J runs its guided check (dots, then looks
down, up, left and right); a right click or Meta+K closes it, as does FIT_TIMEOUT.
A dot captures itself: from CHECK_SETTLE after it shows (the eyes getting there), once the gaze
has held within CHECK_SPREAD for CHECK_WINDOW (the probe's max spread and capture time). It's
the gaze holding still that counts, not where the tracker puts it, so it works however far off
the tracker is. A left click or Meta+J (the pointer helper's "calaccept") takes it now; a right
click or Meta+K ("calquit") closes the panel. The pointer hides meanwhile ("calpanel 1",
The quick check's dot captures itself: from CHECK_SETTLE after it shows (the eyes getting
there), once the gaze has held within CHECK_SPREAD for CHECK_WINDOW (the probe's max spread and
capture time). It's the gaze holding still that counts, not where the tracker puts it, so it
works however far off the tracker is; a left click or Meta+J (the pointer helper's
"calaccept") takes it now. The full calibration's and five's dots wait for that click: you
click when you're looking at the dot (the user asked for that: a steady gaze isn't always on
the dot), and the gaze held still up to then is taken (ACCEPT_SPREAD). They wait as long as
it takes, up to CLICK_IDLE. A right click or Meta+K ("calquit") closes the panel. The pointer hides meanwhile ("calpanel 1",
renewed every second; the helper shows it again by itself when that stops).
What a capture teaches:
@@ -73,7 +76,7 @@ CHECK_SETTLE = 0.45
CHECK_WINDOW = 0.6
CHECK_SPREAD = 1.0 # degrees
ACCEPT_SPREAD = 2.5 # degrees: a capture asked for (calaccept) takes this much
DOT_TIMEOUT = 8.0 # seconds a dot of five or full waits; then it's skipped
CLICK_IDLE = 120.0 # seconds a dot of five or full waits for its click; then the check closes
QUICK_TIMEOUT = 6.0
QUICK_COOLDOWN = 120.0
DON_DELAY = 3.0 # seconds of eyes after AWAY_MIN without: the headset went on
@@ -391,9 +394,9 @@ class Checks:
if c["kind"] == "full":
self.to_panel(f"bg {ROUND_BG[rnd]}")
self.to_panel(f"title Gaze calibration: {ROUND_NAMES[rnd]} round, {rnd + 1} of 3")
self.to_panel("text Look at the dot. Left click or Meta+J: now. Right click or Meta+K: stop")
self.to_panel("text Look at the dot and click (left click or Meta+J). Right click or Meta+K: stop")
elif c["kind"] == "five":
self.to_panel(f"text Look at the dot ({c['i'] + 1} of {len(c['dots'])})")
self.to_panel(f"text Look at the dot and click ({c['i'] + 1} of {len(c['dots'])})")
else:
self.to_panel("text Look at the dot")
self.to_panel(f"dot {yaw:.3f} {pitch:.3f} look")
@@ -437,10 +440,10 @@ class Checks:
run.append((now, s, g[0], g[1]))
window = [p for p in run if p[0] >= now - CHECK_WINDOW]
held = now - run[0][0]
# The ring fills in quarters: each step is a new picture for the panel to upload, so
# few of them keep it solid.
# The quick check's ring fills in quarters: each step is a new picture for the panel, so
# few of them keep it solid. The others wait for the click (see the top): no ring.
progress = math.floor(min(1.0, held / CHECK_WINDOW) * 4) / 4
if progress != self.last_progress:
if c["kind"] == "quick" and progress != self.last_progress:
yaw, pitch, _ = c["dots"][c["i"]]
self.to_panel(f"dot {yaw:.3f} {pitch:.3f} capture {progress:.2f}")
self.last_progress = progress
@@ -449,7 +452,7 @@ class Checks:
if sd <= ACCEPT_SPREAD:
self.capture(window)
return
if held >= CHECK_WINDOW and len(window) >= 20:
if c["kind"] == "quick" and held >= CHECK_WINDOW and len(window) >= 20:
sd, _ = spread([(p[2], p[3]) for p in window])
if sd <= CHECK_SPREAD:
self.capture(window)
@@ -687,9 +690,8 @@ class Checks:
return
if c["kind"] == "quick" and now - c["started"] > QUICK_TIMEOUT:
self.close("ignored")
elif c["kind"] != "quick" and now - c["shown"] > DOT_TIMEOUT:
log(f"{c['kind']} check dot {c['i'] + 1}: skipped (the gaze didn't hold still)")
self.skip()
elif c["kind"] != "quick" and now - c["shown"] > CLICK_IDLE:
self.close(f"no click on dot {c['i'] + 1} for {CLICK_IDLE:.0f} s")
def periodic(self):
svc = self.svc