VR keyboard controller bridge: no traffic while idle

The relay no longer enables the Runtime domain on systemui (bindings work
without it; it streamed the page's console and context events). Full-rate
frames only while a tip is within 10 cm of the keyboard or a trigger is
pulled (not for a laser resting on it); idle frames every 1 s instead of
250 ms. Nothing while the keyboard is hidden.
This commit is contained in:
Pierre Kisters committed 2026-10-01 03:33:07 +02:00
1 parent 224715d926
commit 63d589c261
4 files changed
+14 -12

No files matched your search

+3 -3
View File
@@ -219,9 +219,9 @@ to the laser only. Depends on Steam and SteamVR UI internals (see
animated `trigger` component (not yet verified). The animated `trigger` component (not yet verified). The
`vr-keyboard-controllers-relay` user service carries these frames to `vr-keyboard-controllers-relay` user service carries these frames to
Steam's `SharedJSContext`: up to ~90 Hz (45 Hz and more under load) Steam's `SharedJSContext`: up to ~90 Hz (45 Hz and more under load)
while a hand is within 10 cm of the keyboard, its laser is on it or its while a hand's tip is within 10 cm of the keyboard or its trigger is
trigger is pulled, else every 250 ms, and only while SteamVR shows the pulled, else every second; nothing while the keyboard is hidden. The
keyboard. The laser hit matches SteamVR's laser to a few px. laser hit matches SteamVR's laser to a few px.
- **Touch typing** (patch `vr-keyboard-touch`, `SharedJSContext`): per hand - **Touch typing** (patch `vr-keyboard-touch`, `SharedJSContext`): per hand
`tracker.js` turns the tip's path into press and release, then calls the `tracker.js` turns the tip's path into press and release, then calls the
keyboard's own `HandleTouchStart` / `HandleTouchEnd` with the key under keyboard's own `HandleTouchStart` / `HandleTouchEnd` with the key under
@@ -17,18 +17,18 @@
// the laser's hit on the keyboard (geometry.js), the trigger from the // the laser's hit on the keyboard (geometry.js), the trigger from the
// render model's animated "trigger" component (null without one). // render model's animated "trigger" component (null without one).
// Frames go out through the CDP binding __sfuiCtlOut(json) at TICK_MS while // Frames go out through the CDP binding __sfuiCtlOut(json) at TICK_MS while
// a hand is relevant (tip within NEAR m of the keyboard, laser on it, or // a hand is relevant (tip within NEAR m of the keyboard or trigger pulled),
// trigger pulled), else every IDLE_MS; one { keyboard: null } frame when the // else every IDLE_MS; one { keyboard: null } frame when the keyboard goes.
// keyboard goes. Frame (hub.js adds page px): // Nothing at all while the keyboard is hidden (one DOM lookup per POLL_MS). Frame (hub.js adds page px):
// { seq, t, moving, keyboard: { width } | null, // { seq, t, moving, keyboard: { width } | null,
// hands: { left, right: null | { tip: { u, v, d }, ray: { u, v, dist } | null, // hands: { left, right: null | { tip: { u, v, d }, ray: { u, v, dist } | null,
// trigger: 0..1 | null } } } // trigger: 0..1 | null } } }
// Never touches SteamVR's input or the lasers. Debugging: __sfuiCtl.log, // Never touches SteamVR's input or the lasers. Debugging: __sfuiCtl.log,
// __sfuiCtl.last (the last frame). // __sfuiCtl.last (the last frame).
((find, sigs, opts, hooks, GEO) => { ((find, sigs, opts, hooks, GEO) => {
const VERSION = 2; const VERSION = 3;
const G = window; const G = window;
const TICK_MS = 11, IDLE_MS = 250, POLL_MS = 250, MOVE_QUIET_MS = 300, TIP_CACHE_MS = 5000; const TICK_MS = 11, IDLE_MS = 1000, POLL_MS = 250, MOVE_QUIET_MS = 300, TIP_CACHE_MS = 5000;
const NEAR = 0.1; // m const NEAR = 0.1; // m
const TRIGGER_DEG = 12.5; // frame controller: trigger component's full travel const TRIGGER_DEG = 12.5; // frame controller: trigger component's full travel
const PROBE = 'sfui-ctl-keyboard'; const PROBE = 'sfui-ctl-keyboard';
@@ -136,7 +136,7 @@
return { tip: GEO.toKeyboard(pose.translation, S.kb), ray: GEO.rayHit(pose, S.kb), trigger: trigger(hand, m.rm) }; return { tip: GEO.toKeyboard(pose.translation, S.kb), ray: GEO.rayHit(pose, S.kb), trigger: trigger(hand, m.rm) };
} }
const relevant = (h) => !!h && ((Math.abs(h.tip.d) < NEAR && h.tip.u > -0.2 && h.tip.u < 1.2 && h.tip.v > -0.2 && h.tip.v < 0.7) || const relevant = (h) => !!h && ((Math.abs(h.tip.d) < NEAR && h.tip.u > -0.2 && h.tip.u < 1.2 && h.tip.v > -0.2 && h.tip.v < 0.7) ||
(h.ray && h.ray.u >= 0 && h.ray.u <= 1 && h.ray.v >= 0 && h.ray.v <= 0.6) || h.trigger >= 0.5); h.trigger >= 0.5);
// ---- loops --------------------------------------------------------------------- // ---- loops ---------------------------------------------------------------------
let active = false; let active = false;
+2 -2
View File
@@ -8,8 +8,8 @@
// last the last frame (null: none / lost) // last the last frame (null: none / lost)
// Each frame's hand points (tip, ray) get the keyboard page's px: x, y // Each frame's hand points (tip, ray) get the keyboard page's px: x, y
// (CSS px of the keyboard popup) and onKeyboard (inside the page), if the // (CSS px of the keyboard popup) and onKeyboard (inside the page), if the
// keyboard popup exists. Frames come every ~11 ms while a hand is near the // keyboard popup exists. Frames come at up to ~90 Hz while a hand's tip is
// keyboard, its laser on it or its trigger pulled, else every 250 ms, and // within 10 cm of the keyboard or its trigger is pulled, else every 1 s, and
// only while SteamVR shows the keyboard (then one with keyboard null). // only while SteamVR shows the keyboard (then one with keyboard null).
(() => { (() => {
const VERSION = 1; const VERSION = 1;
+3 -1
View File
@@ -46,7 +46,9 @@ async function connect(side) {
if (m.method === 'Runtime.bindingCalled' && m.params.name === cfg.binding) forward(m.params.payload); if (m.method === 'Runtime.bindingCalled' && m.params.name === cfg.binding) forward(m.params.payload);
}; };
const closed = new Promise((res) => { ws.onclose = res; }); const closed = new Promise((res) => { ws.onclose = res; });
if (cfg.binding) { send('Runtime.enable'); send('Runtime.addBinding', { name: cfg.binding }); } // No Runtime.enable: bindings work without it, and it would stream the
// page's console and context events here.
if (cfg.binding) send('Runtime.addBinding', { name: cfg.binding });
live[side] = (expression) => send('Runtime.evaluate', { expression }); live[side] = (expression) => send('Runtime.evaluate', { expression });
console.log(`${side}: connected`); console.log(`${side}: connected`);
await closed; await closed;