mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-26 22:04:22 +01:00
Merge pull request #296 from saphid/test/framey-native-scroll
Measure native Framey scroll in an overflowed test region
This commit is contained in:
7 files changed
+79
-10
No files matched your search
@@ -25,6 +25,6 @@ Release baseline: Frame Control **v0.4.40**, 26 October 2026. Rows also identify
|
||||
| Local Frame Control extensions | Example panel displayed live Frame status in a browser preview; the real Frame served a read-only example in the experimental headset panel | No | No wearer-visible extension view; third-party extensions and physical Windows/macOS extension folders untested | 25 Oct, v0.4.29: [PR #194](https://github.com/saphid/frame-control/pull/194), [PR #201](https://github.com/saphid/frame-control/pull/201), [extension guide](extensions.md) |
|
||||
| Experimental Frame Control SteamVR panel | Frame OS 0.5.6 registered and served the opt-in overlay. A later unattended check safely woke SteamVR and found the panel in stereo compositor and mirror captures; it removed its temporary service and restored power settings | No | No person wore the headset for that check. Physical controller input, scrolling, VR keyboard, comfort and legibility remain unverified. The large status view has browser and packaging checks, not a wearer review | 26 Oct, v0.4.29–0.4.40: [panel PR #201](https://github.com/saphid/frame-control/pull/201), [unattended evidence](evidence/framey-vr-2026-10-25/README.md), [post-#292 unworn sample exercise](evidence/v0440-release-2026-10-26.md#post-tag-292-panel-regression), [earlier standby-gated exercise](evidence/framey-vr-standby-gate-2026-10-26.md), [worn checklist](framey-vr-check.md) |
|
||||
| Experimental Decky Loader manager | Frame OS 0.5.6 ARM64 user-service install/status/update/remove and managed ProtonDB install/update/remove passed earlier. A private Decky 3.3.0 service returned read-only ProtonDB and Steam Status setting RPCs; a later locked check verified the official Notebook default drawing/save/reopen flow and Simple Timer backend events in local Chromium | No | The loader remains opt-in. Installs/updates reject an archive whose Decky name collides with another installed plugin before download and under the headset transaction lock; this safety path has host/fake Frame tests, not a new real-headset collision test. v0.4.40 classifies the 116 exact-version plugins as 1 works (Notebook default flow only), 49 partial, 55 broken and 11 not applicable. Post-tag PR #289 changes TabMaster to partial, giving 1/50/54/11 on current main; #293 audio evidence is still in CI. Deck Shelves moved to partial after its exact chunked frontend rendered settings in local Chromium; its backend and Steam Home shelves remain unverified. Steamback, PlayTime and Deck Settings became partial after detached read-only library/user snapshots and isolated amd64 backend checks; their official ARM64 backends, automatic behavior and VR panels remain unverified. A read-only Steam bridge returned the Frame country, install folders and system information; IsThereAnyDeal selected that country in local Chromium, but its deployed HTTP endpoint and deal lookup remain unverified. An isolated ARM64 LocalSend core answered its info endpoint, but the official x86-only plugin archive and file transfers remain blocked. Three official audio plugins had local real-backend controls and independent virtual-audio readbacks, but their ARM64 Frame audio sessions and physical output remain unverified. Most official backends and VR controls remain unverified. Physical controller use and wearer-visible official panels remain unverified | 26 Oct, v0.4.31–0.4.40: [managed store PR #208](https://github.com/saphid/frame-control/pull/208), [name-collision PR #269](https://github.com/saphid/frame-control/pull/269), [catalog PR #266](https://github.com/saphid/frame-control/pull/266), [backend event evidence](evidence/framey-events-backend-2026-10-26.json), [Steam read PR #274](https://github.com/saphid/frame-control/pull/274), [LocalSend evidence PR #275](https://github.com/saphid/frame-control/pull/275), [local audio evidence](evidence/framey-audio-backends-2026-10-26.md), [post-release library evidence](evidence/framey-library-context-2026-10-26.md), [compatibility table](framey-compatibility.md) |
|
||||
| Experimental Framey VR plugin panels and store | Unattended Frame OS 0.5.6 runs showed the Framey sample panel in stereo compositor and mirror captures. The latest standby-gated exercise restored the original power settings and released its shared lock. Synthetic OpenVR clicks changed a live sample setting, read its Decky backend file and confirmed it after a fresh iframe reopen | No | This was the repository sample, not an official store plugin. Physical controllers, native scrolling, VR keyboard, wearer comfort and legibility remain unverified. CSS Loader and SteamGridDB exact archives passed fake Frame install/remove and panel packing; this does not establish theme or artwork controls in VR. Panel-local backend events and React toasts have local/real-backend checks, but no wearer review. Pause Games and Tailscale Control settings render in local Chromium; their live actions are unverified. The 116-plugin catalog had 60 fake installs/removals and 56 expected blocks; v0.4.40 recorded 101 local Chromium initializers ready and 15 failed, up from 98/18 in v0.4.39. Notebook is marked works only for its default drawing/save/reopen flow | 26 Oct, v0.4.38–0.4.40: [post-#292 unworn sample evidence](evidence/v0440-release-2026-10-26.md#post-tag-292-panel-regression), [backend event PR #270](https://github.com/saphid/frame-control/pull/270), [settings/cache PR #271](https://github.com/saphid/frame-control/pull/271), [post-release sample exercise](evidence/v0439-release-2026-10-26.md#post-release-framey-library-change-282), [worn checklist](framey-vr-check.md) |
|
||||
| Experimental Framey VR plugin panels and store | Unattended Frame OS 0.5.6 runs showed the Framey sample panel in stereo compositor and mirror captures. The latest standby-gated exercise restored the original power settings and released its shared lock. Synthetic OpenVR clicks changed a live sample setting, read its Decky backend file and confirmed it after a fresh iframe reopen | No | This was the repository sample, not an official store plugin. Physical controllers, native scrolling, VR keyboard, wearer comfort and legibility remain unverified. CSS Loader and SteamGridDB exact archives passed fake Frame install/remove and panel packing; this does not establish theme or artwork controls in VR. Panel-local backend events and React toasts have local/real-backend checks, but no wearer review. Pause Games and Tailscale Control settings render in local Chromium; their live actions are unverified. The 116-plugin catalog had 60 fake installs/removals and 56 expected blocks; v0.4.40 recorded 101 local Chromium initializers ready and 15 failed, up from 98/18 in v0.4.39. Notebook is marked works only for its default drawing/save/reopen flow | 26 Oct, v0.4.38–0.4.40: [post-#294 unworn sample and scroll evidence](evidence/framey-scroll-probe-2026-10-26.md#post-294-exact-main-sample-check), [backend event PR #270](https://github.com/saphid/frame-control/pull/270), [settings/cache PR #271](https://github.com/saphid/frame-control/pull/271), [post-release sample exercise](evidence/v0439-release-2026-10-26.md#post-release-framey-library-change-282), [worn checklist](framey-vr-check.md) |
|
||||
|
||||
Release notes must preserve these limits until a newer recorded check changes them. When a feature is retested, add its date, exact app/Frame version, host type and proof here. The older [v0.3.1 matrix](support-matrix.html) is a historical snapshot and does not describe current coverage.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Unworn Framey scroll transport probe, 26 October 2026
|
||||
|
||||
The previous `scripts/framey-vr-check.py --exercise` runs reported `scrollY=0` before and after one injected smooth-scroll event. That was inconclusive because the repository sample may fit inside the iframe. This probe changed only the **test script**: it created a temporary 160-pixel-high scroll region with 716 pixels of content, moved the native OpenVR pointer over it, and measured its own `scrollTop` and captured wheel events. No production Framey panel code changed.
|
||||
|
||||
Two local diagnostic versions ran on the real Frame from base main `784118b` after PR #292. The final version in this PR tried OpenVR `VREvent_ScrollSmooth` (309) and `VREvent_ScrollDiscrete` (305), using Valve's documented `VREvent_Scroll_t` ABI. The overlay listing showed the temporary Frame Control dashboard visible with `+smoothscroll`; a trusted native pointer move reached the iframe. Neither scroll event reached Chromium's capture listener and the region stayed at `scrollTop=0`. This establishes only that `IVROverlayView_003.PostOverlayEvent` did not deliver these synthetic scroll events in this run. A physical controller may follow a different path.
|
||||
|
||||
| Check | First probe | Final probe |
|
||||
| --- | --- | --- |
|
||||
| Unique shared lock and standby gate | Acquired; activity 3, battery 99% | Acquired; activity 3, battery 100% |
|
||||
| Scroll region | 160 px viewport, 716 px content | 160 px viewport, 716 px content |
|
||||
| Native pointer | Moved over region | Trusted pointer move reached iframe |
|
||||
| Injected scroll | Smooth 309 | Smooth 309, then discrete 305 |
|
||||
| Wheel events / offset | 0 / 0 → 0 | 0 / 0 → 0 |
|
||||
| Other sample checks | Passed | Passed |
|
||||
| Cleanup | Power restored to `false/5`, recovery timer removed, owned lock released; battery 99% | Same cleanup; battery 99% |
|
||||
|
||||
Both runs captured changed stereo compositor frames and a `/dev/video99` mirror frame, saved only in private folders `/tmp/framey-native-scroll-probe/` and `/tmp/framey-native-scroll-events/`. No room pixels are committed. The final run's `result.json` reported `passed=true`, `cleanupErrors=[]`, `powerRecoveryRemoved=true` and `lockReleased=true`. Here `passed` covers the sample setting, quick action, slider, text and cleanup checks; **native scroll did not pass**. The final immediate SteamVR activity was 0 after the temporary wake; original power settings were restored and read back. No person wore the headset, and physical controller scrolling, rays, haptics, keyboard, optical readability and comfort remain unverified. The hourly worn-check schedule remains enabled.
|
||||
|
||||
## Post-#294 exact-main sample check
|
||||
|
||||
After PR #294 merged, the **unmodified** `scripts/framey-vr-check.py --exercise` from exact main `c38e7e8da459e44a9a717d15e6a07ab8a8e8ef8d` exited 0 on the real Frame. Locked preflight found activity 3 standby, proximity 6.3125 and battery 99%. The sample saved `{"enabled": true}` through trusted synthetic OpenVR input and the live Decky backend, read the file independently, then reopened a fresh iframe with the value retained. Quick action, slider value 50 and text entry completed; compositor frames changed and the mirror capture returned a frame. The old scroll diagnostic still reported no offset movement.
|
||||
|
||||
The run removed its temporary services and recovery timer, restored/read back power `false/5`, reported no cleanup errors, and released only its owned lock. Its immediate activity was 0 after the temporary wake. Private captures/result are under `/tmp/framey-post294/` outside Git. This was one setting activation, so it does not prove the new rapid-double-input guard on physical controllers. No person wore the headset.
|
||||
@@ -53,7 +53,12 @@ iframe through its own debugging target, and reads control geometry. OpenVR's
|
||||
actual gamescope overlay. The check verifies delivery to the intended element
|
||||
and a trusted pointer event, toggles the sample, reads its real Decky settings
|
||||
file, reopens a fresh iframe, and checks the saved value. It also clicks the
|
||||
Settings/Panel tabs and quick action. A smooth-scroll event is attempted and its measured scroll offset is reported.
|
||||
Settings/Panel tabs and quick action. The scroll diagnostic creates a temporary
|
||||
160-pixel-high region with more than 700 pixels of content, moves the native
|
||||
pointer over it, then attempts OpenVR smooth and discrete scroll events. It
|
||||
reports region geometry, trusted pointer events, wheel events and `scrollTop`.
|
||||
No wheel event or offset change means this synthetic injection path remains
|
||||
unverified; it does not establish whether a physical controller can scroll.
|
||||
Temporary controls exercise the shipped
|
||||
`DFL.SliderField` and `DFL.TextField`; their values remain UI-only. Text uses
|
||||
CDP `Input.insertText` after OpenVR pointer focus. This is synthetic input,
|
||||
|
||||
@@ -260,6 +260,11 @@ print('{}')
|
||||
except Exception as exc:
|
||||
report['hiddenOverlayError'] = str(exc)
|
||||
raise RuntimeError('Frame Control overlay stayed hidden after focus retry; no input sent')
|
||||
if args.exercise:
|
||||
lines = remote(VRCMD + ' --overlays').splitlines()
|
||||
report['testOverlayListing'] = [line.strip() for index, line in enumerate(lines)
|
||||
if line.startswith("'" + KEY + "'") or
|
||||
(index and lines[index - 1].startswith("'" + KEY + "'"))]
|
||||
if args.wake or args.exercise:
|
||||
shot = remote.python((ROOT/'ui/frame_vrshot.py').read_text(), timeout=20)['path']
|
||||
try:
|
||||
|
||||
@@ -93,9 +93,11 @@ class Overlay:
|
||||
event = Event(kind, 0xFFFFFFFF, 0, Data(mouse=Mouse(x, y, button)))
|
||||
C.CFUNCTYPE(None, C.c_uint64, C.POINTER(Event))(view[2])(self.handle, C.byref(event))
|
||||
|
||||
def post_scroll(self, xdelta, ydelta):
|
||||
def post_scroll(self, xdelta, ydelta, kind=309):
|
||||
if kind not in (305, 309):
|
||||
raise ValueError('Only OpenVR discrete and smooth scroll events are allowed')
|
||||
view = self.interface(b'FnTable:IVROverlayView_003')
|
||||
event = Event(309, 0xFFFFFFFF, 0, Data(scroll=Scroll(xdelta, ydelta, 0, 1)))
|
||||
event = Event(kind, 0xFFFFFFFF, 0, Data(scroll=Scroll(xdelta, ydelta, 0, 1)))
|
||||
C.CFUNCTYPE(None, C.c_uint64, C.POINTER(Event))(view[2])(self.handle, C.byref(event))
|
||||
|
||||
def image_hash(self):
|
||||
|
||||
@@ -46,7 +46,7 @@ def exercise(profile, plugin_id, folder):
|
||||
|
||||
def text(): return panel.eval('document.body.innerText')
|
||||
|
||||
def click(selector):
|
||||
def point(selector):
|
||||
# Account for CSS pixels, iframe position, border, HiDPI scaling, and
|
||||
# OpenVR's bottom-left mouse origin; read geometry after scrolling.
|
||||
panel.eval(f'document.querySelector({json.dumps(selector)}).scrollIntoView({{block:"center"}})')
|
||||
@@ -58,6 +58,10 @@ def exercise(profile, plugin_id, folder):
|
||||
y = sy - (outer['y'] + pos['y']) * sy / outer['h']
|
||||
if not 0 <= x <= sx or not 0 <= y <= sy:
|
||||
raise RuntimeError('Control is outside the visible viewport')
|
||||
return x, y
|
||||
|
||||
def click(selector):
|
||||
x, y = point(selector)
|
||||
count = panel.eval('__vrClickCount')
|
||||
vr.post_mouse(300, x, y)
|
||||
vr.post_mouse(301, x, y, 1)
|
||||
@@ -113,12 +117,39 @@ def exercise(profile, plugin_id, folder):
|
||||
# CDP only types text; OpenVR supplies pointer focus into the field.
|
||||
command(parent, 'Input.insertText', {'text':'framey-ci'})
|
||||
result['textValue'] = wait(lambda:panel.eval('document.querySelector("#vr-controls input[type=text]").value'), lambda v:v == 'framey-ci')
|
||||
panel.eval('window.scrollTo(0,0)')
|
||||
result['scrollBefore'] = panel.eval('scrollY')
|
||||
vr.post_scroll(0,-3)
|
||||
time.sleep(.5)
|
||||
result['scrollAfter'] = panel.eval('scrollY')
|
||||
# The sample alone may be shorter than its iframe. Use an owned,
|
||||
# visibly scrollable region so a zero offset is a meaningful result.
|
||||
panel.eval('''(()=>{
|
||||
const box=document.createElement('div');box.id='vr-scroll-probe';
|
||||
box.style.cssText='height:160px;overflow-y:scroll;border:2px solid #72baf0';
|
||||
box.innerHTML='<div style="height:700px;padding:8px">Scroll probe</div>';
|
||||
document.body.append(box);
|
||||
window.__vrWheelEvents=[];
|
||||
document.addEventListener('wheel',e=>__vrWheelEvents.push({trusted:e.isTrusted,
|
||||
deltaY:e.deltaY,target:e.target?.id||e.target?.tagName}),{capture:true,passive:true});
|
||||
return true;
|
||||
})()''')
|
||||
result['scrollGeometry'] = panel.eval('(()=>{let e=document.getElementById("vr-scroll-probe");return {height:e.clientHeight,content:e.scrollHeight}})()')
|
||||
if result['scrollGeometry']['content'] <= result['scrollGeometry']['height']:
|
||||
raise RuntimeError('Scroll diagnostic did not create overflow')
|
||||
x, y = point('#vr-scroll-probe')
|
||||
panel.eval('window.__vrScrollPointer=[];document.addEventListener("pointermove",e=>__vrScrollPointer.push({trusted:e.isTrusted,target:e.target?.id||e.target?.tagName}),{capture:true});true')
|
||||
vr.post_mouse(300, x, y)
|
||||
time.sleep(.2)
|
||||
result['scrollPointer'] = panel.eval('__vrScrollPointer')
|
||||
result['scrollBefore'] = panel.eval('document.getElementById("vr-scroll-probe").scrollTop')
|
||||
result['scrollAttempts'] = []
|
||||
for kind in (309, 305):
|
||||
vr.post_scroll(0,-3,kind)
|
||||
time.sleep(.5)
|
||||
offset = panel.eval('document.getElementById("vr-scroll-probe").scrollTop')
|
||||
result['scrollAttempts'].append({'eventType':kind,'offset':offset})
|
||||
if offset != result['scrollBefore']:
|
||||
break
|
||||
result['scrollAfter'] = result['scrollAttempts'][-1]['offset']
|
||||
result['scrollEvents'] = panel.eval('__vrWheelEvents')
|
||||
result['nativeScrollMoved'] = result['scrollAfter'] != result['scrollBefore']
|
||||
panel.eval('document.getElementById("vr-scroll-probe").remove()')
|
||||
panel.eval('document.getElementById("vr-controls").remove()')
|
||||
# Scroll saved status into the visible iframe for compositor evidence.
|
||||
panel.eval('document.querySelector("[role=status]").scrollIntoView({block:"center"})')
|
||||
|
||||
@@ -105,6 +105,9 @@ class Safety(unittest.TestCase):
|
||||
overlay = importlib.util.module_from_spec(overlay_spec); overlay_spec.loader.exec_module(overlay)
|
||||
self.assertEqual(ctypes.sizeof(overlay.Event), 60)
|
||||
self.assertEqual(overlay.Event.data.offset, 12)
|
||||
self.assertEqual(ctypes.sizeof(overlay.Scroll), 16)
|
||||
with self.assertRaises(ValueError):
|
||||
object.__new__(overlay.Overlay).post_scroll(0, -3, 308)
|
||||
original = check.frame_headset.bundle()
|
||||
patched = check.debug_bundle(original, '/tmp/owned-profile')
|
||||
import tarfile
|
||||
|
||||
Reference in new issue
Block a user