Compare commits

..
Author SHA1 Message Date
SAPHID 0c99722073 Document current Frame panel layout API limits 2026-10-24 10:09:49 +00:00
Alex SouthwellandSAPHID 5b47cd3330 Document packaged Desktop Capture result and visibility limit (#159)
Co-authored-by: SAPHID <saphid@users.noreply.github.com>
2026-10-24 10:03:36 +00:00
Alex SouthwellandSAPHID aead570b5d Clarify Return to Game Mode switch help (#158)
Co-authored-by: SAPHID <saphid@users.noreply.github.com>
2026-10-24 09:47:24 +00:00
Alex SouthwellandSAPHID de931bc56f Document packaged Return to Game Mode button verification (#157)
Co-authored-by: SAPHID <saphid@users.noreply.github.com>
2026-10-24 09:39:21 +00:00
Alex SouthwellandSAPHID 71288d8980 Document packaged desktop Steam clip Export verification (#156)
Co-authored-by: SAPHID <saphid@users.noreply.github.com>
2026-10-24 09:19:46 +00:00
Alex SouthwellandSAPHID 64b9babe87 Document Linux PC-host feasibility blocker on lxso2 (#155)
Co-authored-by: SAPHID <saphid@users.noreply.github.com>
2026-10-24 09:17:14 +00:00
Alex SouthwellandSAPHID 4e456480ae Document real 100-second Steam clip export (#154)
Co-authored-by: SAPHID <saphid@users.noreply.github.com>
2026-10-24 09:05:12 +00:00
Alex SouthwellandSAPHID 241f538f99 Bump version to 0.4.17 (#153)
Co-authored-by: SAPHID <saphid@users.noreply.github.com>
2026-10-24 08:43:00 +00:00
Alex SouthwellandSAPHID 21dacfcbe7 Distinguish unreadable panel discovery from an empty list (#152)
Co-authored-by: SAPHID <saphid@users.noreply.github.com>
2026-10-24 08:34:56 +00:00
23 changed files with 1287 additions and 10 deletions

No files matched your search

+13
View File
@@ -2,6 +2,19 @@
## Unreleased
- Recheck saved spatial layouts on SteamOS 0.5.6 / SteamVR 2.18.2 (issue #31).
Read-only probes expose some panel geometry, but safe restoration of
Steam/gamescope docking and positions remains unverified. Record exact API
results and a reproducible probe; saved layouts remain unavailable.
- Record packaged v0.4.17 Desktop Capture succeeding on a mapped nested KDE
compositor on a real Frame with SteamOS 0.5.6. Headset overlay visibility was
not established; this is evidence, not a new fix or resolution of issue #73.
- Correct the Return to Game Mode help text: Frame Control selects the switch
supported by the headset's SteamOS build, including `steamosctl` on current
Frames. The button's behavior is unchanged.
- Distinguish an unreadable gamescope panel list from a confirmed empty list in
Desktop Capture and Control (issue #73). Failed discovery now asks users to
check SteamVR and report the exact message and SteamOS version, rather than
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "frame-control",
"version": "0.4.16",
"version": "0.4.17",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "frame-control",
"version": "0.4.16",
"version": "0.4.17",
"license": "MIT",
"devDependencies": {
"electron": "^44.4.5",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "frame-control",
"productName": "Frame Control",
"version": "0.4.16",
"version": "0.4.17",
"description": "Desktop app for managing a Valve Steam Frame over SSH",
"private": true,
"main": "main.js",
+22 -2
View File
@@ -25,8 +25,28 @@ tested in another full Plasma session under a three-minute rollback timer.
`ui/frame_backup.py session` reported Desktop Mode despite gamescope, and
`ui/frame_backup.py game-mode` invoked the same guarded Return function used by
the button. Game Mode became active and a second call made no switch. Battery
remained at 99%; the timer and lock were removed. The literal desktop button
was not clicked in this test, and older SteamOS builds remain fixture-tested.
remained at 99%; the timer and lock were removed. That test used the production
CLI rather than clicking the desktop button.
Later on 2026-10-24, the published **v0.4.17 Linux x86_64 AppImage** passed the
literal packaged-button check on the same Frame/build. With the shared lock and
a verified three-minute rollback timer, the headset entered full Plasma Desktop
Mode. In the packaged Electron window, **Tools → Backup & recovery → Check**
reported Desktop Mode despite gamescope. A trusted automated pointer click on
the visible **Return to Game Mode** button, accepting its confirmation, returned
`switched: true` and `steamosctl switch-to-game-mode`. The systemd targets then
confirmed Game Mode. A second confirmed button click returned `switched: false`;
the Game Mode service invocation and SteamVR PID stayed unchanged. **Check**
then reported Game Mode and disabled Return. The test app/server and virtual
display were stopped; the timer and shared lock were removed. Steam and SteamVR remained running
at 99% battery. [Commands, responses, cleanup and limits](evidence/return-game-mode-packaged-2026-10-24.json),
[before](evidence/return-game-mode-packaged-2026-10-24-before.png) and
[after](evidence/return-game-mode-packaged-2026-10-24-after.png).
This used the unchanged extracted AppImage payload with its bundled Python on
Xvfb (`--no-sandbox --disable-gpu`), following the packaged Export test in #156.
FUSE mounting, desktop integration, hardware rendering and the view inside a
worn headset were not checked. Older SteamOS builds remain fixture-tested.
## Back up
+29
View File
@@ -0,0 +1,29 @@
{
"date_utc": "2026-10-24",
"version": "v0.4.17",
"steam_os_version": "0.5.6",
"steam_os_build": "20261015.6141207",
"result": "PASS: the real Steam-saved clip exported beyond 60 seconds",
"method": "Steam manual recording of a reserved test title, then Frame Control's production frame_clips.export_clip path; no packaged GUI button click",
"source": {
"kind": "saved Steam clip",
"duration_seconds": 100.801,
"chunks": 68,
"gaps": 0
},
"export": {
"format": "MP4",
"bytes": 1377849,
"sha256": "82ef31f65e6933d122bc326d664beb2eb2e3ba6969f26c0032552e80fbe8d823",
"duration_seconds_ffprobe": 100.833335,
"video": "H.264, 1920x1080, 6049 frames",
"audio": "AAC, 48000 Hz, stereo",
"full_ffmpeg_decode": "passed without errors"
},
"cleanup": "Restored Steam recording mode to its prior off setting; stopped and removed only the reserved title and its processes; no recording or running Steam apps remained; Game Mode active, battery 100%, shared test lock released",
"limits": [
"This exercised the production export code path, not a packaged GUI Export click.",
"Visible gameplay quality, perceptual audio/video sync, trims and longer recordings were not checked.",
"The newly saved Steam source clip was retained; no user recording was deleted."
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

@@ -0,0 +1,94 @@
{
"date_utc": "2026-10-24T09:08:35.523Z",
"result": "PASS: packaged Electron visible Export click saved the retained real Steam clip beyond 60 seconds",
"frame_control_tag": "v0.4.17",
"documentation_base_commit": "4e456480aefbe541e334dbd247328537f535bd94",
"host": "lxso2",
"steamos_version": "0.5.6",
"steamos_build": "20261015.6141207",
"package": {
"asset": "Frame-Control-linux-x86_64.AppImage",
"url": "https://github.com/saphid/frame-control/releases/download/v0.4.17/Frame-Control-linux-x86_64.AppImage",
"sha256": "b1b8eb82bf73a227b2cd36e8f0ef3a2ebdf7967c5853efb08a9634f38227015d",
"launch": "Verified release asset; --appimage-extract, then unmodified squashfs-root/AppRun. Electron used the bundled server and Python, not checkout source.",
"display": "Xvfb :137, 1440x1000x24; Electron window 1400x950 including native chrome",
"flags": [
"--no-sandbox",
"--disable-gpu",
"--remote-debugging-port=49371",
"--remote-debugging-address=127.0.0.1",
"--user-data-dir=/tmp/frame-packaged-export-20261024/electron-profile"
],
"isolation": "Throwaway XDG_CONFIG_HOME, FRAME_CONTROL_DATA_DIR and Electron profile; telemetry and update checks disabled; existing SSH identity and normal home Videos destination used."
},
"ui": {
"navigation": "Dismissed optional email prompt with No thanks; clicked Tools, then Capture & Live; scrolled Steam clips into view.",
"row": "Steam recording \u00b7 Clip; Oct 24, 8:53 AM \u00b7 1:40 \u00b7 2 MB",
"action": "Playwright connected to this Electron instance over local CDP; locator.click() clicked its visible button named exactly Export. The test did not directly invoke an export function or API POST.",
"button": "button.small[data-clip-export=\"0\"]",
"click_is_trusted": true,
"button_bounds_css_pixels": {
"x": 1300.390625,
"y": 701.15625,
"width": 61.609375,
"height": 28
},
"post_clips_export_http": 200,
"job": "0ab20b1788adce63",
"job_poll_http": 200,
"saved_message": "Saved clip_15658413054201167872_20261024_085343.mp4 to ~/Videos/SteamFrame",
"final_row": "ON PC; Export again",
"screenshots": [
"clip-packaged-export-2026-10-24-before.png",
"clip-packaged-export-2026-10-24-after.png"
],
"screenshot_scope": "Cropped to Steam clip rows and saved notification; unrelated screenshot thumbnails and host connection identity excluded."
},
"source": {
"clip_id": "10230640/clips/clip_15658413054201167872_20261024_085343",
"duration_seconds": 100.801,
"duration_provenance": "Retained real Steam clip from clip-long-2026-10-24.json; current packaged UI displays 1:40."
},
"output": {
"path": "~/Videos/SteamFrame/clip_15658413054201167872_20261024_085343.mp4",
"bytes": 1377849,
"sha256": "82ef31f65e6933d122bc326d664beb2eb2e3ba6969f26c0032552e80fbe8d823",
"matches_prior_production_export_size_and_sha256": true,
"duration_seconds_ffprobe": 100.833335,
"duration_difference_from_steam_seconds": 0.032335,
"video": "H.264, 1920x1080, 6049 decoded frames",
"audio": "AAC, 48000 Hz, stereo, 4724 decoded audio frames",
"validation": [
"ffprobe -v error -count_frames -show_entries format=duration,size:stream=codec_name,codec_type,width,height,sample_rate,channels,nb_read_frames -of json <output>: exit 0",
"sha256sum <output>: exact match to prior production export",
"ffmpeg -v error -xerror -i <output> -f null -: exit 0, no errors"
]
},
"cleanup": {
"shared_lock": "Acquired with ssh frame mkdir /tmp/frame-test.lock after read-only battery/build/lock checks; released with rmdir after app shutdown; absence read back.",
"battery_percent_before": 99,
"battery_percent_after": 99,
"battery_status": "Not charging; charger Full",
"packaged_app_and_server_exited": true,
"test_xvfb_stopped": true,
"frame_export_cache_directories_remaining": 0,
"ffmpeg_processes_after": 0,
"game_mode_and_steamvr_running_after": true,
"steam_source_clip_retained": true,
"user_recordings_deleted": false,
"headset_install_or_settings_changes": false,
"local_export_retained": true
},
"limits": [
"The unchanged AppImage payload ran after extraction on a virtual X desktop, with Chromium sandbox and GPU disabled. FUSE mounting, desktop integration and hardware rendering were not tested.",
"The click was automated trusted pointer input in a visible Electron window on Xvfb, not a human click on a physical monitor. T3 collaborative preview opened successfully but drives separate browser tabs; Electron-native automation was used for this window.",
"No new game or Steam recording was started. Visible gameplay quality, audible content and perceptual audio/video sync were not assessed.",
"Clips longer than this retained 100-second clip, trims and automatic copying remain unverified.",
"No release, tag or application code was changed."
],
"validation_local": {
"clip_regression_suite": "python3 -m unittest discover -s tests -p test_clips.py -v: 34 tests passed in 20.173s, including real ffmpeg long-recording export",
"json": "python3 -m json.tool: passed",
"diff": "git diff --check: passed"
}
}
@@ -0,0 +1,206 @@
{
"date_utc": "2026-10-24",
"base_commit": "de931bc",
"issue": 73,
"result": "Packaged Desktop Capture passed for a mapped nested KDE compositor on the real Frame; wearer-visible overlay condition was not established. Do not close #73 from this test.",
"package": {
"tag": "v0.4.17",
"asset": "Frame-Control-linux-x86_64.AppImage",
"sha256": "b1b8eb82bf73a227b2cd36e8f0ef3a2ebdf7967c5853efb08a9634f38227015d",
"release_digest_matched": true,
"panel_helper_matches_base": true,
"unmodified_payload": true,
"host": "lxso2",
"display": "Xvfb :139",
"flags": [
"--no-sandbox",
"--disable-gpu"
],
"private_profile_and_data": true,
"telemetry_and_update_checks_disabled": true
},
"preflight": {
"steamos_version": "0.5.6",
"build_id": "20261015.6141207",
"battery_percent": 99,
"battery_status": "Not charging",
"game_mode": true,
"full_plasma_session": false,
"nested_desktop_running": false,
"nested_runtime_present": false,
"lock_present": false,
"steam_pid": 297826,
"vrserver_pid": 297506,
"vrcompositor_pid": 297620,
"gamescope_service_invocation": "e8c1085b3bbd4e79a323b3e4a9bc6355",
"focusable_windows": [],
"focused_windows": []
},
"session": {
"started_utc": "2026-10-24T09:43:48Z",
"lock_acquired": true,
"launch": [
"systemd-run",
"--user",
"--unit=fc-issue73-desktop-20261024",
"--collect",
"--property=RuntimeMaxSec=180s",
"--property=TimeoutStopSec=15s",
"--property=KillMode=control-group",
"--setenv=DISPLAY=:0",
"--setenv=XDG_RUNTIME_DIR=/run/user/1000",
"--setenv=DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus",
"--setenv=XDG_SESSION_TYPE=x11",
"--setenv=XDG_CURRENT_DESKTOP=gamescope",
"/usr/bin/steamos-nested-desktop"
],
"response": "Running as unit: fc-issue73-desktop-20261024.service; invocation ID: 92cff1e57c794212997d46c0a68877ec\n"
},
"discovery": {
"focus": 62914560,
"focus_display": ":0",
"panels": [
{
"window": 62914560,
"display": ":0",
"name": "KDE Wayland Compositor - Press right control key to grab input",
"x": 0,
"y": 0,
"width": 1920,
"height": 1080,
"app": 0,
"focused": true
}
]
},
"window_evidence": {
"xwininfo_map_state": "IsViewable",
"width": 1920,
"height": 1080,
"focused_after_show_request": true,
"steamvr_overlay_key": "valve.steam.desktopgame.0",
"steamvr_overlay_visibility_after_show_request": "not_visible"
},
"capture": {
"repeat_utc": "2026-10-24T09:44:47.323Z",
"route": "/api/screenshot?view=panel&window=62914560&display=%3A0",
"http": 200,
"content_type": "image/png",
"content_length": 1209980,
"source_header": "panel",
"decoded": {
"width": 1920,
"height": 1080,
"source": "panel",
"selected": {
"window": 62914560,
"display": ":0",
"name": "KDE Wayland Compositor - Press right control key to grab input",
"x": 0,
"y": 0,
"width": 1920,
"height": 1080,
"app": 0,
"focused": true
},
"stamp": "9:44:48 AM",
"badge": "Desktop \u00b7 KDE Wayland Compositor - Press right control key to grab input"
},
"canvas_png_bytes": 2011278,
"first_capture_pixel_statistics": {
"width": 1920,
"height": 1080,
"min": 2.3333333333333335,
"max": 255,
"hidden": false
},
"first_capture_click_event": {
"isTrusted": true,
"utc": "2026-10-24T09:44:12.738Z"
},
"actions": "Two Playwright locator.click() actions on the visible #shotBtn in the actual packaged Electron window; first click recorded isTrusted=true. No direct screenshot API invocation by harness."
},
"device_cleanup": {
"utc": "2026-10-24T09:45:33Z",
"before_stop": {
"exit": 0,
"out": "MainPID=300739\nControlGroup=/user.slice/user-1000.slice/user@1000.service/app.slice/fc-issue73-desktop-20261024.service\nActiveState=active\n",
"err": ""
},
"hide_dashboard": {
"exit": 0,
"out": "HideDashboard",
"err": ""
},
"stop_owned_unit": {
"exit": 0,
"out": "",
"err": ""
},
"after_stop": {
"exit": 0,
"out": "MainPID=0\nLoadState=not-found\nActiveState=inactive\n",
"err": ""
},
"desktop_processes": {
"exit": 1,
"out": "",
"err": ""
},
"nested_runtime_exists": false,
"session": {
"exit": 0,
"out": "Id=gamescope-session.target\nActiveState=active\nInvocationID=a2fee8ebf55c42c7b546390d14cd985c\n\nId=plasma-workspace-wayland.target\nActiveState=inactive\nInvocationID=\n\nId=gamescope-session.service\nActiveState=active\nInvocationID=e8c1085b3bbd4e79a323b3e4a9bc6355\n",
"err": ""
},
"steam": {
"exit": 0,
"out": "297826 /home/steamos/.local/share/Steam/steamrtarm64/steam -cef-enable-debugging -deckard -gamepadui -steamdeck -steamos3 -vrgamepadui\n",
"err": ""
},
"steamvr": {
"exit": 0,
"out": "297506 /opt/steamvr/bin/linuxarm64/vrserver -waitformonitor -ipccontrolfilepath /tmp/SteamVR-IPCControlFile-297420\n297620 /opt/steamvr/bin/linuxarm64/vrcompositor\n",
"err": ""
},
"xprops": {
"exit": 0,
"out": "GAMESCOPE_FOCUSABLE_WINDOWS(CARDINAL) = \nGAMESCOPE_FOCUSED_WINDOW(CARDINAL) = \n",
"err": ""
},
"battery": {
"capacity": "99",
"status": "Not charging"
},
"owned_lock_released": true
},
"host_cleanup": {
"electron": {
"pid": 1578431,
"exists_after": false
},
"xvfb": {
"pid": 1578428,
"exists_after": false
},
"x_socket_absent": true
},
"delayed_final_readback": "2026-10-24T09:46:39Z\nshared-lock-absent\nLoadState=not-found\nActiveState=inactive\nId=gamescope-session.target\nActiveState=active\n\nId=plasma-workspace-wayland.target\nActiveState=inactive\nInvocationID=e8c1085b3bbd4e79a323b3e4a9bc6355\n297826 /home/steamos/.local/share/Steam/steamrtarm64/steam -cef-enable-debugging -deckard -gamepadui -steamdeck -steamos3 -vrgamepadui\n297506 /opt/steamvr/bin/linuxarm64/vrserver -waitformonitor -ipccontrolfilepath /tmp/SteamVR-IPCControlFile-297420\n297620 /opt/steamvr/bin/linuxarm64/vrcompositor\nnested-runtime-absent\n100\nFull\nGAMESCOPE_FOCUSABLE_WINDOWS(CARDINAL) = \nGAMESCOPE_FOCUSED_WINDOW(CARDINAL) = \n",
"validation": {
"focused_panel_suite": "42 tests passed in 14.805s (test_touch.py), including production-page Node behavior tests",
"website": "2 tests passed (node --test site/test/*.test.mjs)",
"node_panels": "Passed independently (node tests/page/desk_panels.mjs)",
"metadata_and_links": "JSON parsed; result/cleanup assertions and relative evidence/streaming/testing links passed",
"diff": "git diff --check passed"
},
"limits": [
"SteamVR continued reporting not_visible after ShowDashboard, despite mapped X window and confirmed focus. No worn-headset visual assessment; does not establish the reporter's visible-overlay condition.",
"SteamOS 0.5.6 only; reporter used 0.4.5 with unknown Frame Control version and no reproduction steps.",
"Nested desktop launched with SteamOS steamos-nested-desktop from a bounded transient user unit using Steam display/session variables; not launched through a wearer's Steam UI.",
"Extracted unchanged AppImage on a virtual X display with Chromium sandbox and GPU disabled; no FUSE, physical monitor or hardware-rendering test.",
"No Live video, Control input or full Desktop Mode test.",
"Nested-desktop journal included gamescope WSI/Vulkan-hook warnings. No warning was dismissed; captured compositor pixels were nonblank.",
"Initial CDP response.body() yielded zero bytes despite successful image decode. Repeat verified Content-Length, fresh decoded image, panel selection and PNG export from canvas; the initial raw-body size is not treated as capture failure or proof.",
"No private desktop pixels, issue screenshot or raw desktop journal committed."
]
}
@@ -0,0 +1,100 @@
# Packaged Desktop Capture, 2026-10-24
Base: `de931bc`. Package: Linux x86_64 **v0.4.17**. Device: real Frame,
SteamOS **0.5.6**, BUILD_ID **20261015.6141207**. Host: lxso2.
[Sanitized state and response evidence](desktop-capture-packaged-2026-10-24.json).
## Result and issue boundary
The packaged app discovered the nested KDE compositor and both automated
Desktop **Capture** button clicks succeeded. The repeat returned HTTP 200,
`Content-Type: image/png`, `X-Capture-Source: panel`, and Content-Length
1,209,980. The app decoded a fresh, nonblank 1920×1080 image and displayed
the Desktop panel badge. No further product failure was reproduced or fixed.
This does **not** establish the full visible-desktop condition in
[#73](https://github.com/saphid/frame-control/issues/73). `xwininfo` reported
the desktop window `IsViewable`, and gamescope confirmed focus after
`vrcmd --showdashboard valve.steam.desktopgame.0`. However, SteamVR still
reported that overlay `not_visible`. There was no worn-headset visual check.
The observation proves capture of a mapped nested compositor, not its visible
presentation to a wearer. Do not close #73 from this evidence.
The issue screenshot shows a desktop panel in the headset view while Desktop
Capture logs “no app panel is open in the headset.” The reporter used SteamOS
0.4.5, gave no Frame Control version or reproduction steps, and has not replied
to the maintainer's 0.4.3 update request. The duplicate page function was already
fixed; [PR #152](https://github.com/saphid/frame-control/pull/152) subsequently
improved unknown-state diagnostics. Its [earlier investigation](panel-discovery-2026-10-24.md)
was blocked from starting a desktop by another task's lock. This run adds a
current-package capture observation, without reproducing the older failure.
## Procedure
1. Read-only preflight found Game Mode active, full Plasma inactive, Steam and
SteamVR running, battery 99%, no nested desktop/runtime and no test lock.
2. Compared the retained AppImage's SHA-256 with the live v0.4.17 release asset
digest: `b1b8eb82bf73a227b2cd36e8f0ef3a2ebdf7967c5853efb08a9634f38227015d`.
Extracted an independent copy; `cmp` confirmed its `frame_touch.py` matches
the base checkout. No payload was edited.
3. Started the package's own AppRun, bundled server and Python on Xvfb `:139`
with private config/cache/data/profile, telemetry and update checks disabled,
and `--no-sandbox --disable-gpu`. Used the existing authorized `ssh frame`
identity. T3 preview opened the package server page but does not attach to
the existing Electron window. The literal Electron button test used the
same Playwright-over-loopback-CDP workflow as the earlier packaged tests.
4. Acquired `/tmp/frame-test.lock` with atomic `mkdir` and wrote an owner marker.
Rechecked no nested runtime or Plasma was present. Started SteamOS's
`/usr/bin/steamos-nested-desktop` in the owned transient user unit
`fc-issue73-desktop-20261024.service`, using Steam's display/session variables.
`RuntimeMaxSec=180s`, `TimeoutStopSec=15s` and `KillMode=control-group` bounded
the session. This launches a desktop panel inside Game Mode; no full Desktop
Mode switch was sent.
5. In the actual packaged Electron window, navigated Tools → Capture & Live →
Desktop and clicked Capture with `locator.click()`. The first click event
recorded `isTrusted=true`. The page selected the KDE compositor on `:0`;
the production screenshot path used window `62914560`. Requested the desktop
dashboard, read back focus/visibility, and clicked Capture again. Recorded
response metadata, fresh decoded image dimensions and canvas pixel range.
No direct screenshot API call was substituted for these button clicks.
6. Ran the packaged `frame_touch.py panels` read-only over SSH to retain its
exact discovery result: one focused KDE compositor panel, app ID 0, 1920×1080,
display `:0`, with no diagnostic `why` field.
## Cleanup and limits
The owned desktop session ran **09:43:48–09:45:33 UTC** (105 seconds). Hid the
dashboard to restore its initial hidden state, stopped only the owned unit,
and verified it was unloaded/inactive with MainPID 0. No Plasma/KWin desktop
process or nested runtime remained; gamescope's focusable/focused lists were
empty again. Released only this task's lock, after checking its owner marker.
Delayed readback at **09:46:39 UTC** confirmed the lock absent, Game Mode active,
full Plasma inactive and the original Steam, vrserver and vrcompositor PIDs
still running. The Game Mode service invocation was unchanged. Battery was
99% at cleanup and 100%/Full on the delayed check. Closed both owned preview
tabs, Electron and Xvfb; their processes and X socket were gone. No test server
remained. No reboot, installation, security/global setting change or full
Desktop Mode transition was performed.
The nested desktop journal included gamescope WSI/Vulkan-hook warnings. No
warning was dismissed. Capture produced nonblank compositor pixels, but this
run does not assess their usability to a wearer. Private image files stayed
outside the repository; only state and response metadata are published.
The initial CDP `response.body()` read yielded zero bytes despite successful
decoding in the page. The repeat verified the PNG Content-Length, fresh image
dimensions/source/selection and a 2,011,278-byte PNG exported from the canvas.
The initial raw-body measurement is a harness limitation, not evidence of a
product capture failure. No Live video, Control input, physical monitor, FUSE
mounting or hardware rendering was tested. Older SteamOS and other packages
remain outside this observation.
## Offline verification
`python3 -m unittest discover -s tests -p test_touch.py -v`: **42 tests passed**,
including the production page's Node panel behavior tests, duplicate-function
guard, absent versus empty panel diagnostics, alternate displays, capture/input
target validation and discovery deadlines. JSON parsing and `git diff --check`
were also checked. No new regression test was added because this is an
evidence-only change with no reproduced product defect.
@@ -0,0 +1,114 @@
# Linux PC host feasibility on lxso2, 2026-10-24
**Blocked on this host's current desktop environment. No feature enabled and
no feature PR opened.** Investigation for [issue #23](https://github.com/saphid/frame-control/issues/23),
starting at `4e45648` (main after #154). This concerns flat desktop panels,
not Linux PC VR streaming.
## Decision
The smallest useful candidate is one explicitly selected **whole screen**,
captured through the ScreenCast portal's restricted PipeWire connection,
encoded locally with GStreamer/x264, and sent to the existing viewer through
the shared SSH controller. Input, individual windows and virtual displays
would remain unsupported until separately verified. This candidate was **not
implemented**: lxso2 currently cannot exercise the real capture permission path.
Development packages could unblock compilation, but would not create a desktop,
portal backend or consented screen. Synthetic encoding, an Xvfb screen grab or
using the Frame as its own host would not verify Linux PC desktop capture.
No host packages or services were changed.
## Evidence
Host: `lxso2`, Linux x86-64, kernel `6.8.0-139-generic`.
- `DISPLAY`, `WAYLAND_DISPLAY` and `XDG_SESSION_TYPE` were unset. The user bus
was reachable at `/run/user/1000/bus`.
- `/run/user/1000/pipewire-0` and `/run/user/1000/wayland-0` did not exist.
No processes matched Xorg, Xwayland, Xvfb, GNOME Shell, KWin Wayland, Weston,
PipeWire, WirePlumber or xdg-desktop-portal at the process check.
- D-Bus `ListNames` returned D-Bus, systemd and anonymous connections, with no
`org.freedesktop.portal.Desktop`. `ListActivatableNames` also had no desktop
portal. These calls queried D-Bus itself without activating capture services.
- `dpkg-query` found no `xdg-desktop-portal*` or `pipewire*` packages.
`libgstreamer1.0-0:amd64` was installed at `1.24.2-1ubuntu0.1`, but
`gstreamer1.0-tools` was uninstalled. `gst-launch-1.0` and `gst-inspect-1.0`
were absent from PATH. Xvfb was installed, but supplies no portal by itself.
- `pkg-config --modversion` failed for `gstreamer-1.0`,
`gstreamer-app-1.0`, `gstreamer-video-1.0` and `libpipewire-0.3`.
`gio-unix-2.0` reported `2.80.0`.
- `desktop/bundle/pc-host.so` did not exist.
`python3 desktop/build.py --with-x264 --out /tmp/frame-linux-pc-feasibility`
exited **1** before compilation: required GStreamer pkg-config modules were
missing. No native build or encoder runtime success is claimed.
Reproduce the non-activating portal checks in the intended desktop user's
session, using that session's actual runtime directory and bus:
```sh
gdbus call --session --dest org.freedesktop.DBus \
--object-path /org/freedesktop/DBus --method org.freedesktop.DBus.ListNames
gdbus call --session --dest org.freedesktop.DBus \
--object-path /org/freedesktop/DBus --method org.freedesktop.DBus.ListActivatableNames
pkg-config --modversion gstreamer-1.0 gstreamer-app-1.0 gstreamer-video-1.0 gio-unix-2.0
```
## Existing implementation reviewed
- `ui/frame_pcview.py` selects PCView only on Windows and rejects Linux even
when enabled. Linux installers do not carry its native bundle.
- `desktop/portal.c` creates a **RemoteDesktop** session, requests pointer and
keyboard devices, and selects monitors and windows (`types=3`). It is not
ScreenCast-only or screen-only. Its restricted fd/node and fresh-connection
handling are reusable.
- `ui/frame_pc_agent.py` calls the selection a shared window or screen and
picks VA-API when the element is present, falling back to x264. A bounded
x264 slice needs explicit encoder policy and unsupported-input behavior;
exposing the existing Linux path alone would not meet that scope.
- `app/main.js` starts Python with the inherited environment; it does not
supply a compositor, portal backend or capture permission.
- `.github/workflows/pc-host.yml` installs native dependencies and tests
synthetic x264/protocol behavior with fake capture consent/input. This does
not establish interactive Linux desktop capture.
- Issue #23 and its six existing comments distinguish prior synthetic
helper/viewer results from untested Linux desktop capture and consent.
## Verification and limits
`python3 -m unittest discover -s tests -p 'test_pc*.py' -v` exited **0**:
27 tests ran successfully, with one additional native-class setup skip
(`native PC libraries not built on this host`). Coverage includes the compiled
C controller trace, Python adapters, grants and default-off/Windows-only gates.
It does not prove native GStreamer capture or local-client streaming.
No fake/local native stream, consent prompt, real desktop capture, Frame viewer
run, benchmark, input or window mode was tested in this investigation. No SSH
connection to the Frame was needed; no lock was acquired and no headset install,
launch, stop or settings change was attempted. Issue #113 was left alone.
No issue comments or other public messages were sent.
## Concrete unblock and next slice
1. Supply an interactive Linux desktop, physical or VM, with a working
ScreenCast portal/backend, PipeWire and accessible consent dialog. Installing
the portal frontend alone is insufficient. A VM can establish software
capture/x264 behavior while physical-host behavior remains unverified.
2. Install the native dependencies listed in `pc-host.yml` in that environment
and build a local `--with-x264` bundle. Resolve x264 distribution
licensing/source obligations before shipping it: the current build labels
that option test-only and the Windows installer omits it.
3. Implement a ScreenCast-only session with monitor-only selection, no
RemoteDesktop/device request, disabled input in protocol and viewer, hidden
window/virtual-display modes, and explicit x264 selection. Preserve the
default-off gate and existing Mac/Windows paths.
4. Verify consent acceptance/refusal/cancellation, Stop/reopen, connection loss
and portal closure with a local client decoding actual captured content.
Run the shared benchmark and record results without inferring hardware or
physical laser behavior.
5. Only after local capture works, acquire the shared Frame lock once and
follow [the headset procedure](../testing.md#headset-smoke-test) for a brief
viewer test. If occupied, leave the lock alone and defer device testing.
These are prerequisites and proposed scope, not a ready feature. The Linux
host request in #23 remains open.
+168
View File
@@ -0,0 +1,168 @@
# Saved panel layouts: feasibility recheck, 2026-10-24
Issue: [#31](https://github.com/saphid/frame-control/issues/31). Base: main
`5b47cd3`. This investigation covers spatial save/restore for existing
Steam/gamescope panels without another overlay application.
## Decision and user behavior
Keep saved layouts unavailable. The current Frame exposes some geometry,
including a standing-space coordinate transform, but this investigation did
not establish a complete dock-state snapshot or a controlled restore path for
these panels. The existing companion/headset switcher still lists panels and
requests focus; users arrange them with SteamVR's dashboard controls. No layout
file, new control, overlay application or background service is added.
This is a limit of the investigated route, not proof that every native
implementation is impossible. In particular, failure of
`GetOverlayTransformAbsolute` does **not** mean all pose readback is denied.
## Device and read-only results
SteamOS **0.5.6**, BUILD_ID **20261015.6141207**; `vrcmd --version`:
**2.18.2**. `gamescope-session.service` was active and
`plasma-workspace.target` inactive: Game Mode. gamescope used the OpenVR backend
and `--virtual-connector-strategy PerAppId`. Battery was 99%, Not charging,
before the probe and 100%, Full, afterwards.
The [exact probe](panel-layout-2026-10-24/probe.py) loads only
`FnTable:IVROverlay_028` from the shipped
`/opt/steamvr/bin/linuxarm64/libopenvr_api.so`. It uses application type 3
(`VRApplication_Background`, avoiding runtime startup) and shuts down its own
OpenVR connection in `finally`. Initialization and interface acquisition both
returned 0. It calls getters only, with no image capture or input injection.
[Raw result](panel-layout-2026-10-24/readback.json):
| Getter | `valve.steam.desktopgame.0` | `valve.steam.gamepadui.main` |
|---|---|---|
| FindOverlay | 0, Success | 0, Success |
| Rendering process | `gamescope-wl` | `gamescope-wl` |
| Width | 0; 2.6700000763 m | 0; 2.6670000553 m |
| Curvature | 0; 0 | 0; 0 |
| Transform type | 0; 5, DashboardTab | 0; 5, DashboardTab |
| Absolute transform | 18, WrongTransformType | 18, WrongTransformType |
| Tracked-device-relative transform | 18, WrongTransformType | 18, WrongTransformType |
| Coordinate transform, center, standing origin | 0; identity matrix | 0; scaled/translated matrix |
| Coordinate transform, center, seated origin | 23, RequestFailed | 23, RequestFailed |
| Coordinate transform, center, raw origin | 23, RequestFailed | 23, RequestFailed |
The dashboard and both panels were reported hidden; neither panel was active.
The standing-space matrix is the transform for the overlay point `(0.5, 0.5)`;
it does not identify World/Dashboard/Theater/controller docking or recreate a
dashboard frame. The Steam panel's basis has scale about 0.369. Treat neither
that scaled matrix nor the hidden Gamescope panel's identity as a validated
physical placement to feed to an absolute-transform setter. Visible placement,
matrix freshness and correspondence to dashboard size were not checked by a
wearer.
## API and dashboard boundary
The ABI and result names were checked against Valve's
[OpenVR v2.12.14 C header](https://github.com/ValveSoftware/openvr/blob/v2.12.14/headers/openvr_capi.h)
and [C++ interface comments](https://github.com/ValveSoftware/openvr/blob/v2.12.14/headers/openvr.h).
That header declares `IVROverlay_028`; its SHA-256 is
`d58a2b149e434b2b8ee25cb4253ca6157feed8092f621e78ecfed6846d35d3e4`.
The absolute getter is explicitly limited to absolute transforms. The
coordinate getter returns the 3D transform for a 2D overlay point. Public
`IVROverlay_028` has no getter/setter pair for dashboard dock location or frame
scene-graph state. The probe fails closed if that exact interface is absent.
Read-only inspection of the shipped dashboard
`/opt/steamvr/resources/webinterface/dashboard/chunk~8012d0c89.js`
(SHA-256 `937ec62c67a481f030a416099ceb98b0fe2a702aea5a0705601c6c40d4969f6d`)
found:
- `dockLocation` reads an internal `_getDockLocation(frameID)` store;
`SetDockLocation` updates that store or sends a VRLink request.
- `m_mapLastRelativeTransformForDockLocation` keeps per-location transforms.
World initialization requests a dashboard scene-graph transform through
`requestSGTransform`, then updates the component's `xfTransform`.
- The `vrcmd_dock_overlay` handler accepts `dashboard`, `lefthand`, `righthand`,
`theater` and `world`. It selects the frame associated with the supplied key,
**falls back to the active frame if no match exists**, sets dock location and
may switch frame. This is not an exact pose restore or a readback response.
Those are observations of shipped internal code, not proof that Frame Control
can safely access its state through a supported external API. No dashboard
JavaScript was injected, private store edited or VRLink command sent. The
active-frame fallback is an additional reason not to experiment with docking
commands on a stale key.
The [2026-09-28 probe](../panels.md#saved-spatial-layouts-blocked-on-the-current-panel-route)
on SteamOS 0.4.1 / SteamVR 2.18.1 recorded an absolute setter returning
12 (`PermissionDenied`) on a temporary, test-owned gamescope panel. That setter
result is **historical**, not a new test on 2.18.2. The newer read-only results
above neither overturn nor reverify it.
## Commands, safety and cleanup
Read-only inspection used `ssh frame` with `BatchMode=yes` and
`ConnectTimeout=10`, `cat /etc/os-release`, battery sysfs reads,
`systemctl --user is-active gamescope-session.service plasma-workspace.target`,
process inspection, and `vrcmd --version`, `--help`, `--overlays`. An initial
`--listoverlays` attempt returned `Unknown command`; `--overlays` is the actual
enumeration command. Dashboard sources were read/copied to local `/tmp` for
inspection. No files were installed on the Frame.
Run the retained probe from the repository root, following
[the shared-device rules](../testing.md#headset-smoke-test). Stop if the lock is
occupied or battery is at/below 20%; never remove another task's lock:
```sh
ssh -o BatchMode=yes -o ConnectTimeout=10 frame \
'set -eu; mkdir /tmp/frame-test.lock || exit 3; trap "rmdir /tmp/frame-test.lock" EXIT; timeout 25 python3 -' \
< docs/evidence/panel-layout-2026-10-24/probe.py
```
This command acquired the lock, completed with exit 0 and released it. A
subsequent check confirmed the lock absent, Game Mode active, and Steam,
`vrserver` and `vrcompositor` running. The probe's only headset filesystem
change was creating/removing that shared lock. Its Python source ran from
stdin. There were no test panels, installs, profiles or services to remove.
No mutation test was essential: without verified dock/relative-frame readback,
there was no exact original layout and bounded rollback to justify changing a
panel. Existing user panels were never focused, shown, hidden, resized, moved
or docked. No global settings, security, passwords, runtime processes or modes
were changed; no reboot or reset occurred.
## Validation and remaining gaps
Existing `tests/test_panels.py` covers main-panel enumeration, hidden panels,
focus revalidation, unavailable-runtime errors and HTTP authentication/Host/
Origin guards. `tests/test_page_scripts.py` checks the production page's shared
JavaScript scope. These offline checks do not prove spatial placement. The
retained probe is evidence tooling, not a new product API; no fake layout
backend or layout tests claiming device support were added.
Validation passed:
```sh
python3 -m unittest discover -s tests -p test_panels.py -v # 8 passed
python3 -m unittest discover -s tests -p test_page_scripts.py -v # 5 passed
python3 -m py_compile docs/evidence/panel-layout-2026-10-24/probe.py
python3 -m json.tool docs/evidence/panel-layout-2026-10-24/readback.json
git diff --check
```
The initial panel-suite attempt hit sandbox socket restrictions in four HTTP
tests; rerunning with loopback sockets permitted passed all eight. No real
headset connection is used by those tests. The full suite and fake Frame were
not run for this evidence-only change.
Before shipping layouts, a native route still needs proof of dock state and
relative-frame readback, authorized restore on an owned test panel with exact
rollback, and correspondence between saved size/pose and visible placement.
It also needs stable source identity and a fresh capture-permission/ticket
flow when reopening viewers. Reconnect, panel recreation, reboot recovery,
Steam-launched gameplay, controller docking and worn-headset input were not
tested here. The existing Frame Control-owned media renderer is not proof that
Steam/gamescope panels can be restored. No third-party overlay app was used.
## Announcement
We rechecked saved panel layouts on SteamOS 0.5.6. SteamVR exposes some panel
geometry, but we have not verified safe restoration of Steam's panel positions
and docking. Saved layouts remain unavailable; use SteamVR's dashboard to
arrange panels. The panel switcher continues to list and show open panels.
@@ -0,0 +1,89 @@
"""Read-only issue #31 probe; pipe to `ssh frame 'timeout 25 python3 -'`.
Acquire /tmp/frame-test.lock first per docs/testing.md. No overlay is created,
focused, docked or changed. Background init (3) does not start SteamVR.
ABI: ValveSoftware/openvr v2.12.14 headers/openvr_capi.h, IVROverlay_028.
"""
import ctypes as C
import json
from pathlib import Path
H = C.c_uint64
Matrix = C.c_float * 12
class Vector2(C.Structure):
_fields_ = [('v', C.c_float * 2)]
def probe():
vr = C.CDLL('/opt/steamvr/bin/linuxarm64/libopenvr_api.so')
vr.VR_InitInternal2.argtypes = [C.POINTER(C.c_int), C.c_int, C.c_char_p]
vr.VR_InitInternal2.restype = C.c_uint32
vr.VR_GetGenericInterface.argtypes = [C.c_char_p, C.POINTER(C.c_int)]
vr.VR_GetGenericInterface.restype = C.c_void_p
vr.VR_ShutdownInternal.argtypes = []
vr.VR_ShutdownInternal.restype = None
err = C.c_int()
vr.VR_InitInternal2(C.byref(err), 3, None)
report = {'init_error': err.value, 'interface': 'IVROverlay_028', 'panels': []}
if err.value:
return report
try:
ptr = vr.VR_GetGenericInterface(b'FnTable:IVROverlay_028', C.byref(err))
report['interface_error'] = err.value
if err.value or not ptr:
return report
table = C.cast(ptr, C.POINTER(C.c_void_p))
def function(slot, result, *args):
return C.CFUNCTYPE(result, *args)(table[slot])
def read(slot, handle, output_type):
output = output_type()
rc = function(slot, C.c_int, H, C.POINTER(output_type))(handle, C.byref(output))
return {'rc': rc, 'value': output.value if rc == 0 else None}
report['dashboard_visible'] = function(68, C.c_bool)()
for key in ('valve.steam.desktopgame.0', 'valve.steam.gamepadui.main'):
row = {'key': key}
report['panels'].append(row)
handle = H()
row['find_rc'] = function(0, C.c_int, C.c_char_p, C.POINTER(H))(
key.encode(), C.byref(handle))
if row['find_rc']:
continue
pid = function(10, C.c_uint32, H)(handle)
row['rendering_pid'] = pid
try:
row['rendering_comm'] = Path('/proc/%d/comm' % pid).read_text().strip()
except OSError:
row['rendering_comm'] = None
row['visible'] = function(45, C.c_bool, H)(handle)
row['active_dashboard'] = function(69, C.c_bool, H)(handle)
row['width_m'] = read(23, handle, C.c_float)
row['curvature'] = read(25, handle, C.c_float)
row['transform_type'] = read(32, handle, C.c_int)
for name, slot, scalar in (('absolute', 34, C.c_int),
('tracked_device_relative', 36, C.c_uint32)):
origin = scalar()
matrix = Matrix()
rc = function(slot, C.c_int, H, C.POINTER(scalar), C.POINTER(Matrix))(
handle, C.byref(origin), C.byref(matrix))
row[name] = {'rc': rc, 'origin_or_device': origin.value if rc == 0 else None,
'matrix': list(matrix) if rc == 0 else None}
row['coordinate_transforms'] = []
for origin in (0, 1, 2): # seated, standing, raw/uncalibrated
matrix = Matrix()
rc = function(46, C.c_int, H, C.c_int, Vector2, C.POINTER(Matrix))(
handle, origin, Vector2((.5, .5)), C.byref(matrix))
row['coordinate_transforms'].append({
'tracking_origin': origin, 'coordinates': [.5, .5], 'rc': rc,
'matrix': list(matrix) if rc == 0 else None})
return report
finally:
vr.VR_ShutdownInternal()
if __name__ == '__main__':
print(json.dumps(probe(), indent=2, allow_nan=False))
@@ -0,0 +1,152 @@
{
"init_error": 0,
"interface": "IVROverlay_028",
"panels": [
{
"key": "valve.steam.desktopgame.0",
"find_rc": 0,
"rendering_pid": 297037,
"rendering_comm": "gamescope-wl",
"visible": false,
"active_dashboard": false,
"width_m": {
"rc": 0,
"value": 2.6700000762939453
},
"curvature": {
"rc": 0,
"value": 0.0
},
"transform_type": {
"rc": 0,
"value": 5
},
"absolute": {
"rc": 18,
"origin_or_device": null,
"matrix": null
},
"tracked_device_relative": {
"rc": 18,
"origin_or_device": null,
"matrix": null
},
"coordinate_transforms": [
{
"tracking_origin": 0,
"coordinates": [
0.5,
0.5
],
"rc": 23,
"matrix": null
},
{
"tracking_origin": 1,
"coordinates": [
0.5,
0.5
],
"rc": 0,
"matrix": [
1.0,
0.0,
0.0,
0.0,
0.0,
1.0,
0.0,
0.0,
0.0,
0.0,
1.0,
0.0
]
},
{
"tracking_origin": 2,
"coordinates": [
0.5,
0.5
],
"rc": 23,
"matrix": null
}
]
},
{
"key": "valve.steam.gamepadui.main",
"find_rc": 0,
"rendering_pid": 297037,
"rendering_comm": "gamescope-wl",
"visible": false,
"active_dashboard": false,
"width_m": {
"rc": 0,
"value": 2.6670000553131104
},
"curvature": {
"rc": 0,
"value": 0.0
},
"transform_type": {
"rc": 0,
"value": 5
},
"absolute": {
"rc": 18,
"origin_or_device": null,
"matrix": null
},
"tracked_device_relative": {
"rc": 18,
"origin_or_device": null,
"matrix": null
},
"coordinate_transforms": [
{
"tracking_origin": 0,
"coordinates": [
0.5,
0.5
],
"rc": 23,
"matrix": null
},
{
"tracking_origin": 1,
"coordinates": [
0.5,
0.5
],
"rc": 0,
"matrix": [
0.3689989447593689,
0.0,
0.0008894503116607666,
-0.5048462152481079,
0.0,
0.36899998784065247,
0.0,
1.2510287761688232,
-0.0008894503116607666,
0.0,
0.3689989447593689,
-1.1726053953170776
]
},
{
"tracking_origin": 2,
"coordinates": [
0.5,
0.5
],
"rc": 23,
"matrix": null
}
]
}
],
"interface_error": 0,
"dashboard_visible": false
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

@@ -0,0 +1,226 @@
{
"date_utc": "2026-10-24T09:27:25.354Z",
"result": "PASS: literal packaged desktop Return to Game Mode button switched the real Frame from full Plasma; a repeat visible click made no second switch",
"documentation_base_commit": "71288d8980040c14f777f0e417f41f7105eaaac4",
"host": "lxso2",
"steamos_version": "0.5.6",
"steamos_build": "20261015.6141207",
"prior_evidence": [
"https://github.com/saphid/frame-control/issues/142",
"https://github.com/saphid/frame-control/pull/143",
"https://github.com/saphid/frame-control/pull/156"
],
"package": {
"tag": "v0.4.17",
"asset": "Frame-Control-linux-x86_64.AppImage",
"url": "https://github.com/saphid/frame-control/releases/download/v0.4.17/Frame-Control-linux-x86_64.AppImage",
"sha256": "b1b8eb82bf73a227b2cd36e8f0ef3a2ebdf7967c5853efb08a9634f38227015d",
"verification": "sha256sum matched the live release asset digest. Independently extracted the retained asset into a new private test directory. cmp confirmed bundled frame_backup.py and frame_backup_remote.py equal the checkout versions. No source or payload was changed.",
"launch": "--appimage-extract, then squashfs-root/AppRun --no-sandbox --disable-gpu --remote-debugging-port=49372 --remote-debugging-address=127.0.0.1 --user-data-dir=/tmp/frame-return-button-20261024/electron-profile --window-size=1400,950",
"display": "Xvfb :138 -screen 0 1440x1000x24 -nolisten tcp; Electron content screenshot 1400x923",
"isolation": "Private XDG_CONFIG_HOME, XDG_CACHE_HOME, FRAME_CONTROL_DATA_DIR and Electron profile; FRAME_CONTROL_TELEMETRY=0 and FRAME_CONTROL_NO_UPDATE_CHECK=1. Existing authorized ssh frame identity retained; bundled server and Python used."
},
"preflight": {
"read_only_first": true,
"initial_mode": "gamescope-session.target=active; plasma-workspace-wayland.target=inactive",
"initial_battery_percent": 99,
"initial_battery_status": "Not charging; charger Full",
"steam_and_steamvr_running": true,
"running_steam_apps": [],
"default_login_mode": "game",
"lock": "mkdir /tmp/frame-test.lock succeeded atomically; no foreign lock was removed. Initial harmless probe attempt released its own lock, then a fresh atomic acquisition covered both full-session attempts."
},
"rollback": {
"unit": "fc-return-button-rollback-20261024",
"probe": "systemd-run --user --unit=fc-return-button-probe-20261024 --on-active=2s --timer-property=AccuracySec=1s --property=Type=oneshot --property=RemainAfterExit=yes /usr/bin/true",
"probe_execution": "Result=success\nExecMainStartTimestamp=Sat 2026-10-24 20:27:07 AEDT\nExecMainExitTimestamp=Sat 2026-10-24 20:27:07 AEDT\nExecMainCode=1\nExecMainStatus=0\n",
"arm": "systemd-run --user --unit=fc-return-button-rollback-20261024 --on-active=180s --timer-property=AccuracySec=1s /bin/sh -c <guarded command>",
"guarded_command": "if [ \"$(systemctl --user is-active gamescope-session.target)\" != active ]; then exec /usr/bin/steamosctl switch-to-game-mode; fi",
"verified_before_desktop": "TimersMonotonic={ OnActiveUSec=3min ; next_elapse=1d 21h 53min 49.130174s }\nNextElapseUSecMonotonic=1d 21h 53min 49.130174s\nBindsTo=\nPartOf=\nActiveState=active\nExecStart={ path=/bin/sh ; argv[]=/bin/sh -c if [ \"$(systemctl --user is-active gamescope-session.target)\" != active ]; then exec /usr/bin/steamosctl switch-to-game-mode; fi ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=0 ; code=(null) ; status=0/0 }\nBindsTo=\nPartOf=\n",
"user_manager_linger": "yes",
"desktop_entry": "ssh frame steamosctl switch-to-desktop-mode",
"first_click_utc": "2026-10-24T09:27:27.037Z",
"cancelled_before_deadline": true,
"fired": false,
"limit": "The benign transient timer was observed executing. The actual rollback timer configuration was read back, then cancelled after the UI restored Game Mode; its recovery action was not deliberately allowed to fire."
},
"ui": {
"navigation": "Clicked nav Tools, Backup & recovery, and Check in the actual packaged Electron window. Initial Game Mode disabled Return. After full Plasma entry, Check enabled it.",
"automation": "Playwright connected to the packaged Electron instance over local CDP and used locator.click() on the visible button; accepted the native JavaScript confirm dialog. No Return API/CLI or production handler was directly invoked by the test. T3 preview was inspected separately and closed; it cannot control this Electron window.",
"clicks": [
{
"isTrusted": true,
"utc": "2026-10-24T09:27:27.037Z"
},
{
"isTrusted": true,
"utc": "2026-10-24T09:27:32.942Z"
}
],
"button_bounds_css_pixels": {
"x": 47,
"y": 709.59375,
"width": 156.203125,
"height": 44
},
"confirmation_dialogs": [
{
"type": "confirm",
"message": "Return the headset to Game Mode?\n\nThis closes the desktop and any apps open in it, then starts Steam's Game Mode.",
"utc": "2026-10-24T09:27:27.042Z"
},
{
"type": "confirm",
"message": "Return the headset to Game Mode?\n\nThis closes the desktop and any apps open in it, then starts Steam's Game Mode.",
"utc": "2026-10-24T09:27:32.943Z"
}
],
"desktop_check": {
"http": 200,
"body": {
"mode": "desktop",
"gamescope": true,
"plasma": true,
"nested_desktop": false,
"configured": "gamescope-wayland.desktop",
"config_file": "/etc/sddm.conf.d/steamos.conf",
"selector": "steamosctl",
"session_targets": {
"gamescope-session.target": "inactive",
"plasma-workspace-wayland.target": "active"
},
"mode_source": "session-targets"
}
},
"return_responses": [
{
"http": 200,
"body": {
"message": "Asked the headset to return to Game Mode; it restarts its session in a few seconds",
"state": {
"mode": "desktop",
"gamescope": true,
"plasma": true,
"nested_desktop": false,
"configured": "gamescope-wayland.desktop",
"config_file": "/etc/sddm.conf.d/steamos.conf",
"selector": "steamosctl",
"session_targets": {
"gamescope-session.target": "inactive",
"plasma-workspace-wayland.target": "active"
},
"mode_source": "session-targets"
},
"switched": true,
"command": "steamosctl switch-to-game-mode"
},
"utc": "2026-10-24T09:27:27.316Z"
},
{
"http": 200,
"body": {
"message": "The headset is already in Game Mode. The desktop you see is a panel: close it from the Steam dashboard.",
"state": {
"mode": "game",
"gamescope": true,
"plasma": false,
"nested_desktop": false,
"configured": "gamescope-wayland.desktop",
"config_file": "/etc/sddm.conf.d/steamos.conf",
"selector": "steamosctl",
"session_targets": {
"gamescope-session.target": "active",
"plasma-workspace-wayland.target": "inactive"
},
"mode_source": "session-targets"
},
"switched": false
},
"utc": "2026-10-24T09:27:33.126Z"
}
],
"final_check": {
"http": 200,
"body": {
"mode": "game",
"gamescope": true,
"plasma": false,
"nested_desktop": false,
"configured": "gamescope-wayland.desktop",
"config_file": "/etc/sddm.conf.d/steamos.conf",
"selector": "steamosctl",
"session_targets": {
"gamescope-session.target": "active",
"plasma-workspace-wayland.target": "inactive"
},
"mode_source": "session-targets"
}
},
"final_return_disabled": true,
"final_text": "Game Mode: the headset is running normally. A desktop you see is a panel; close it from the Steam dashboard.",
"screenshots": [
"return-game-mode-packaged-2026-10-24-before.png",
"return-game-mode-packaged-2026-10-24-after.png"
],
"screenshot_scope": "Cropped to the session controls and repeat-action notification; host identity, connection address and backup folder excluded."
},
"systemd_verification": {
"command": "ssh frame systemctl --user show gamescope-session.target plasma-workspace-wayland.target -p Id -p ActiveState -p SubState; ssh frame systemctl --user show gamescope-session.service -p InvocationID",
"before": "Id=gamescope-session.target\nActiveState=inactive\nSubState=dead\n\nId=plasma-workspace-wayland.target\nActiveState=active\nSubState=active\nInvocationID=892b843dbe1a4d2ba91b93057bd38463\n295589 /home/steamos/.local/share/Steam/steamrtarm64/steam -silent -vrdisable -deckard\n295066 /opt/steamvr/bin/linuxarm64/vrserver -waitformonitor -ipccontrolfilepath /tmp/SteamVR-IPCControlFile-294978\n",
"after_first_click": "Id=gamescope-session.target\nActiveState=active\nSubState=active\n\nId=plasma-workspace-wayland.target\nActiveState=inactive\nSubState=dead\nInvocationID=e8c1085b3bbd4e79a323b3e4a9bc6355\n297506 /opt/steamvr/bin/linuxarm64/vrserver -waitformonitor -ipccontrolfilepath /tmp/SteamVR-IPCControlFile-297420\n",
"after_repeat_click": "Id=gamescope-session.target\nActiveState=active\nSubState=active\n\nId=plasma-workspace-wayland.target\nActiveState=inactive\nSubState=dead\nInvocationID=e8c1085b3bbd4e79a323b3e4a9bc6355\n297826 /home/steamos/.local/share/Steam/steamrtarm64/steam -cef-enable-debugging -deckard -gamepadui -steamdeck -steamos3 -vrgamepadui\n297942 /home/steamos/.local/share/Steam/steamrtarm64/steam -child-update-ui -child-update-ui-socket 8 -cef-enable-debugging -deckard -gamepadui -steamdeck -steamos3 -vrgamepadui\n297506 /opt/steamvr/bin/linuxarm64/vrserver -waitformonitor -ipccontrolfilepath /tmp/SteamVR-IPCControlFile-297420\n",
"repeat_did_not_restart_game_mode": true,
"unchanged_game_service_invocation": "e8c1085b3bbd4e79a323b3e4a9bc6355",
"unchanged_steamvr_pid": 297506,
"delayed_final_readback_utc": "2026-10-24T09:28:05Z"
},
"cleanup": {
"complete": true,
"final_readback": "2026-10-24T09:28:05Z\nId=gamescope-session.target\nActiveState=active\n\nId=plasma-workspace-wayland.target\nActiveState=inactive\nInvocationID=e8c1085b3bbd4e79a323b3e4a9bc6355\nNEXT LEFT LAST PASSED UNIT ACTIVATES\n\n0 timers listed.\nId=fc-return-button-rollback-20261024.timer\nLoadState=not-found\nActiveState=inactive\n\nId=fc-return-button-rollback-20261024.service\nLoadState=not-found\nActiveState=inactive\n\nId=fc-return-button-probe-20261024.timer\nLoadState=not-found\nActiveState=inactive\n\nId=fc-return-button-probe-20261024.service\nLoadState=not-found\nActiveState=inactive\n297826 /home/steamos/.local/share/Steam/steamrtarm64/steam -cef-enable-debugging -deckard -gamepadui -steamdeck -steamos3 -vrgamepadui\n297506 /opt/steamvr/bin/linuxarm64/vrserver -waitformonitor -ipccontrolfilepath /tmp/SteamVR-IPCControlFile-297420\n99\nCharging\ngame\n",
"owned_lock_release_exit": 0,
"owned_lock_absent": true,
"host_processes": {
"xvfb": {
"pid": 1562020,
"exists": false,
"cmd": null,
"state": null
},
"electron": {
"pid": 1562023,
"exists": false,
"cmd": null,
"state": null
},
"x_socket_absent": true
},
"remaining_test_servers": [],
"battery_final_percent": 99,
"battery_final_status": "Charging",
"battery_range_observed_percent": [
99,
100
],
"game_mode_steam_steamvr_running": true,
"default_login_mode_after": "game",
"headset_installs_or_global_settings_changed": false,
"rebooted": false,
"notes": "Stopping the timer succeeded. systemctl stop also named its never-started, unloaded service and returned 5; explicit readback confirms both rollback and probe timer/service units are not-found/inactive. No further switch was sent."
},
"attempts_and_harness_limits": [
"Initial harmless timer probe was garbage-collected before its exit metadata could be read. No Desktop Mode switch was attempted. Repeated with RemainAfterExit=yes and verified successful execution.",
"First full Plasma attempt clicked the visible packaged Return button and got HTTP 200 / switched=true. The harness then treated absent pgrep matches during the expected session restart as a fatal error. Independent readback confirmed Game Mode and SteamVR had returned. Rollback remained armed until that readback, then was cancelled. This incomplete attempt was not counted as the final pass.",
"Final full Plasma attempt tolerated transiently absent Steam/SteamVR while polling systemd targets, passed both trusted visible clicks, and completed the final Check. Stopping the unused rollback service returned unit-not-loaded; explicit subsequent readback verified all four owned probe/rollback units not-found/inactive and zero timers. Cleanup was completed before releasing our lock."
],
"limits": [
"Unchanged AppImage payload ran after extraction on a virtual X desktop with Chromium sandbox and GPU disabled. FUSE mounting, desktop integration and hardware rendering were not tested.",
"Automated trusted pointer clicks in a visible Electron window; no physical-monitor human click or worn-headset visual/interaction assessment.",
"Verified only SteamOS 0.5.6 build 20261015.6141207 and the Linux x86_64 v0.4.17 package; older selectors/builds and other desktop platforms remain covered only by their existing tests.",
"No release, tag, application code, passwords or security/global settings changed."
],
"validation": {
"focused_suite": "python3 -m unittest discover -s tests -p test_backup.py -v: 28 tests passed in 2.479s",
"json": "python3 -m json.tool: passed",
"diff": "git diff --check: passed"
}
}
+18 -1
View File
@@ -190,7 +190,24 @@ HTTP focus is not evidence of worn-headset laser input. Follow the
## Saved spatial layouts: blocked on the current panel route
**Verified 2026-09-28**, same build, using a temporary xterm panel with
**Rechecked 2026-10-24**, SteamOS 0.5.6, BUILD_ID `20261015.6141207`,
SteamVR 2.18.2: a read-only probe of the existing Gamescope and Steam main
panels still reports type 5 (`DashboardTab`), and both the absolute and
tracked-device-relative getters return 18 (`WrongTransformType`). Width and
curvature read successfully. **Some pose readback is available:**
`GetTransformForOverlayCoordinates` at the panel center succeeds in standing
space; seated and raw origins return 23 (`RequestFailed`). Both panels and the
dashboard were hidden. That matrix snapshot does not identify docking, prove
visible placement, or establish a restore operation. The shipped dashboard
stores dock location and relative transforms internally; no supported external
snapshot/restore pair was established. No setters or docking commands were run
in this recheck. See the [exact probe, results, cleanup and remaining
gaps](evidence/panel-layout-2026-10-24.md). Saved layouts remain unavailable.
### Earlier mutation evidence (2026-09-28)
**Verified 2026-09-28**, SteamOS 0.4.1, BUILD_ID `20260925.6191901`,
SteamVR 2.18.1, using a temporary xterm panel with
`STEAM_GAME=2000999031` and `FnTable:IVROverlay_028` from
`/opt/steamvr/bin/linuxarm64/libopenvr_api.so`:
+6
View File
@@ -107,6 +107,12 @@ The Linux adapter from #38 (desktop portal, PipeWire, VA-API) is in the tree,
because it shares `capture.c` and the controller, and CI still builds and tests
it. The app never offers it and the Linux installers don't include it.
The [2026-10-24 lxso2 feasibility check](evidence/linux-pc-host-lxso2-2026-10-24.md)
found no running or activatable desktop portal, no PipeWire socket and missing
native build dependencies. Linux support remains disabled. The evidence records
the build failure and prerequisites for a screen-only, view-only x264 slice;
Linux capture, input and window modes have not been verified there.
## Measuring
The Mac benchmark accepts the PC host:
+31 -3
View File
@@ -170,14 +170,42 @@ The temporary server stopped, the shared device lock was released, and the
headset remained in Game Mode at 99% battery. See
[`docs/evidence/clip-gui-export-2026-10-24.json`](evidence/clip-gui-export-2026-10-24.json)
for the narrow proof. This did not exercise the packaged desktop installer.
On 2026-10-24, the exact v0.4.17 tag exported a second real Steam-saved clip
through `frame_clips.export_clip`, the same production path used by **Export**.
Steam listed the clip at **100.801 seconds**, with 68 chunks and no gaps. The
copied MP4 was **100.833 seconds** by `ffprobe` and decoded completely with
`ffmpeg`; it was not cut at 60 seconds. The test restored Steam's recording
mode, removed its reserved title and processes, and left Game Mode active at
100% battery. See [the long-clip evidence](evidence/clip-long-2026-10-24.json).
That code-path test did not click **Export** in the packaged desktop app or
verify visible gameplay quality or perceptual audio/video sync.
Later that day, the published v0.4.17 x86_64 Linux AppImage also passed the
packaged desktop **Export** check against the retained 100.801-second clip.
The asset SHA-256 was verified before launching its unchanged extracted payload
in an isolated Electron profile on a virtual X desktop. An automated pointer
click on the visible **Tools → Capture & Live → Steam clips → Export** button
saved the MP4 and changed the row to **ON PC / Export again**. The saved file
was 100.833335 seconds, decoded completely, and matched the prior production
export by size and SHA-256. The app and test display were closed,
the Frame export cache was empty, the source clip was retained, and the shared
lock was released at 99% battery with Game Mode and SteamVR still running.
See [the packaged export evidence](evidence/clip-packaged-export-2026-10-24.json)
and its cropped before/after screenshots. This verifies the packaged Electron
button and bundled export path; AppImage FUSE mounting, desktop integration,
physical display/GPU rendering, gameplay quality and perceptual sync remain
unverified. The virtual-display launch used `--no-sandbox --disable-gpu`.
Remaining checks include:
- Perceptual audio/video sync and audible content. Frame Control's separate
**Record** path stamps camera and audio inputs with the wall clock because
they otherwise count from different clocks.
- Long saved clips, trims starting partway through a Steam recording, and the
packaged desktop app's **Export** button. One real saved clip's directory
layout was checked; the parser remains deliberately loose for other layouts.
- Clips longer than the 100-second test and trims starting partway through a
Steam recording. Two real saved clips' directory layouts were checked; the
parser remains deliberately loose
for other layouts.
- How much x264 at 1080p30 costs the Frame while a game is running.
- Automatic copying: that 30 seconds without a write is a safe sign Steam has
finished saving a clip, and how a once-a-minute listing feels on the Frame.
+7
View File
@@ -7,6 +7,9 @@ This covers three directions, plus input:
- **C. iPhone → Frame**: mirror the phone inside the headset.
- **PC VR from Linux**: [feasibility and options](linux-vr-streaming.md),
including Valve's streaming and USB support. No Linux host tested yet.
- **Linux desktop → Frame panels**: [PC-host feasibility on lxso2](evidence/linux-pc-host-lxso2-2026-10-24.md).
Blocked on missing desktop portal/PipeWire and native build dependencies;
Linux desktop capture remains disabled and unverified.
- **Input**: type and point in the Frame from the Mac or iPhone.
- **Live view and Control**: watch a panel flat and tap on it to use it.
@@ -292,6 +295,10 @@ How (**verified 2026-09-29**, SteamOS 0.4.1, build 20260925.6191901):
if it persists, report the exact message and SteamOS version. The confirmed
empty-list advice is retained when the property is present but empty.
[2026-10-24 investigation and verification limits](evidence/panel-discovery-2026-10-24.md).
A later [packaged v0.4.17 test on the real Frame](evidence/desktop-capture-packaged-2026-10-24.md)
captured a mapped, focused nested KDE compositor on SteamOS 0.5.6. SteamVR
still reported its overlay hidden, so that test does not establish the
wearer's visible-desktop condition in #73 or justify closing it.
- Tested from the iPhone app (Simulator): a tap on the Desktop view focused a
text box in the panel and the text field typed into it; a trackpad move went
exactly (+40, +25).
+8
View File
@@ -165,6 +165,14 @@ Record the build, actual interaction results, cleanup and any unworn-headset
limits alongside screenshots or logs. These are caller responsibilities; the
smoke script below does not acquire this shared lock itself.
[Packaged Desktop Capture evidence, 2026-10-24](evidence/desktop-capture-packaged-2026-10-24.md)
records v0.4.17 GUI button captures of a real mapped nested desktop on SteamOS
0.5.6, a bounded owned user unit, and cleanup back to the original Game Mode
state. Check X window mapping/focus and SteamVR overlay visibility separately:
successful capture of a mapped window does not prove the desktop is visible
to a wearer. That run's overlay stayed `not_visible`; #73 remains unverified
for its reported visible-desktop/0.4.5 condition. Only state/log metadata is public.
```sh
scripts/frame-smoke.sh # needs `ssh frame` to work without a password
scripts/frame-smoke.sh --pair # also pairs a throwaway key: approve it in the headset
+1 -1
View File
@@ -1738,7 +1738,7 @@
<button class="small" id="sessionCheck">Check</button></div>
<div class="sub" id="sessionState">Check which mode the headset is in.</div>
<div class="row" style="margin-top:12px"><button class="small" id="sessionGame" disabled>Return to Game Mode</button></div>
<div class="hint">Uses the same switch as Valve's developer tools (<code>steamos-session-select gamescope</code>).
<div class="hint">Uses Valve's supported Game Mode switch for this SteamOS build.
It closes the desktop and anything open in it. If the headset can't be reached, hold Power for 10 seconds to
restart it; <a href="https://github.com/saphid/frame-control/blob/main/docs/backup-and-recovery.md#stuck-in-desktop-mode" target="_blank">more ways back</a>.</div>
</section>