Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84122ade16 | ||
|
|
318d0cc298 | ||
|
|
c9b566c4e8 | ||
|
|
8b817ff4e1 | ||
|
|
aef3843f9c | ||
|
|
43d4a44b6b | ||
|
|
f87e112710 | ||
|
|
fb60e91181 | ||
|
|
e8c750bd53 | ||
|
|
20ae3bebab | ||
|
|
da58afa3e3 | ||
|
|
51a866389a | ||
|
|
1f992a4970 | ||
|
|
68ef5eb6e8 | ||
|
|
6fb05e7bd5 | ||
|
|
5ceef3f8dc | ||
|
|
65296a5d1f | ||
|
|
c75b480a72 | ||
|
|
0c02f6ca15 | ||
|
|
bffa2f01d3 | ||
|
|
4f871b2033 | ||
|
|
24e9ca3cb5 | ||
|
|
8d0a225349 | ||
|
|
68771a3e1b | ||
|
|
8f0d65fd50 | ||
|
|
1763462d12 | ||
|
|
488ba70e66 | ||
|
|
6e42d99bfe | ||
|
|
3d2b5a6d63 | ||
|
|
b07bf1da11 | ||
|
|
a3362b98f7 | ||
|
|
030a170fc1 | ||
|
|
05a05d430b | ||
|
|
786c2eaa85 | ||
|
|
2b1b5cb031 | ||
|
|
868badb442 | ||
|
|
638600510a | ||
|
|
4c3056f96f | ||
|
|
b516ece20b | ||
|
|
61db8747ec | ||
|
|
44f8989d11 | ||
|
|
fe2fab7a8f | ||
|
|
0cf923dbc2 | ||
|
|
94538965d9 | ||
|
|
e2d6fe4db5 | ||
|
|
fa80ebf556 | ||
|
|
e96f6da681 | ||
|
|
f294e62925 | ||
|
|
1b987a7a8e | ||
|
|
f68ab37aa0 | ||
|
|
b100ee8710 | ||
|
|
8ffc33eef4 | ||
|
|
dd596dc9b2 | ||
|
|
1555ae1248 | ||
|
|
6e0dbd230b | ||
|
|
6b51f9b93f | ||
|
|
0c04684e88 | ||
|
|
f7b9f388de | ||
|
|
2d81727415 | ||
|
|
543c710d96 | ||
|
|
ba8ff97b00 | ||
|
|
4965e53a28 | ||
|
|
8ae6320910 | ||
|
|
174ec07231 | ||
|
|
25f4daa59c | ||
|
|
161594db42 | ||
|
|
e0293c6386 | ||
|
|
e842e1cc02 | ||
|
|
922f7f31d4 | ||
|
|
efe0ed9df8 | ||
|
|
5e087a7d6a | ||
|
|
427a2d79af | ||
|
|
7cf06e0332 | ||
|
|
3c4c705253 | ||
|
|
c89c5aefae | ||
|
|
2fd2672237 | ||
|
|
a9e7f95dfc | ||
|
|
8df7fe68a7 | ||
|
|
81beaf9806 | ||
|
|
fa0669a952 | ||
|
|
382183b594 | ||
|
|
394b25c5c8 | ||
|
|
8071ae7fac | ||
|
|
538ac66499 | ||
|
|
fdac2a451a | ||
|
|
17056473ce | ||
|
|
030c0f676b | ||
|
|
4499949781 | ||
|
|
090f7fa1af | ||
|
|
12025e73dd | ||
|
|
8d4fb7b6e1 | ||
|
|
4b513e195f | ||
|
|
206a2448b3 | ||
|
|
104c003c44 | ||
|
|
b281cd5fdb | ||
|
|
4fec0656ff | ||
|
|
a205743561 | ||
|
|
dd3bdf5d35 | ||
|
|
ca10e109e6 | ||
|
|
4e8486418f | ||
|
|
d801ff6bc8 | ||
|
|
c67f79ebc6 | ||
|
|
c469327590 | ||
|
|
b00abe3ce1 | ||
|
|
eaf4e3d5b6 | ||
|
|
05f221b216 | ||
|
|
472c607efd | ||
|
|
33bb14e034 | ||
|
|
0c06e1142f | ||
|
|
358db1360f | ||
|
|
1b43c1c544 | ||
|
|
5ae303fc45 | ||
|
|
72edbebac1 | ||
|
|
ca80018d98 | ||
|
|
a585e24647 | ||
|
|
b42d85d7c4 | ||
|
|
c9501bd446 | ||
|
|
bc728bb121 | ||
|
|
838952b4f5 | ||
|
|
eab28b98c5 | ||
|
|
1f64d089e1 | ||
|
|
49de808650 | ||
|
|
54a2d84755 | ||
|
|
31b4c684fe | ||
|
|
5d483746f9 | ||
|
|
1f27cd47a3 | ||
|
|
ff592e41e8 | ||
|
|
72d755973f | ||
|
|
d02ff3c9aa | ||
|
|
107215525f | ||
|
|
a32e48367a | ||
|
|
95a226d54f | ||
|
|
6a6b6be5b5 | ||
|
|
01ad5e6b97 | ||
|
|
33d306a11e | ||
|
|
8e463a55de | ||
|
|
faca9bf396 | ||
|
|
e0a9b5f7cb | ||
|
|
3e906d55cf | ||
|
|
5e668f33d3 | ||
|
|
feb4658dde | ||
|
|
4ed7929b1c | ||
|
|
61a6b3e2b2 | ||
|
|
78fc926a48 | ||
|
|
503e64367c | ||
|
|
3deb279270 | ||
|
|
0df1212bfe |
No files matched your search
@@ -59,7 +59,21 @@ Detailed device findings per game/issue: docs/DEVICE_NOTES.md (read before debug
|
||||
agent ends at EOF); sources and costs in docs/FRAME_RUNTIME.md "Monitoring sources"; game card (fps from
|
||||
FrameBridge pacing; End game = Steam's Exit game, then cmd_stop), tiles with `C.Sparkline` (Flet canvas, no charts
|
||||
extension), details, a pooled process table (Game / Steam & SteamVR / All; right-click: end / force kill / end
|
||||
game; MON_CRITICAL needs force, MON_NEVER is refused)); settings; welcome; activity panel). Files, Screenshots and the Library share right-click menus
|
||||
game; MON_CRITICAL needs force, MON_NEVER is refused)); room (`views/room.py`, persistent, "experimental" pill:
|
||||
scan with progress + Cancel = agent v106 `room_capture_stop`, play area, delete; cache `ui/room_state.py` shared
|
||||
with the Frame page's short card, mesh cached in `<data>/rooms-cache/<frame>/`; in-window 3D `ui/room3d.py`
|
||||
RoomCanvas over `mr/view3d.py` (pure-Python projection, mesh reduced to ≤4k/≤1.5k edges at rest/moving, ≤18 fps
|
||||
worker; each redraw sends NEW shape controls: changing a Points list made Flet diff it item by item, 0.3-1 s);
|
||||
"Open full 3D view" = `mr/room_viewer.py` token server on 127.0.0.1 + hand-written WebGL `mr/viewer_page.py`,
|
||||
stopped on disconnect/close; Test Quest MR tip = `mr.TESTER_*`; editing cards `views/room_edit.py` (agent v107):
|
||||
Clean-up = fp_roomscan's refinement settings, `install/rooms.REFINE_DEFAULTS` = the owner's defaults + slider
|
||||
metadata (agent `ROOM_REFINE_DEFAULTS` must match, a test checks), saved non-defaults in the Frame's
|
||||
`rooms/refine.json`, used by every build; Apply = job → agent `room_refine` (`--refine-only` on a copy of the room
|
||||
folder from `room-raw.txt`/`room-mesh-raw.bin`, scan.json → status `report`/`labels`/`suggestions`); Labels =
|
||||
the label rules' decisions + Add as door / Add as closet; Furniture & screens = box editor (click a box in the
|
||||
canvas: `view3d.pick`) + doors/windows on a wall (`mr.room.plane_on_wall`), Save = `rooms.put_room` → `room_put
|
||||
edited` (keeps raw files, `edits.txt` survives a new clean-up; "Back to the scan's boxes" = keep_edits false); no
|
||||
eraser in the GUI (`rooms.refine(erase=)` only)); settings; welcome; activity panel). Files, Screenshots and the Library share right-click menus
|
||||
(one `ft.ContextMenu` per view, filled on right-click; on one of several selected items they act on the whole
|
||||
selection, `C.menu_targets`) and click-and-drag multi-select (`C.DragSelect`: pan start/end on the area + item
|
||||
hover events, which Flutter also sends with the button held; Flet can't report item positions, so no rubber band).
|
||||
@@ -251,7 +265,11 @@ Rick and Morty runs on the Frame via its catalog recipe (OpenVR, no Revive),
|
||||
restore = untrusted input (staged + checked first), rollback copy `<base>/saves-before-restore.tar.gz` (undo =
|
||||
`rollback: true`). PC `install/saves.py` (`<data>/save-backups/<pkg>/<ts>.tar.gz` + `.json`, keep `saves.keep`
|
||||
= 10), GUI `ui/views/saves_dialog.py` (game menu section "Saves", Frame page "Back up all saves", uninstall's
|
||||
"back up before deleting"), CLI `frameport saves backup|list|restore|undo`.
|
||||
"back up before deleting"), CLI `frameport saves backup|list|restore|undo`. Agent v101: PC folder `saves.folder`
|
||||
(Settings → Data; old folders stay listed in `saves.previous_folders`), automatic snapshots `saves.auto` (default
|
||||
off; flag `~/.local/share/frameport/save-snapshots/enabled`, Quest launchers run `_snapshot_close` in the EXIT
|
||||
trap under the launch lock, newest 3 per game on the Frame; `install/saves.sync_snapshots` imports them as
|
||||
"automatic" once, ledger `<data>/save-snapshots-synced.json`). Device-checked 2026-10-17 with BlazeRush.
|
||||
- `patches/` — **the unit of modularity**. `base.py` (Patch interface, registry), `overport.py` (overport CLI patch ids,
|
||||
discovered dynamically via `overport patches`), `frame/*.py` (one module per Frame fix), `settings.py` (FrameBridge
|
||||
adapter keys + device files as patches). Add a patch = add a module that calls `register(...)`.
|
||||
@@ -451,7 +469,9 @@ Repo is on an NTFS drive (`core.fileMode=false`); line endings are LF (`.gitattr
|
||||
- No XR_FB_display_refresh_rate (`xrGetDisplayRefreshRateFB` -7), yet OVRPort's dispatcher offers it and answered 0 Hz
|
||||
→ FrameBridge always answers with the measured rate (1/predictedDisplayPeriod, else 72). PreyVR (#306, idTech 4:
|
||||
`USERCMD_MSEC = 1000 / GetRefresh()`) had 0 ms tics: async thread spinning, `com_frameTime` stuck at 0, menu black.
|
||||
- Guardian STAGE bounds report 1×1 m (scene emulation uses ≥1.5 m).
|
||||
- Guardian STAGE bounds report 1×1 m (scene emulation uses ≥1.5 m). OVRPlugin treats that rect as a configured
|
||||
boundary but can't outline it ("ovrpBoundary_Outer is not supported"): Demeter's guardian check indexed an empty
|
||||
array → with `scene_emul` the app's STAGE `xrGetReferenceSpaceBoundsRect` returns XR_SPACE_BOUNDS_UNAVAILABLE.
|
||||
- GL goes through **Zink** (Mesa GL on Vulkan). Mesa GLSL is strict: `#pragma` before `#extension` fails, implicit
|
||||
int/float conversions fail (enable `GL_EXT_shader_implicit_conversions`), num_views=2 shaders on single-view FBOs
|
||||
give GL_INVALID_OPERATION. Some GLES games hit `zink: DEVICE LOST` (Unity MSAA RTT; Sniper Elite VR even without).
|
||||
@@ -553,6 +573,7 @@ Output is deterministic (same input + runtime → same bytes), which is what mak
|
||||
- VrApi-direct engines (CryEngine Climb 2, POTW) need the VrApi bridge; the bridge drops whole frames on unknown layer
|
||||
types (→ black screen with audio).
|
||||
|
||||
**Quest controller mode:** adapter `quest_buttons` (0/1/2, per game + library `input.quest_buttons`) suggests Frame Controller bindings translated from the game's Touch set (`suggest_frame`, mapping docs/input/quest_buttons.json = single source for C table, tests, diagram `scripts/controller_diagram.py`); runtime accepts them headless. → docs/DEVICE_NOTES.md#quest-controller-mode-quest_buttons-2026-10-17
|
||||
**Steam Frame controller models:** adapter setting `controller_models` (`native/adapter/render_model.c`, emulates XR_FB_render_model) + agent `install_controller_models` (Frame's SteamVR models → glb at finalize); no catalog game uses runtime models; Valve's models are never committed or copied off the Frame. → docs/DEVICE_NOTES.md#steam-frame-controller-models
|
||||
**overport's dispatcher (`libopenxr_loader.so`) only forwards functions in its own table** (`overportOXR: Unknown
|
||||
proc addr: …`), so adapter emulations of functions it doesn't know are unreachable. For XR_FB_render_model,
|
||||
@@ -585,6 +606,7 @@ Toy Master doesn't request a model after that (it uses its own), so the glb load
|
||||
**Vader Immortal (GitHub #49):** stuck on its loading card and leaked GPU mappings (solved below: `gl_error_clear`); diagnostics FrameBridge `layer_debug`, `strip_color_bias`, `frame_balance`, `snapshot=N` (`snapshot_gl.c`: saves the submitted left-eye image as `files/fb_snap_*.ppm`). → docs/DEVICE_NOTES.md#vader-immortal-headless-investigation
|
||||
**SteamVR overlay apps:** host OpenVR overlays composite over Lepton games; detection `analysis/vroverlay.py`, patch `pcvr.vr_overlay`, Linux fields `vr_overlay`/`vr_overlay_autostart`, agent `register_vr_overlay`/`unregister_vr_overlay`/`launch_vr_overlay`; SteamVR on the Frame reads only `binary_path_linux_arm`; FramePort's own autostart (`ensure_overlay_service`, `frameport-vr-overlays.service`). See also docs/FRAME_RUNTIME.md "Overlay apps". → docs/DEVICE_NOTES.md#steamvr-overlay-apps
|
||||
**Vader's leak solved / AC Nexus flips:** stale GL errors broke SteamVR's GL interop → adapter `gl_error_clear` (GLES, catalog for Vader ep1-3); read `xrclient_<pkg>.txt` in Steam's logs for runtime errors; the runtime caps swapchains per session (~15) → flip emulation in place; adapter `flip_dump`. → docs/DEVICE_NOTES.md#vaders-leak-solved--ac-nexus-flips
|
||||
**Mixed reality: scanned rooms (branch mr-scene):** agent `room_capture` records Valve's RoomView 3D depth mesh -> `fp_roomscan` -> `room.txt` + `room-mesh.bin` (docs/mr/ROOM_FORMAT.md; refined by default: the owner's cleanup passes + label rules in `native/roomscan/refine.c`/`rules.c`, unrefined output kept as `room-raw.txt` + `room-mesh-raw.bin`, head path/settings/report/labels/door suggestions in `scan.json`, `--refine-only <dir>` re-runs it, `--refine 0` = old output; docs/mr/scan.md "Refinement") in `~/.local/share/frameport/rooms/<universe>/`, copied next to settings.conf (`_room_sync`); FrameBridge (`scene_room.c/scene_meta.c/scene_anchors.c`, all through xrshim) serves walls, volumes, planes, the room mesh, META discovery/persistence, saved anchors and `XR_META_boundary_visibility`; `scene_emul` + `frame.meta_permissions` for every USE_SCENE/USE_ANCHOR_API game (migrations `scene_emul_all`, `scene_permissions`); per-game `scene_hide_bounds` (Demeter). → docs/DEVICE_NOTES.md#mixed-reality-scanned-rooms
|
||||
**Unreal's self-restart:** UE 4.21+ restarts itself after the first shader precompile and Lepton ends the container → launch.sh RESTART_BLOCK starts Lepton once more (`upgrade_launchers`; `FRAMEPORT_NO_RESTART=1` off). → docs/DEVICE_NOTES.md#unreals-self-restart
|
||||
**xrWaitFrame runs ahead:** the runtime sometimes returns a display time <1 refresh after the last submit and rejects the second submission (`VRCompositorError_AlreadySubmitted`) → the VrApi bridge re-waits (`pacing 2s:` counters). → docs/DEVICE_NOTES.md#xrwaitframe-runs-ahead-journey-of-the-gods
|
||||
**Lepton's logcat mirror dies** (EOF, read failure, or silent) → launch.sh `_logcat_keeper` reads `podman exec <container> logcat` itself into launch.log (log `<base>/logcat-keeper.log`). → docs/DEVICE_NOTES.md#leptons-logcat-mirror-dies
|
||||
|
||||
@@ -36,6 +36,9 @@ Frame, patches each game so it runs there, uploads it and adds it to your Steam
|
||||
in FramePort.
|
||||
- **Your Frame from your PC:** Type on Frame (your keyboard on the Frame), Files, Screenshots, Live view (what the
|
||||
headset shows, in your browser) and Monitor (frame rate, temperatures, battery, processes).
|
||||
- **Your room for mixed reality (experimental):** scan the room in the headset, see it in 3D, clean up the scan and
|
||||
fix furniture, screen and door labels, so mixed-reality games get your room. Try it with
|
||||
[Test Quest MR](https://github.com/spoopyghosty0/TestQuestMR).
|
||||
- **Updates itself**, and reports problems without personal data.
|
||||
|
||||
Details for each: [Install and first steps](docs/INSTALL.md).
|
||||
@@ -44,6 +47,8 @@ Details for each: [Install and first steps](docs/INSTALL.md).
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Quick start
|
||||
|
||||
1. [Download](https://github.com/spoopyghosty0/frameport/releases/latest) the build for Windows, macOS (Apple
|
||||
@@ -124,6 +129,7 @@ What FramePort adds: [Architecture](docs/ARCHITECTURE.md).
|
||||
| [Compatibility](docs/COMPATIBILITY.md) | What runs and how well |
|
||||
| [Tested games](docs/GAMES.md) | Tested games and how well they run |
|
||||
| [FAQ](docs/FAQ.md) | Game folders and common questions |
|
||||
| [Controller buttons](docs/CONTROLLERS.md) | Quest Touch buttons on the Frame: SteamVR default, Mirror and Quest pair |
|
||||
| [Porting playbook](docs/PLAYBOOK.md) | Symptoms and fixes per game |
|
||||
| [Steam Frame runtime reference](docs/FRAME_RUNTIME.md) | Facts about the Frame's runtime |
|
||||
| [Architecture](docs/ARCHITECTURE.md) | How the code is organized |
|
||||
|
||||
@@ -8,16 +8,16 @@ d6337034fb0bdcca7ef9b7063d85b7586b8508b63b435959e8fffaba0691091c ./arm64-v8a/li
|
||||
321da502e0f8f46f0880aad39fe6b84bdef88f925164568814cf704a1b31a577 ./arm64-v8a/libfp_ovrp.so
|
||||
40defdaddcda53bd2649eb48076fae1622bfc2bfc88e9e03c94fc8b45af2a4f2 ./arm64-v8a/libfp_ovrtrace.so
|
||||
0167c2fde5dfde62897f9d85afc4fc8e97784e988b5b8df6cea8c407240b3424 ./arm64-v8a/libfp_tkeys.so
|
||||
468b97ee3c36481d939a87425b15880c7a801d16732350e1e2832b385d63f20a ./arm64-v8a/libfp_vk.so
|
||||
e32f9b3222bfe2e58c193db809ac2cf54086df36fd2ec6651aa2e58308f6b39d ./arm64-v8a/libfp_vk.so
|
||||
383054f8b3b41dde76d062c71856cd3163655e50009c08454bc1793f393ead65 ./arm64-v8a/libfpg.so
|
||||
4a13a33e112c9e4e3c5ff4337ed6e988a9e5b3602f48d654df148ef5fafbf426 ./arm64-v8a/libfpglmv.so
|
||||
36400eae4ba3807bcfcd54463daa217347032b07218792faa448d8cbbd086a0e ./arm64-v8a/libframe_xrshim.so
|
||||
19ba5ed3c4b780f5ae46251f6958f552acfc5f13c8bb39f995579593fa8990c6 ./arm64-v8a/libglshim.so
|
||||
2d1726aaa8ee0ce788df1b8727bd55ab72efae749e7652f38f67e5e317b74b2f ./arm64-v8a/libopenxr_loader_generic.so
|
||||
001e699b64062e2cc260915c75aa3508aff10e590125a38c1a3007f61c13c681 ./arm64-v8a/libopenxr_loader_generic.so
|
||||
1feaeafad467c4cafdf2b018a4d84b0bee200c3f711697b4ce97e66a3ba256ca ./arm64-v8a/libovrplatformcompat.so
|
||||
a4ed80ae45ca90758239f152ea5a2e8c127d491e1e697ea5680db6fd7f72db07 ./arm64-v8a/libvrapi.so
|
||||
a4ed80ae45ca90758239f152ea5a2e8c127d491e1e697ea5680db6fd7f72db07 ./arm64-v8a/libvrapi_foveation.so
|
||||
eba4e9ed8563fa0de00d88bb85f960e79f13a6f60eddd437b203c8d3b7d308af ./armeabi-v7a/libopenxr_loader_generic.so
|
||||
b022dc0d48c9ee46e5afa6e169386cdce36d896102fb8677f930abf15408e963 ./armeabi-v7a/libopenxr_loader_generic.so
|
||||
1871eae093432d277da4bc751bf5f3269dfcf11f9168260b0c3caadb0dedb19d ./dex/oculusos-stubs.dex
|
||||
b634ab5640e258563c536e658cad87080553df6f34f62269a21d554844e58bfe ./hevc/COPYING.FFmpeg
|
||||
141de01f0d40b97d4f9a720db5ab8ea442f6aa35db5272048899292884994a1c ./hevc/libstagefrighthw.so
|
||||
@@ -27,6 +27,38 @@ b634ab5640e258563c536e658cad87080553df6f34f62269a21d554844e58bfe ./hevc/COPYING
|
||||
6cda3076a8c09b31bb1f90da614ea89e7ee194589626eeac613e522849604e47 ./licenses/LuaJIT.txt
|
||||
1aa733117cf57ccff7cf23f0425dbfd73b0332a3ef1c905ceeca0ccd0449c4e0 ./linux-arm64/XR_APILAYER_FRAMEPORT_timefix.json
|
||||
28c2430a02bbd8902c5bfb9562c6fd0e318654f9e05c095bfeb94b0450ab1b07 ./linux-arm64/libxr_frameport_timefix.so
|
||||
2cc16b90abcd3d359f1658c581172bd650017bf2725e24268915f50904c000ec ./linux-arm64-bin/fp_roomscan
|
||||
b10b3a5c10c3339fc63fd9f6cb4d01ea29be11a863d0ebad1389ea69fd471940 ./linux-arm64-bin/fp_venc
|
||||
00c2dead0fd7497929833467f0e55699d57d26be1b85dce35402993852f7b111 ./roomscan/ceiling.png
|
||||
c1962de31ed2810d31c760cf9ee63316ac2b8c9528ccbcd9299ebf5ba39b2995 ./roomscan/done.png
|
||||
0dfaea4c68979b0a45a9fef06fdf1a92468ffc8f46f80a3a014fce1d805972d5 ./roomscan/failed.png
|
||||
f2bae84f1d1512f87b25e688acdddb29b3f360f3f0d0cd4fdeeb741a79e7b19a ./roomscan/finish_00.png
|
||||
9be7720c45f162e99813cb9a7a03e026fed53b41f06637b30ded309eeac91759 ./roomscan/finish_01.png
|
||||
220d699f879b3a8669cfaf4dc714496e8c1db7e7878e7cd12b1a41aec9b62faa ./roomscan/finish_02.png
|
||||
42871b440b77449d4fe41be1c809a930148a874caa8bc92b13b5d25bb6effa83 ./roomscan/finish_03.png
|
||||
a470f62624c94ed6791cc7ee03066e1ffb0a676d520658a0a8b02a6af1605b3b ./roomscan/finish_04.png
|
||||
195ff1c649fd9b42bfa6eea29c43a8b67116fd6ffe9b0fe53dcbdb1074bd34fa ./roomscan/finish_05.png
|
||||
a45accab86b02d7b78327a17b84a6fea543ecc549282055dd3182c686fc2b1b8 ./roomscan/finish_06.png
|
||||
b88d6c590ea839a9d11b3a4de8c3effb39752f7114318966fddae09995e0fd37 ./roomscan/finish_07.png
|
||||
4a3615db26cf765b855a997be3e067aed58464894dab4a6700fdec5c2b854501 ./roomscan/finish_08.png
|
||||
f532a83044e707c66dbc74787ce7b844890bc05829d3e9c8f7e33dae9db246a5 ./roomscan/finish_09.png
|
||||
ffce61a9f13bbb250fad3582396a5bec82a1bad1d585b7ef9b8f65d343806b26 ./roomscan/finish_10.png
|
||||
4a63772adacb31004749ab04b592076bd36cda61eef52f59037d5abdc9e527db ./roomscan/finish_11.png
|
||||
f373e7021cfce0d1078227f473425a10035ee426102b78f13e0fea4833b736d5 ./roomscan/finish_12.png
|
||||
1147391822be3f1aa5351374b8fd9ec85f8b5ce40e3cb9f3146d778e6fbc652a ./roomscan/floor.png
|
||||
eb3c8b55a9dbafa030797ca9f06a22c3ee3bbef96cb7adea26d433ebad43d43f ./roomscan/look_00.png
|
||||
6ba965ff6d22a469c558f2a68229bd2ff48fc28badb95889ca245310d1dd3e96 ./roomscan/look_01.png
|
||||
0a11cec25ebc76fe11d505d1f2ec162c9b4a6754393065800e73b1f81e2d94c8 ./roomscan/look_02.png
|
||||
fa5a4c94d20af56797b9fcc111148767847a6633ade9217cab373de232efad90 ./roomscan/look_03.png
|
||||
e80fa493e380fba39084be2ee62859adcff4e39aa66566abee10a4d6339c52e9 ./roomscan/look_04.png
|
||||
133617f0d8d1e69c37408aac80033f71856b592b62e3b780cc12b446bd974680 ./roomscan/look_05.png
|
||||
d5dd296ff23641c6973c57ac139147e99c7b783316ab60537adb2155dd196880 ./roomscan/look_06.png
|
||||
c08570aa78f966ae937b2e21d0bad887c5352fbca56aa579a2a7ef8541fb96ad ./roomscan/look_07.png
|
||||
3767e7db5fb6babc872fa62c42604f749c65936717d332101745b201cd450cd5 ./roomscan/look_08.png
|
||||
2924dc65e5c497545340209edc16060e12e1834d11af6871d1c1baa50b7b2562 ./roomscan/look_09.png
|
||||
cd0150dfc10b2aea39403c74eff72fd05fb0111178ef39b4c98657a6d16d105e ./roomscan/look_10.png
|
||||
011355e412f73ade3896e9037490e607c4365d0cf8ceb807d236bf68b414d3ed ./roomscan/look_11.png
|
||||
1b8b47ffb47568d540921f065b5958eeb44a6489498d58e0f33f5d1f535154e2 ./roomscan/look_12.png
|
||||
926c7bc6a103adee5b9bf938f5d4e39095ab77fd6270b5301be3b0428edd9ae6 ./roomscan/start.png
|
||||
c2c70f4af1a5c3e6089dff1130161dd417f8044f99ec639eeecf1083930aab16 ./win-x64/fp_oculushmd.exe
|
||||
60ebc5fea05b8945082e2ca8b9f2eb74616012a26eb50133af273dff0fcddc42 ./win-x64/fp_vrsettings.exe
|
||||
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 26 KiB |
@@ -1,9 +1,9 @@
|
||||
package: ScaredBySquares.Game
|
||||
title: Scared By Squares
|
||||
status: unknown
|
||||
status: works
|
||||
notes: 'Showed black and dropped back to the home environment while the game kept running (sound only, GitHub 323).
|
||||
Fixed in FramePort 1.2.1 and checked on the Frame without the headset (the game runs at 72 fps and SteamVR keeps it
|
||||
as the running game); not yet confirmed in the headset. Rebuild and reinstall the game after updating FramePort.'
|
||||
Fixed in FramePort 1.2.1 (confirmed in the headset by the reporter). Rebuild and reinstall the game after updating
|
||||
FramePort.'
|
||||
details: 'The game (Unreal 4.27, OpenXR) creates two controller actions named "turn" (an action mapping and an axis
|
||||
mapping called Turn). The Frame''s runtime accepts the duplicate, then fails to load the input manifest it builds
|
||||
from the actions ("SetXrInputConfig failed. Failed to load manifest") and treats the game as closed. FrameBridge''s
|
||||
@@ -14,6 +14,9 @@ xr: OpenXR
|
||||
frame:
|
||||
- frame.hw_video_decode
|
||||
- frame.unreal_skip_obb_check
|
||||
updated: '2026-10-16'
|
||||
verified:
|
||||
date: '2026-10-17'
|
||||
issue: 323
|
||||
updated: '2026-10-17'
|
||||
min_app: 1.2.1
|
||||
source_hint: Scared By Squares
|
||||
@@ -0,0 +1,16 @@
|
||||
package: com.And7Limited.Hitstream
|
||||
title: Hitstream
|
||||
status: works
|
||||
tested_version: 2.06bQuest
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
adapter:
|
||||
controller_models: 1
|
||||
haptic_fix: 1
|
||||
notes: Fitness rhythm game; uses controller models and haptics fix.
|
||||
verified:
|
||||
date: '2026-10-12'
|
||||
known_good_sha256: ecc9e2eee79073ae97ad5d3f70c21e8f049f899f21b51dfd541430bb7dd04cac
|
||||
source_hint: Hitstream
|
||||
@@ -0,0 +1,23 @@
|
||||
package: com.Appnori.SummerSports
|
||||
title: Summer Sports
|
||||
status: works
|
||||
tested_version: 0.5.1
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
- frame.unity_runtime_msaa_off
|
||||
- frame.unity_gl_shim
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-17'
|
||||
known_good_sha256: 582adf5a538eb9565921cc0384e61bf113ab9215c1ad462fade82d06ad3302fa
|
||||
app: 1.2.1
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 96
|
||||
issue: 395
|
||||
source_hint: All in One summer Sports VR
|
||||
@@ -0,0 +1,23 @@
|
||||
package: com.BolverkGamesFlat2VR.SurvivingMarsPioneer
|
||||
title: 'Surviving Mars: Pioneer'
|
||||
status: works
|
||||
tested_version: 1.0.14
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
- frame.hw_video_decode
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
scale: 2.0
|
||||
verified:
|
||||
date: '2026-10-18'
|
||||
known_good_sha256: 3a9ff281417c219736ee10969e6ce4f7d8395472cb60718db8839d65edac4777
|
||||
app: 1.2.1
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 96
|
||||
issue: 399
|
||||
source_hint: Surviving Mars Pioneer
|
||||
@@ -0,0 +1,24 @@
|
||||
package: com.CarbonStudio.TheWizards
|
||||
title: The Wizards
|
||||
status: works
|
||||
tested_version: '1.11'
|
||||
engine: Unreal
|
||||
xr: VrApi
|
||||
alt_overport:
|
||||
- patch_remove_unreal_force_quit
|
||||
use_alt: true
|
||||
frame:
|
||||
- frame.unreal_thumb_touch
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
pose_time_fix: 1
|
||||
scale: 2.0
|
||||
verified:
|
||||
date: '2026-10-18'
|
||||
known_good_sha256: c1d979d746c2be941e150e948aec755d85034de829abfe30e5bf0761a49135ef
|
||||
app: 1.2.1
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 96
|
||||
issue: 397
|
||||
source_hint: the wizards
|
||||
@@ -0,0 +1,25 @@
|
||||
package: com.CortopiaStudios.Spectre
|
||||
title: Spectre
|
||||
status: works
|
||||
notes: Works since 1.2.1 (the loading screen waited for a Meta analytics request).
|
||||
tested_version: 1.0.1.1354
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
- frame.hw_video_decode
|
||||
- frame.cortopia_org_id
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
scale: 2.0
|
||||
verified:
|
||||
date: '2026-10-17'
|
||||
known_good_sha256: a37a5400ef58906b575f6da1649b62ac7ce024eab831fae105457c1d5d12c2de
|
||||
app: 1.2.1
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 96
|
||||
issue: 390
|
||||
source_hint: Wonder
|
||||
@@ -2,7 +2,8 @@ package: com.CyDream.BattleTalent
|
||||
title: BattleTalent
|
||||
status: issues
|
||||
notes: "Plays normally. The mixed-reality mode doesn't appear in the start menu (also with an APK from a Quest 3); MR
|
||||
progress in #355. Multiplayer not tested yet."
|
||||
progress in #355. LAN multiplayer works; online rooms need Meta's account check (user proof), so online
|
||||
multiplayer can't work (#408)."
|
||||
tested_version: '4.7'
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package: com.GexagonVR.HardBullet
|
||||
title: HardBullet
|
||||
status: issues
|
||||
notes: Loading screens (a cube overlay) are missing or corrupted up to FramePort 1.2.1; cube_quads (next release)
|
||||
shows them as six panels, not confirmed in the headset yet. Level loads stall frame submission for up to ~1 s, so
|
||||
the view freezes during loads.
|
||||
tested_version: 1.0.9.704Q
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
- frame.hw_video_decode
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-17'
|
||||
known_good_sha256: ecbb758d98083a590f100d5ac6efe9490ef74ad39b59217e0b4a11db920ecd51
|
||||
app: 1.2.1
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261015.6141207'
|
||||
agent: 96
|
||||
issue: 385
|
||||
source_hint: HARD BULLET
|
||||
@@ -0,0 +1,20 @@
|
||||
package: com.Meta4.TFBeyondReality
|
||||
title: 'Transformers Beyond Reality: Redux'
|
||||
status: unknown
|
||||
notes: Starts and runs at 72 fps in headless launch tests (2026-10-17); not yet checked in the headset. Steam's shader
|
||||
cache layer is left out and a few shaders the Frame's driver refuses are skipped, so some objects may be missing.
|
||||
details: Unreal Engine game (Binned3 allocator, GitHub issue 349). Crash 1 - the allocator's 76 GiB up-front
|
||||
reservation is refused by the Frame's kernel (frame.unreal_noreserve, suggested by analysis). Crash 2 - Unreal's
|
||||
"Shader compilation failures are Fatal." With Lepton's Fossilize layer loaded every graphics pipeline failed with
|
||||
VK_ERROR_OUT_OF_HOST_MEMORY, even a constant-colour one (not the pipeline cache, not the CPU allocator, not Valve's
|
||||
FDM/RPO layers); vk_no_fossilize takes the layer out of the game's instance. A few pipelines with the game's
|
||||
multiview vertex shaders then still fail in Turnip; vk_pipeline_fallback creates them with blank shaders.
|
||||
tested_version: 1.495.34291
|
||||
engine: Unreal
|
||||
xr: OpenXR
|
||||
adapter:
|
||||
vk_no_fossilize: 1
|
||||
vk_pipeline_fallback: 1
|
||||
verified:
|
||||
date: '2026-10-17'
|
||||
issue: 349
|
||||
@@ -12,6 +12,7 @@ overport_extra:
|
||||
- patch_force_passthrough
|
||||
adapter:
|
||||
scene_emul: 1
|
||||
scene_hide_bounds: 1
|
||||
device:
|
||||
- device.text_input_window
|
||||
verified:
|
||||
@@ -22,4 +23,4 @@ verified:
|
||||
frame:
|
||||
- frame.meta_permissions
|
||||
- frame.unity_text_input
|
||||
updated: '2026-10-15'
|
||||
updated: '2026-10-17'
|
||||
@@ -0,0 +1,21 @@
|
||||
package: com.WenklyStudio.SurvivalNation
|
||||
title: Survival Nation
|
||||
status: works
|
||||
tested_version: 1.14.1
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
scale: 1.8
|
||||
verified:
|
||||
date: '2026-10-18'
|
||||
known_good_sha256: 7903dfee1c9c36d19e1c6c78a1f0ede1f0b25ce6a6065811997297e701009250
|
||||
app: 1.2.1
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 96
|
||||
issue: 401
|
||||
source_hint: survival nation
|
||||
@@ -0,0 +1,24 @@
|
||||
package: com.cosmorama.tabletroopers
|
||||
title: Table Troopers
|
||||
status: works
|
||||
tested_version: 2026.40.4
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.gms_platform_version
|
||||
- frame.photon_voice_audio_route
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
scale: 2.0
|
||||
verified:
|
||||
date: '2026-10-17'
|
||||
known_good_sha256: b0a4b0e29855277712b580bd5507721fff137bde77b1911b34cca89c627feef1
|
||||
app: 1.2.1
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 96
|
||||
issue: 391
|
||||
source_hint: Troopers
|
||||
@@ -0,0 +1,22 @@
|
||||
package: com.el.blackholepooldemo
|
||||
title: Black Hole Pool
|
||||
status: works
|
||||
tested_version: 5.3.2
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.vivox_audio_route
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-17'
|
||||
known_good_sha256: 17c23df3813324250ec1792d8dc0488dadf0df0a7ccbe33157d88f427889a92e
|
||||
app: 1.2.1
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 96
|
||||
issue: 396
|
||||
source_hint: Black Hole Pool
|
||||
@@ -5,8 +5,10 @@ source_hint: NOPE CHALLENGE
|
||||
engine: Unreal
|
||||
xr: OpenXR
|
||||
status: works
|
||||
notes: Meta XR Audio telemetry patch + nodebug. MR "Reality Breach" mode unavailable.
|
||||
details: The MR 'Reality Breach' mode needs passthrough; standard VR mode used.
|
||||
notes: "Meta XR Audio telemetry patch + nodebug. MR mode: needs a scanned room."
|
||||
details: The MR 'Reality Breach' mode needs passthrough and a room; standard VR mode was verified.
|
||||
adapter:
|
||||
scene_emul: 1
|
||||
alt_overport:
|
||||
- patch_remove_unreal_force_quit
|
||||
frame:
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package: com.mrf.tvb.cubism
|
||||
title: cubism
|
||||
status: works
|
||||
notes: works and passthrough play works also
|
||||
tested_version: 1.8.0
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
scene_emul: 1
|
||||
verified:
|
||||
date: '2026-10-17'
|
||||
known_good_sha256: fd1ba8eae19efe0f2a17fc146847de677263125f9ec5437cea5c632f0cd88dcf
|
||||
app: 1.2.0
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 91
|
||||
issue: 403
|
||||
source_hint: Cubism
|
||||
@@ -0,0 +1,19 @@
|
||||
package: com.mrf.tvb.laser_dance
|
||||
title: Laser Dance
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
status: works
|
||||
notes: Mixed reality laser obstacle course; requires spatial data and room layout emulation (scene_emul=1). frame.meta_depth_occlusion keeps close lasers visible (its asset fixes only for the build they were made for); tested version not recorded (PR #246).
|
||||
details: Mixed reality game that projects lasers across your room; needs Meta scene emulation and spatial data permission.
|
||||
adapter:
|
||||
controller_fix: 1
|
||||
haptic_fix: 1
|
||||
scene_emul: 1
|
||||
passthrough_emul: 1
|
||||
frame:
|
||||
- frame.meta_permissions
|
||||
- frame.meta_depth_occlusion
|
||||
- frame.unity_scene_active_rooms
|
||||
- frame.unity_text_input
|
||||
min_app: 1.2.2
|
||||
updated: '2026-10-18'
|
||||
@@ -0,0 +1,12 @@
|
||||
package: com.rrrgames.ThiefSimVRQuest
|
||||
title: Thief Simulator
|
||||
status: unsupported
|
||||
notes: Quits at start with the game's own "installed from unknown source" message. Lepton installs the game itself at
|
||||
every start (there is no Meta store app on the Frame), so the game never sees the Meta store as its source.
|
||||
tested_version: 1.1.7.3
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
verified:
|
||||
date: '2026-10-17'
|
||||
issue: 368
|
||||
updated: '2026-10-17'
|
||||
@@ -0,0 +1,26 @@
|
||||
package: com.team17.killerfrequency
|
||||
title: KillerFrequency
|
||||
status: works
|
||||
tested_version: 1.0.767.38513
|
||||
engine: Unity
|
||||
xr: VrApi
|
||||
frame:
|
||||
- frame.unity_no_msaa
|
||||
- frame.unity_text_input
|
||||
- frame.unity_runtime_msaa_off
|
||||
- frame.hw_video_decode
|
||||
- frame.unity_gl_shim
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
scale: 1.4000000000000001
|
||||
verified:
|
||||
date: '2026-10-18'
|
||||
known_good_sha256: a11b008fcc0f5bc7aa5a3bb04d4267144d5ec60b26529a9b20e6499e12dcf079
|
||||
app: 1.2.1
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 96
|
||||
issue: 402
|
||||
source_hint: killer freq
|
||||
@@ -1,13 +1,14 @@
|
||||
package: com.zenstudios.PFXVRQuest
|
||||
title: Pinball FX VR
|
||||
status: issues
|
||||
notes: Plays; mixed reality mode not working yet. Space warp off (fewer stutters).
|
||||
notes: "Plays; space warp off (fewer stutters). MR mode: needs a scanned room."
|
||||
tested_version: '1.9'
|
||||
engine: Unreal
|
||||
xr: OpenXR
|
||||
alt_overport:
|
||||
- patch_remove_unreal_force_quit
|
||||
adapter:
|
||||
scene_emul: 1
|
||||
equirect_emul: 1
|
||||
hide_space_warp: 1
|
||||
verified:
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package: io.github.spoopyghosty0.testquestcontrollers
|
||||
title: Test Quest Controllers
|
||||
status: works
|
||||
notes: A controller tester for the Controller buttons settings. Picking a controller type on the Frame closes the
|
||||
app; start it again to use that type (SteamVR's runtime can't switch within one app start).
|
||||
tested_version: 0.2.2
|
||||
engine: Other
|
||||
xr: OpenXR
|
||||
verified:
|
||||
date: '2026-10-18'
|
||||
known_good_sha256: 7db99515fb1a9c9796ee4613f716c0b76caa72c68003db8fc6c14bf20a8eecce
|
||||
app: 1.2.1
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261015.6141207'
|
||||
agent: 102
|
||||
source_hint: Test Quest Controllers
|
||||
@@ -0,0 +1,20 @@
|
||||
package: io.github.spoopyghosty0.testquestmr
|
||||
title: Test Quest MR
|
||||
status: works
|
||||
notes: An MR debug viewer that shows what the Meta MR APIs return (room, planes, mesh, anchors, play area). With
|
||||
FramePort's emulated room it shows a room made from the play area.
|
||||
tested_version: 0.2.1
|
||||
engine: Other
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.meta_permissions
|
||||
adapter:
|
||||
scene_emul: 1
|
||||
verified:
|
||||
date: '2026-10-18'
|
||||
known_good_sha256: 98936ca80d548a753a6645ab89610af19f6e8c3190b5f5f67854cf854691fba5
|
||||
app: 1.2.1
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261015.6141207'
|
||||
agent: 102
|
||||
source_hint: Test Quest MR
|
||||
@@ -0,0 +1,21 @@
|
||||
package: jp.strippers.punchfit
|
||||
title: Punch Fit
|
||||
status: works
|
||||
tested_version: 1.7.2
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.meta_permissions
|
||||
- frame.unity_text_input
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
pose_time_fix: 1
|
||||
scene_emul: 1
|
||||
passthrough_emul: 1
|
||||
notes: Mixed reality workout arena; composition layer trimming prevents 21+ layer overflow.
|
||||
verified:
|
||||
date: '2026-10-12'
|
||||
known_good_sha256: 1a0050272526360542d62e7e6d1fc2daecb765c34378a6fe4ad51568ec550c0d
|
||||
source_hint: Punch Fit
|
||||
min_app: 1.2.2
|
||||
updated: '2026-10-18'
|
||||
@@ -0,0 +1,15 @@
|
||||
package: org.primedgun.v2
|
||||
title: PrimedGun v2
|
||||
status: unknown
|
||||
notes: 'Without the Vulkan library rename: the game builds the library name from two pieces, so the renamed name didn''t
|
||||
exist ("libfp_vkn.so"), its renderer fell back to OpenGL and VR couldn''t start (GitHub #254). Not yet confirmed in
|
||||
the headset.'
|
||||
tested_version: 2.0.0-alpha.4
|
||||
engine: Other
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.sdl_clipboard
|
||||
frame_remove:
|
||||
- frame.vk_sanitize
|
||||
updated: '2026-10-18'
|
||||
source_hint: PrimedGun v2 Quest APK (github.com/iChris4/MetroidPrimePortVR)
|
||||
@@ -6,6 +6,18 @@
|
||||
# with diagnostics. Launch tests and real play sessions (agent session_log, `frameport session`) are both matched.
|
||||
# Sources: every failure we hit porting 34 games to the Steam Frame (see docs/PLAYBOOK.md).
|
||||
signatures:
|
||||
- id: mr-no-room
|
||||
pattern: 'FrameBridge[^\r\n]*scene: (?:capture requested -> (?:guardian box|using guardian room)|room from guardian|room [^\r\n]*(?:from guardian|configured/default size|box))'
|
||||
severity: info
|
||||
diagnosis: 'The game is using a simple fallback room. Scan your room for mixed reality, or use Frame > Room > Use my play area for walls without furniture. A recorded scan can be imported until headset capture is available.'
|
||||
suggest: []
|
||||
- id: mr-discovery-unsupported
|
||||
pattern: '(?:Unknown proc addr:\s*xrDiscoverSpacesMETA|(?:MRUK[^\r\n]*)?NoRoomsFound)'
|
||||
# discovery reached FrameBridge and found the room (OVRPort's "Unknown proc addr" comes before the shim takes over)
|
||||
unless: 'FrameBridge[^\r\n]*scene: discovery request=\d+ results=[1-9]'
|
||||
severity: warning
|
||||
diagnosis: 'The game cannot discover a Meta room. Enable room layout (adapter.scene_emul), rebuild and reinstall for the updated extension shim, then save a room under Frame > Room.'
|
||||
suggest: [adapter.scene_emul]
|
||||
- id: quest2-headset-identity
|
||||
pattern: 'venderId[^\r\n]*systemName Oculus Quest2'
|
||||
severity: info
|
||||
@@ -313,6 +325,16 @@ signatures:
|
||||
diagnosis: The game needs Meta's colour-space extension, which the Frame's runtime doesn't offer, and quits before
|
||||
it starts VR (for example OutRun Quest VR). FrameBridge can offer it.
|
||||
suggest: [adapter.fb_color_space]
|
||||
- id: vulkan-pipeline-refused
|
||||
# GitHub #349: with Lepton's Fossilize layer loaded every Transformers pipeline failed with -1; without it a few
|
||||
# multiview vertex shaders still fail in Turnip. Unreal ends at the first one.
|
||||
pattern: '(Shader compilation failures are Fatal|vk shim: vkCreateGraphicsPipelines failed: -\d+)'
|
||||
severity: fatal
|
||||
diagnosis: The Frame's graphics driver refused to build some of the game's shaders, and Unreal ends the game at the
|
||||
first one (for example Transformers Beyond Reality). Try leaving out Steam's shader cache layer, and skipping the
|
||||
shaders the driver still refuses (they then draw nothing).
|
||||
suggest: [adapter.vk_no_fossilize, adapter.vk_pipeline_fallback]
|
||||
supersedes: [native-crash, force-quit]
|
||||
- id: graphics-requirements-missing
|
||||
pattern: '(XR_ERROR_GRAPHICS_REQUIREMENTS_CALL_MISSING|Failed to create XR session: -50\b|xrCreateSession[^\n]*-50\b)'
|
||||
severity: fatal
|
||||
@@ -373,8 +395,12 @@ signatures:
|
||||
diagnosis: Frame controller models are on, but the converted models are missing from the game's files. The agent found no Frame controller render models in SteamVR at install time; see the install log, or run the agent's controller_models command.
|
||||
suggest: []
|
||||
- id: scene-missing
|
||||
# not OVRPlugin's routine "Unavailable OpenXR extension: XR_FB_scene" (printed by every OVRPlugin game)
|
||||
pattern: '(?<!Unavailable OpenXR extension: )XR_FB_scene|XR_FB_spatial_entity.*(not supported|UNSUPPORTED)|xrQuerySpacesFB'
|
||||
# not extension listings, which every Meta XR game prints: OVRPlugin's "Unavailable OpenXR extension:" and
|
||||
# "Name=XR_FB_scene SpecVersion=1", OVRPort's "overportOXR: - XR_FB_scene 1", Unity's 'Meta XR Feature: ...
|
||||
# Extensions="..."' (GitHub #404); XR_FB_scene_capture alone doesn't count either
|
||||
pattern: '(?m)^(?!.*(?:Unavailable OpenXR extension: |Name=|overportOXR: - |Extensions=")).*XR_FB_scene\b|XR_FB_spatial_entity.*(not supported|UNSUPPORTED)|xrQuerySpacesFB'
|
||||
# FrameBridge serves the room (OVRPort still lists XR_FB_scene and the shim names xrQuerySpacesFB): no finding
|
||||
unless: 'FrameBridge[^\r\n]*scene: emulation active'
|
||||
severity: warning
|
||||
diagnosis: The game wants Meta's room data (scene API), which the Frame lacks. The adapter can emulate a room from the guardian bounds.
|
||||
suggest: [adapter.scene_emul]
|
||||
@@ -523,6 +549,12 @@ signatures:
|
||||
diagnosis: The game crashed while leaving VR, inside Valve's foveation layer when it destroys a swapchain (for example Lambda1VR). This can happen after manually quitting; it is not a startup crash and does not explain distorted geometry seen before quitting.
|
||||
suggest: []
|
||||
supersedes: [native-crash]
|
||||
- id: batman-audio-geometry-allocation
|
||||
pattern: '(?s)\A(?=.*com\.camouflaj\.manta)(?=.*Trying to allocate: [1-9][0-9]{10,}B)(?=.*MetaXRAudioUnityMemoryAllocator\.cpp)'
|
||||
severity: fatal
|
||||
diagnosis: "Batman's Meta XR Audio geometry loader requested tens of gigabytes while loading a scene. Its background task can outlive the UnityWebRequest that owns the input buffer. Update Batman on Frame with the current FramePort: the adapter retains a private copy until parsing finishes, for the verified Batman build. This is a game allocation crash, not evidence of a headset thermal shutdown."
|
||||
suggest: [frame.adapter]
|
||||
supersedes: [native-crash]
|
||||
- id: native-crash
|
||||
pattern: 'Fatal signal (\d+) \(SIG[A-Z]+\)'
|
||||
severity: fatal
|
||||
@@ -543,6 +575,12 @@ signatures:
|
||||
severity: fatal
|
||||
diagnosis: 'Games can''t start until the Frame restarts: it ran out of kernel keys. Rootless podman leaks one session keyring per container start; the FramePort agent sets keyring=false in ~/.config/containers/containers.conf, and a reboot frees the leaked keys.'
|
||||
suggest: []
|
||||
- id: app-separate-process
|
||||
pattern: 'Start proc \d+:[\w.]+:\w+/\S+ for (pre-)?top-activity'
|
||||
severity: error
|
||||
diagnosis: The game's activity runs in an Android process of its own, while Lepton watches only the app's main process, which never starts. About a minute in, Lepton decides the app "did not start" and closes it, wherever the game is. Newer FramePort versions run the activity in the main process (patch "Start the game in the app's main process"); update FramePort and use Update on Frame.
|
||||
suggest: []
|
||||
supersedes: [app-not-started, force-quit]
|
||||
- id: app-not-started
|
||||
pattern: 'Target app \(.*\) did not start'
|
||||
severity: error
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
# Batman audio geometry input lifetime
|
||||
|
||||
Batman: Arkham Shadow 1.4.1 could abort while loading a save or streaming the area
|
||||
around a real-time cutscene. Unity reported requests for 26,388,238,103 or
|
||||
147,520,448,640 bytes through `MetaXRAudioUnityMemoryAllocator.cpp`, followed by
|
||||
`SIGTRAP` on an IL2CPP worker. The first captured stack reached
|
||||
`GeometricAudioMeshPreprocessor::readMeshInfo` through
|
||||
`ovrAudio_AudioGeometryReadMeshMemory`.
|
||||
|
||||
The serialized asset on disk was intact. In one failed run its vertex count was
|
||||
read as 1 instead of 17,251. The managed-code trace showed a lifetime risk:
|
||||
`LoadGeometryAsync` obtains a `NativeArray<byte>.ReadOnly` from a
|
||||
`UnityWebRequest` download handler, calls async-void `LoadGeometryFromMemory`,
|
||||
then returns. The background task retains the pointer/length pair, but not the
|
||||
request owning those bytes. Its `GC.KeepAlive` boxes the read-only view rather
|
||||
than retaining the request. The trace and device test support premature source
|
||||
reuse as the cause; no hardware watch established the exact freeing instruction.
|
||||
|
||||
## Repair and isolation
|
||||
|
||||
`audio_geometry_owned.h` copies the input synchronously at
|
||||
`LoadGeometryFromMemory`, before `Task.Run` can outlive the request. A mutex
|
||||
protects the list of copies. The original worker still parses and processes the
|
||||
audio geometry; its completion wrapper removes and frees the corresponding copy.
|
||||
This also handles a worker finishing before the queuing call returns. NativeArray
|
||||
length/padding bits and IL2CPP's hidden method-info arguments are preserved.
|
||||
Null/invalid lengths and allocation failure retain the original call. A task
|
||||
that never runs retains its copy until process exit; there is no timed expiry
|
||||
that could free data beneath a late worker.
|
||||
|
||||
`batman_audio_geometry.h` enables the repair only in `com.camouflaj.manta`, on
|
||||
AArch64, after the existing audio metadata initializer verifies Meta XR Audio
|
||||
Wwise build `e1619e7fb839badf0ea5ecc22d9f028d0b17c34d`. It additionally requires
|
||||
IL2CPP build `1d48a41d7e9011bef5fae0c23c404c62a4e7a73c` and exact 16-byte entry
|
||||
sequences at both managed hooks. Unknown builds are left untouched. The live
|
||||
IL2CPP addresses are `0x2e61bb4` (queue) and `0x2e63a28` (worker); text file
|
||||
offsets in this build are 0x1000 lower. These offsets must not be reused for
|
||||
another game or build.
|
||||
|
||||
Both geometry hooks and the metadata hooks use `audio_detour.h`. After the
|
||||
16-byte entry guard, only the first, naturally aligned instruction is replaced:
|
||||
a release atomic store publishes an AArch64 `B` to a nearby stub. The stub's
|
||||
absolute jump and the trampoline (one verified non-PC-relative instruction,
|
||||
then a direct branch to entry+4) are complete, instruction-cache flushed and RX
|
||||
before publication. The original-call pointer is published first. Remaining
|
||||
entry instructions are unchanged, so workers already past the entry can finish
|
||||
normally. `/proc/self/maps` supplies free gaps, clipped to the ranges of both
|
||||
branches. Page-aligned candidates are tried outward from the target (lower
|
||||
address first on equal distances) with `MAP_FIXED_NOREPLACE`; EEXIST/EINVAL
|
||||
advance to the next candidate. Existing mappings are never replaced, and a
|
||||
kernel that ignores the flag and relocates a mapping has that result unmapped.
|
||||
If no candidate works, that hook is skipped without changing its entry. No ELF
|
||||
page-slack fallback is used: zero bytes alone cannot establish that padding is
|
||||
outside all section content, and the loaded program headers do not prove this.
|
||||
|
||||
Every mprotect result is checked. Failures before entry publication free the
|
||||
mapping. If restoring RX after publication fails, the entry word is restored,
|
||||
its instruction cache is flushed, and restoring RX is attempted again. The
|
||||
mapping and original-call pointer must then remain valid until process exit:
|
||||
a worker may already have followed the published branch. Freeing them without
|
||||
quiescing those threads would introduce a use-after-unmap. A persistent kernel
|
||||
mprotect failure can leave the entry page RWX. Each hook failure logs its target
|
||||
and failing operation once; metadata installation is attempted once per verified
|
||||
library. Allocation failure reports the closest eligible free page, last
|
||||
attempted address, actual mmap result, errno and attempt count. Null closest and
|
||||
zero attempts mean no eligible free gap. Protection failures identify stub RX,
|
||||
entry RWX, or entry RX, including the rollback RX errno (zero means it succeeded).
|
||||
This exceptional rollback cannot provide immediate reclamation or guarantee RX
|
||||
when the kernel refuses it. The previous hint-based publication build failed installation on the Steam
|
||||
Frame; this gap-search replacement has host validation only.
|
||||
|
||||
This is separate from the existing [audio metadata retirement repair](AUDIO_METADATA.md).
|
||||
It does not change the decoder, acoustic processing, tracking, foveation,
|
||||
resolution, refresh rate, or other games' recipes. Temporary input dumps, native
|
||||
parser bypasses and redundant late copies used during investigation are not
|
||||
included. Production logs are limited to installation and the first four
|
||||
copy/completion pairs. No game assets are included in the repository.
|
||||
|
||||
The `frame.adapter` package revision increases only for Batman. Existing Batman
|
||||
installs therefore offer **Update on Frame**; other games are not marked outdated
|
||||
by this repair. Updating/rebuilding Batman installs the new adapter through the
|
||||
normal path; no launcher script or additional option is needed.
|
||||
|
||||
## Validation
|
||||
|
||||
- Native regressions cover deferred and inline completion, source destruction
|
||||
before worker execution, concurrent loads, cleanup, allocation failure,
|
||||
argument preservation, and rejection of unknown build IDs/function entries.
|
||||
Host C tests also cover branch-range endpoints, stub/trampoline encoding,
|
||||
publication order, synthetic maps with in-range/out-of-range/absent gaps and
|
||||
adjacent mappings, outward placement order, EEXIST/EINVAL fallback, rejected
|
||||
relocated mmap results and mprotect failure cleanup/rollback.
|
||||
- On the headset, the affected save loaded successfully with the repair. Logs
|
||||
showed private copies of 2,599,300, 159,020 and 628,088 bytes made before dispatch;
|
||||
each original parser completed and each copy was released. Another 180,120-byte
|
||||
geometry input also completed normally.
|
||||
- The game continued for over seven minutes after that load with zero allocation
|
||||
failures, rejected meshes or fatal signals. Acoustic geometry was loaded, not
|
||||
disabled. That device test used the ownership repair with diagnostic guards;
|
||||
none of the guards rejected a mesh. The production repair omits those guards.
|
||||
- The rebuilt latest adapter passes the repository checks recorded in the PR;
|
||||
the device result above is for the experimental build tested before integration.
|
||||
|
||||
This does not establish long-term stability or resolve the separately observed
|
||||
whole-headset power-offs. Those require their own power/reset investigation.
|
||||
@@ -0,0 +1,93 @@
|
||||
# Controller buttons
|
||||
|
||||
> **Experimental.** The Frame's runtime accepts these bindings, but the modes haven't been tried in the headset on
|
||||
> many games yet. If buttons stop working in a game, set it back to **SteamVR default** (Game settings →
|
||||
> Controllers) and report it.
|
||||
|
||||
Quest games usually bind Quest Touch controllers. Steam Frame has a d-pad and View button on the left controller,
|
||||
and an A/B/X/Y diamond and Menu button on the right. Without a game's own Frame bindings, SteamVR maps Touch X/A
|
||||
to the bottom button and Touch Y/B to all three top buttons. View sends Touch Menu. The right X and Y therefore
|
||||
send Touch B, rather than the left-hand X and Y of a Quest controller.
|
||||
|
||||
**Controller buttons** chooses an alternative for Quest games running through FrameBridge:
|
||||
|
||||
- **SteamVR default (0)** leaves SteamVR's Touch remapping in place. This is the initial default.
|
||||
- **Mirror (1)** uses every face button. On each controller, bottom and outer send X/A; top and inner send Y/B, as on Quest controllers, whose second button sits up and toward the other hand.
|
||||
- **Quest pair (2)** uses two face buttons per controller, placed like Touch's: left X = d-pad down, Y = d-pad
|
||||
right; right A = A, B = X. The other face buttons send no Touch input.
|
||||
|
||||

|
||||
|
||||
The small label identifies the physical button; the larger label is the Touch input the game receives. Blue is
|
||||
X/A, orange is Y/B or Menu, and a dash means no Touch input. Left and right refer to the hand holding the controller.
|
||||
|
||||
<!-- quest-buttons-table:start -->
|
||||
| Frame input | SteamVR default (0) | Mirror (1) | Quest pair (2) |
|
||||
|---|---|---|---|
|
||||
| Left d-pad ↑ | Y | Y | — |
|
||||
| Left d-pad ↓ | X | X | X |
|
||||
| Left d-pad ← | Y | X | — |
|
||||
| Left d-pad → | Y | Y | Y |
|
||||
| Left View | Menu | Menu | Menu |
|
||||
| Right A (bottom) | A | A | A |
|
||||
| Right B (right) | B | A | — |
|
||||
| Right X (left) | B | B | B |
|
||||
| Right Y (top) | B | B | — |
|
||||
| Right Menu | — | — | — |
|
||||
| Left bumper | — | — | — |
|
||||
| Right bumper | — | — | — |
|
||||
<!-- quest-buttons-table:end -->
|
||||
|
||||
The table describes button clicks and face-button touches. View sends Menu click only. Triggers, grip pressure,
|
||||
thumbsticks, tracked poses and vibration keep their corresponding Touch bindings.
|
||||
|
||||
## Choose a mode
|
||||
|
||||
For one game, open its **Game settings → Controllers → Controller buttons**. Choose **Use global default** to
|
||||
follow Settings, or select a specific mode to keep an override for that game. An explicit **SteamVR default**
|
||||
keeps mode 0 even when the global choice is Mirror or Quest pair.
|
||||
|
||||
For the library default, open **Settings → Controllers**. The diagram follows the selected mode. New installs
|
||||
and configuration updates use this default for games without an override. With a Frame connected, choose
|
||||
**Apply to installed games now** to send it to installed Quest games that follow the global default. Overrides
|
||||
are kept. Changing the default does not mark every game as needing an update.
|
||||
|
||||
Changes take effect at the **next launch**. Close and relaunch a running game.
|
||||
Games installed before this feature need **Update on Frame** once for the mode to work.
|
||||
|
||||
The CLI can change an installed game's runtime setting:
|
||||
|
||||
```sh
|
||||
frameport settings <pkg> quest_buttons=2
|
||||
```
|
||||
|
||||
Use `0` for SteamVR default or `1` for Mirror. This command writes the installed settings directly; it does not
|
||||
save a per-game recipe override. To keep the value through later installs and global applies, also save it in
|
||||
the recipe, or use Game settings:
|
||||
|
||||
```sh
|
||||
frameport recipe <pkg> --set quest_buttons=2
|
||||
```
|
||||
|
||||
## Limits
|
||||
|
||||
- SteamVR's binding UI can still override these suggested bindings. Check custom bindings if the result differs
|
||||
from the diagram.
|
||||
- The d-pad has a single touch sensor: a thumb resting on it touches X and Y together. The right controller's
|
||||
face buttons sense touch one by one.
|
||||
- Bumpers have no Touch counterpart and are unbound. Thumbrest touch is dropped; the Frame profile has no
|
||||
corresponding input. System buttons keep their runtime role.
|
||||
- Mirror and Quest pair require the Frame runtime's `XR_VALVE_frame_controller_interaction` extension. If it is
|
||||
absent, FrameBridge logs that fact and leaves SteamVR's remap in place.
|
||||
- Games suggesting their own Frame Controller bindings keep those bindings and continue seeing the Frame
|
||||
Controller profile, even if they also suggest Touch bindings. These modes do not replace the game's own mapping.
|
||||
- Touch suggestions using bare pose, trigger, squeeze or button paths are translated too; explicit components
|
||||
such as `/pose`, `/value`, `/click` and `/touch` are also supported where the mapping provides them.
|
||||
- Finger proximity emulation (`proximity_emul`) continues to work with Mirror and Quest pair. Dropping the
|
||||
unsupported thumbrest path does not disable proximity emulation from the other touch inputs.
|
||||
- The setting applies to Quest VR games, including 32-bit games through the bridge. It does not change PC VR or
|
||||
flat Android Steam Input layouts.
|
||||
|
||||
The mapping follows [Valve's Frame Controller input reference](https://partner.steamgames.com/doc/steamhardware/steamframe/input)
|
||||
and the [shared mapping data](input/quest_buttons.json). To regenerate the diagram, app illustrations and table,
|
||||
run `uv run python scripts/controller_diagram.py`; add `--check` to verify them without writing files.
|
||||
@@ -288,7 +288,7 @@ Steam placeholder set (no Steam art at all) → fixed in artwork/steam.py.
|
||||
|
||||
## PR #34: Meta XR Audio metadata (Batman)
|
||||
|
||||
**PR #34 (merged 2026-10-04, Lucas-Mathieu):** `native/adapter/audio_metadata.h` patches Meta XR Audio Wwise (only build ID e1619e7f…, Batman: Arkham Shadow 1.4.1) so queued audio metadata isn't freed while the current audio frame references it (smoke-bomb crash). Verified headless: installs ("metadata reclamation follows …"), 72 fps; we added a once-per-second scan limit. Outside PRs: review source, rebuild `artifacts/` ourselves, merge locally (contributor's commit kept), push main.
|
||||
**PR #34 (merged 2026-10-04, Lucas-Mathieu):** `native/adapter/audio_metadata.h` patches Meta XR Audio Wwise (only build ID e1619e7f…, Batman: Arkham Shadow 1.4.1) so queued audio metadata isn't freed while the current audio frame references it (smoke-bomb crash). The shared detour now publishes one aligned AArch64 branch to a range-checked nearby stub, with a one-instruction trampoline; protection failures roll back (published mappings must remain alive for in-flight threads). See [AUDIO_GEOMETRY.md](AUDIO_GEOMETRY.md) for failure limits and host validation; this publication change has not been device-tested. Original repair verified headless: installs ("metadata reclamation follows …"), 72 fps; we added a once-per-second scan limit. Outside PRs: review source, rebuild `artifacts/` ourselves, merge locally (contributor's commit kept), push main.
|
||||
|
||||
## Heuristics eval (2026-10-04)
|
||||
|
||||
@@ -447,11 +447,34 @@ head-pose deadline ends VR mode without a worn headset (not a game bug).
|
||||
|
||||
## Cube swapchains
|
||||
|
||||
**Cube swapchains (GitHub #107, 2026-10-09):** the Frame's runtime refuses `faceCount=6` swapchains (-2,
|
||||
XR_ERROR_RUNTIME_FAILURE); OVRPlugin ignores that ("CreateSwapchain for eye 0: 0x0, 0 stages") and crashes in
|
||||
ovrp_EndFrame4 (memset). FrameBridge `cube_standin` (default on, `native/adapter/cube_standin.c`) serves a refused cube
|
||||
swapchain as one GL cube-map texture in the app's context (GLES only) and drops its layers. Verified headless with
|
||||
Budget Cuts Ultimate (2048² sRGB, 12 mips; runs on at ~70 fps); what the cube layer showed is simply missing.
|
||||
**Cube swapchains (GitHub #107/#385, 2026-10-17):** the Frame's runtime refuses `faceCount=6`
|
||||
swapchains (-2, XR_ERROR_RUNTIME_FAILURE). `cube_standin` (default on, `native/adapter/cube_standin.c`)
|
||||
serves refused cubes with a GLES cube texture in the app's context or a Vulkan cube-compatible image.
|
||||
This prevents OVRPlugin's empty-image-list crash (Budget Cuts Ultimate; Vulkan UNDERDOGS).
|
||||
|
||||
`cube_quads` (int, default 1) now copies mip 0 into six runtime quad swapchains, replacing each cube at
|
||||
its original layer-list position. Face order is +X, -X, +Y, -Y, +Z, -Z; inward-facing quad right/up axes
|
||||
match GL cube s/t. Vulkan copies reverse rows for its top-left quad image origin. The cube orientation
|
||||
rotates the box; its centre follows the stereo view midpoint in the layer's space at displayTime
|
||||
(falls back to the space origin without valid tracking). Faces are 100 m square at 50 m distance.
|
||||
Eye visibility and layer flags are retained. Quads submitted by the game retain their order.
|
||||
|
||||
The Frame draws quads above **every** projection, regardless of order. This implementation conservatively
|
||||
drops cubes whenever any projection is submitted, including alpha projections: it supports cube-only
|
||||
loading screens with optional quad overlays, not skyboxes behind gameplay. Multiple cubes of the same
|
||||
size/format share six swapchains, using separate array slices so their images cannot overwrite one another.
|
||||
Each pool is copied once per frame. Other sizes need another six swapchains; runtime allocation refusal
|
||||
(particularly near its ~15-swapchain cap) frees partial allocations and disables that pool until reset.
|
||||
Pools are freed on stand-in creation/destruction, xrEndSession and session destruction; stand-ins are also freed at
|
||||
session destruction. Set `cube_quads=0` for the previous drop-only behaviour.
|
||||
|
||||
Look for `cube_quads: created six ... face swapchains (GLES|Vulkan)` and
|
||||
`cube_quads: submitting cube as six quads (half-size=50m, mip=0)`. Refusals log
|
||||
`cube_quads: face swapchain creation failed (result=...); dropping cube layers`; mixed frames log
|
||||
`cube_quads: skipping cube with projection; preserving scene visibility` once.
|
||||
Host math/settings/artifact checks and Android builds are available; no device test yet. Headset checks
|
||||
still needed: face seams/orientation, sRGB/alpha, GLES context availability, Vulkan synchronization and
|
||||
loading-to-gameplay transitions (HardBullet's 1024²/32² Vulkan cubes; Budget Cuts GLES).
|
||||
|
||||
## Hardware video decoding
|
||||
|
||||
@@ -718,3 +741,104 @@ can be enabled and persist after restart. Check actual runtime refresh/pacing se
|
||||
identity alone does not implement refresh-rate support. Rebuild with Quest 2 for the control: original `Oculus Quest2`,
|
||||
original Quest 2 UUID, type 9 if logged, and previous option visibility. Also smoke-test a Unity OVRPlugin game
|
||||
with the default Quest 3 choice for regressions. No device access was used during implementation.
|
||||
|
||||
## Mixed reality: scanned rooms
|
||||
|
||||
Live floor coverage (agent v104) is enabled by default for manual and game-requested scans; the capture
|
||||
payload accepts `preview: false`, and `frameport frame room scan --no-preview` disables it. A world-locked,
|
||||
upward-facing 6 m square at floor height follows the current raw-to-STAGE calibration and stays centred on the
|
||||
scan's first recorded camera position. Every 16th depth vertex contributes to raw-space 10 cm voxels; at most once
|
||||
per second, newly occupied cells redraw a 256×256 RGBA map in one colour (FramePort's secondary blue), each cell a
|
||||
filled 10 cm square, only the opacity telling floor (<0.3 m, faint), raised surfaces (0.3–1.5 m) and tall surfaces
|
||||
(>1.5 m, strongest, the room's outline) apart; unseen transparent. Overlays have no depth (they draw over hands and
|
||||
passthrough), so the map stays faint; three colours in 3x3 blotches clashed in the owner's first run. It shows
|
||||
coverage, not a validated room boundary. IVROverlay_028 slots 1/3/20/22/33/43/62 (OpenVR 2.15.6 header)
|
||||
create/destroy/sort order (1, below the scan prompts' 10)/set width/set absolute STAGE transform/show/upload raw
|
||||
pixels. Each upload is 256 KiB (at most 256 KiB/s); timing for the first
|
||||
20 uploads and every tenth thereafter, plus failures, goes to `rooms/scans/scan-*.fprs.log` and the built room’s `scan.log`. Overlay errors or an
|
||||
upload taking over 50 ms disable the preview while capture continues; headset appearance and upload cost still
|
||||
need device validation.
|
||||
|
||||
When an MR game asks for a room scan, FramePort scans in the headset and FrameBridge loads the new room before
|
||||
answering the game. If no scanner answers within five seconds, the game gets the stored room. Set
|
||||
`scene_scan_request=0` to always use the stored room. A scan that starts but fails keeps the old room and reports
|
||||
a capture failure to the game.
|
||||
|
||||
**Mixed reality: scanned rooms (agent v92, 2026-10-16, branch mr-scene; docs/mr/):** Quest MR games read the room
|
||||
through Meta's scene APIs; the Frame offers none, and Valve's chaperone is no room (2D outline raised to 2.43 m, often
|
||||
the default 1 m circle -> the Android runtime's STAGE rect falls back to 1x1 m; XRService's plane detection is
|
||||
horizontal-only, its persistent map sparse ORB, its XRIPC single-client; cameras are XRService's alone,
|
||||
IVRTrackedCamera gives no frames). **The passthrough's RoomView 3D depth mesh is readable** (docs/mr/PHASE0.md:
|
||||
IVRBlockQueue_005 `/xrservice/roomview3d/meshData|meshLayout`, metadata through the C++ `IVRPaths_002` on the block
|
||||
handle, `meshIsUpToDate` on the queue handle; 132x128 fan, ~30/s; raw->standing moves during a session -> convert
|
||||
with the final one), so FramePort scans rooms itself: agent `room_capture` (Background/Overlay OpenVR app, prompts =
|
||||
artifacts/roomscan/*.png via IVROverlay_028 slots 1/3/20/22/35/43/44/63, `scripts/mr/render_prompts.py`) -> `.fprs`
|
||||
dump -> `fp_roomscan` (native/roomscan, freestanding, offline) -> `room.txt` + `room-mesh.bin`
|
||||
(docs/mr/ROOM_FORMAT.md; outline = positive shoelace in x-z, wall inward normal (-dz, dx)) in
|
||||
`~/.local/share/frameport/rooms/<universe>/`; launch.sh copies it next to settings.conf (`_room_sync`, games with
|
||||
`scene_emul`); FrameBridge (`scene_room.c/scene_meta.c/scene_anchors.c`) serves N walls, volumes (BOUNDED_3D),
|
||||
planes, GLOBAL_MESH (XR_META_spatial_entity_mesh), XR_META discovery/persistence (newer Meta SDKs and MRUK), anchors
|
||||
saved in `files/framebridge-anchors.txt`; all through xrshim (OVRPort's dispatcher drops unknown META calls).
|
||||
`scene_emul` is now suggested for every USE_SCENE/USE_ANCHOR_API game (migration `scene_emul_all`; NOPE and Pinball
|
||||
FX had none). PC: Frame page "Room" card, `frameport frame room scan|status|from-boundary|import|export|build|show`.
|
||||
Real data (owner's room, 2026-10-16): floor at 0, table surfaces, orthogonal walls found; the ceiling needs "look up".
|
||||
|
||||
Scan on request (agent v103): scene-emulation launchers run `_scan_watch` alongside the game. FrameBridge's
|
||||
`framebridge-scan-request` in the game's external files directory gets an ack, then the existing prompted headset
|
||||
capture/build flow saves and copies the new room before an `ok` result; errors, cancellation and timeout return
|
||||
`failed`. Captures share a global lock, stale protocol files are cleared on launch, and the watcher stops with the
|
||||
launcher. Logs: `<base>/scan-watch.log`. Device validation is still required.
|
||||
|
||||
PR #246 (@alicecatpaw, merged into mr-scene 2026-10-18): FrameBridge trims frames to maxLayerCount (Punch Fit sent
|
||||
21+ layers; projection layers and FrameBridge's fade quad always stay), drops quads with zero size or an empty image
|
||||
rect, and logs up to 10 quads with pose and swapchain. Two Unity patches, both only from a catalog recipe or the
|
||||
user's choice: `frame.meta_depth_occlusion` (Depth API occlusion off; Laser Dance's lasers vanished up close) and
|
||||
`frame.unity_scene_active_rooms` (OVRSceneManager's player-in-room check). The PR's own META discovery and boundary
|
||||
visibility code was replaced by this branch's (`scene_meta.c`). The PR's recipes were checked on the author's Frame
|
||||
only; open question: does OVRPort's dispatcher advertise XR_META_environment_depth (then a FrameBridge setting that
|
||||
hides it would fix depth occlusion for every engine).
|
||||
|
||||
## Vulkan pipelines refused: Fossilize and Turnip (Transformers, #349)
|
||||
|
||||
Transformers Beyond Reality: Redux (Unreal, Vulkan) ended its first frames with Unreal's "Shader compilation failures
|
||||
are Fatal." (2026-10-17, headless on the dev Frame). `vk_pipeline_log=1` showed every graphics pipeline failing in
|
||||
pairs: the precache attempt with `VK_PIPELINE_CREATE_FAIL_ON_PIPELINE_COMPILE_REQUIRED_BIT` (flags 0x100) answers
|
||||
`1000297000` (= VK_PIPELINE_COMPILE_REQUIRED, normal), the real compile answers `-1` (VK_ERROR_OUT_OF_HOST_MEMORY)
|
||||
in the same millisecond, and the shim never logged a single success. The dumped SPIR-V was valid (spirv-val), one
|
||||
module a constant-colour fragment shader. Ruled out in one run each: the app's VkPipelineCache, its
|
||||
VkAllocationCallbacks (retry without either/both), Valve's VALVE_rpo/VALVE_fdm_injection (`device.foveation: off`).
|
||||
Khronos' validation layer added a crash of its own (null deref in its vkCreateRenderPass2 on the game's create info,
|
||||
the shim changed nothing there): don't use it for this game. Removing `VK_LAYER_fossilize` from GraphicsEnv's debug
|
||||
layer list (`android::GraphicsEnv::setDebugLayers`, as `native/zinkfix` adds its layer) made the pipelines compile:
|
||||
360 modules instead of 10, frames submitted. Two Fossilize instances are still created by other threads before the
|
||||
engine loads the shim; they aren't the renderer's. About six pipelines with the game's multiview vertex shader
|
||||
(module 12944, ViewIndex) still fail in Turnip; a blank fragment shader alone doesn't help, blank vertex + fragment
|
||||
does. Settings `vk_no_fossilize` + `vk_pipeline_fallback` (catalog entry): headless RUNNING after 72 s at 72 fps.
|
||||
Headset check pending (some objects may be missing).
|
||||
|
||||
## Quest controller mode (`quest_buttons`, 2026-10-17)
|
||||
|
||||
Adapter setting `quest_buttons` (0 SteamVR default, 1 Mirror, 2 Quest pair; contract docs/input/QUEST_BUTTONS.md,
|
||||
user page docs/CONTROLLERS.md). FrameBridge enables `XR_VALVE_frame_controller_interaction` and, after a game's Touch
|
||||
suggestion succeeds, suggests `/interaction_profiles/valve/frame_controller_valve` bindings translated from the
|
||||
accepted Touch set (SteamVR uses a Frame binding before its own Touch remap: Touch B/Y on all three top buttons).
|
||||
|
||||
Headless check on the device (I Am Cat, Unity + OVRPlugin, mode 2, `input_diag=1`): the runtime offers and enables the
|
||||
extension in a Lepton app, and accepts the generated Frame suggestion (`quest_buttons: frame_controller bindings
|
||||
(mode 2, 27 bindings, 7 dropped): 0`); the game's Touch (34 paths) and hand-interaction suggestions were accepted as
|
||||
before; 72 fps, no new errors. Translations as in the JSON at the time: left `x/*` -> `dpad_down/*`, `y/*` -> `dpad_left/*`,
|
||||
`menu/click` -> `view/click`. Not provable headless (tracking needs the headset worn): that SteamVR then reads the
|
||||
buttons from the Frame binding; one launch in the headset settles it.
|
||||
|
||||
Pitfall seen during the test: OVRPort runs from two sessions on the same data folder share
|
||||
`overport-workspace/dec/working.apk` and fail at once (`OVRPort failed (exit 1)`, `FileAlreadyExistsException` in
|
||||
OVRPort's own output, which the CLI doesn't print without a verbose reporter). Build one game at a time per data folder.
|
||||
|
||||
Headset round 2026-10-18 (owner, TestQuestControllers): the button mapping works. Owner's corrections, now in the JSON:
|
||||
Quest's second button sits up and toward the other hand, so Quest pair = left Y on d-pad right, right B on X (Mirror
|
||||
follows: X/A = bottom + outer, Y/B = top + inner). The d-pad has one capacitive sensor: a thumb on it touches every
|
||||
direction, so X and Y touch light together (left as is, hardware limit); the right face buttons sense touch singly.
|
||||
SteamVR's own Touch remap (`/opt/steamvr/drivers/frame_controller/resources/input/frame_controller_remapping.json`,
|
||||
layout `oculus_touch`) is the diagram's "SteamVR default": X -> d-pad down, Y -> d-pad left/right/up, A -> A,
|
||||
B -> B/X/Y, Menu -> View, plus trigger click -> bumper. Games with their own Valve binding file in that folder
|
||||
(Beat Saber, Batman, Accounting+, In Death, ...) use that file instead.
|
||||
@@ -202,13 +202,24 @@ Saves stay on the Frame when you update or reinstall a game.
|
||||
|
||||
## Can I back up a game's saves?
|
||||
|
||||
Yes. Steam doesn't sync Quest games' saves, so FramePort copies them to your PC: the game's menu (**…** on its page) →
|
||||
**Saves** → **Back up saves**, or **Back up all saves** on the Steam Frame page (Installed games). Close the game
|
||||
Yes. Steam doesn't sync Quest games' saves, so FramePort copies them to your PC: **Back up saves** next to Play on
|
||||
the installed game's page, or **Back up all saves** on the Steam Frame page (Installed games). Close the game
|
||||
first. A backup holds the game's own storage (`/data/data/<package>` and `/sdcard/Android/data/<package>`), without
|
||||
caches, shader caches and large downloaded content (videos, OBBs, files over 64 MB); the job says when something was
|
||||
left out. FramePort keeps the 10 newest backups per game (Settings → Data). They're in FramePort's data folder under
|
||||
`save-backups/<package>/` (**Open backups folder** in the game's menu).
|
||||
|
||||
In **Settings → Data**, **Choose backup folder…** selects a different folder, including a local Drive or Nextcloud
|
||||
sync folder. FramePort checks that it can write there. **Use the default** resets the location. Existing backups stay
|
||||
where they are and remain listed for restoring; retention only removes old backups in the currently selected folder.
|
||||
FramePort writes local files; your cloud client handles uploading them.
|
||||
|
||||
**Back up saves when a game closes** (Settings → Data, off by default) keeps the newest three changed snapshots per
|
||||
game on the Frame, even while FramePort is closed. Connect once after changing this setting to apply it to the Frame.
|
||||
New snapshots are copied to your backup folder when FramePort connects or polls the connection, marked **automatic**,
|
||||
and count toward the same backup limit. Unchanged saves are skipped. A snapshot captures what the game wrote to disk;
|
||||
it cannot recover progress a forcibly closed game never saved.
|
||||
|
||||
**Restore saves…** puts one back: it replaces the game's saves on the Frame. The Frame keeps a copy of the saves it
|
||||
had, so **Undo last restore** in the same window takes you back. When you uninstall a game and tick *Also delete its
|
||||
saves*, FramePort backs them up first. Command line: `frameport saves backup|list|restore|undo <game>`.
|
||||
|
||||
@@ -66,7 +66,54 @@
|
||||
`launch.sh` at the new folder and only then deletes the old copy; on the same filesystem it renames instead.
|
||||
Moving a game whose Lepton data holds absolute paths, or a PC VR prefix, is not yet verified on the device.
|
||||
|
||||
## Save backups (Quest games, agent v95, discussion #352)
|
||||
## Room data
|
||||
|
||||
Live floor coverage (agent v104) is enabled by default for manual and game-requested scans; the capture
|
||||
payload accepts `preview: false`, and `frameport frame room scan --no-preview` disables it. A world-locked,
|
||||
upward-facing 6 m square at floor height follows the current raw-to-STAGE calibration and stays centred on the
|
||||
scan's first recorded camera position. Every 16th depth vertex contributes to raw-space 10 cm voxels; at most once
|
||||
per second, newly occupied cells redraw a 256×256 RGBA map in one colour (FramePort's secondary blue), each cell a
|
||||
filled 10 cm square, only the opacity telling floor (<0.3 m, faint), raised surfaces (0.3–1.5 m) and tall surfaces
|
||||
(>1.5 m, strongest, the room's outline) apart; unseen transparent. Overlays have no depth (they draw over hands and
|
||||
passthrough), so the map stays faint; three colours in 3x3 blotches clashed in the owner's first run. It shows
|
||||
coverage, not a validated room boundary. IVROverlay_028 slots 1/3/20/22/33/43/62 (OpenVR 2.15.6 header)
|
||||
create/destroy/sort order (1, below the scan prompts' 10)/set width/set absolute STAGE transform/show/upload raw
|
||||
pixels. Each upload is 256 KiB (at most 256 KiB/s); timing for the first
|
||||
20 uploads and every tenth thereafter, plus failures, goes to `rooms/scans/scan-*.fprs.log` and the built room’s `scan.log`. Overlay errors or an
|
||||
upload taking over 50 ms disable the preview while capture continues; headset appearance and upload cost still
|
||||
need device validation.
|
||||
|
||||
- SteamVR on the Frame has no Meta scene/anchor extensions. Its chaperone supplies only a 2D outline raised to
|
||||
2.43 m, without furniture, ceiling or a room mesh. Valve's default boundary is a 20-point circle of roughly 1 m
|
||||
radius; FramePort refuses to treat it as a room.
|
||||
- FrameBridge's `scene_emul=1` serves saved room data, with a guardian box when no room is available. OVRPort's
|
||||
loader only dispatches functions in its own table: newer Meta entry points go through FramePort's extension shim.
|
||||
MRUK uses discovery (`xrDiscoverSpacesMETA`, older versions `xrQuerySpacesFB`) and requests Space Setup when
|
||||
discovery returns no rooms. Volumes hang below their top-face anchor in Meta's coordinate convention.
|
||||
- Room files use STAGE coordinates (metres, +Y up, floor y=0, -Z forward). The format contract is
|
||||
[mr/ROOM_FORMAT.md](mr/ROOM_FORMAT.md). On the Frame: `~/.local/share/frameport/rooms/<universe>/room.txt`,
|
||||
optional `room-mesh.bin` and `scan.log`; `rooms/current` points to the most recently saved room.
|
||||
- A room belongs to its chaperone universe. The agent reads the last universe switch from `vrserver.txt`, falling
|
||||
back to the newest universe in `~/.config/openvr/config/chaperone_info.vrchap`; dummy 424242 means none.
|
||||
A matching room is selected first, else `current`; the app reports `aligned: false` after a universe change.
|
||||
- Quest launchers copy that room beside `settings.conf` before Lepton starts; FrameBridge reads
|
||||
`dirname($FRAMEBRIDGE_CONFIG)/room.txt`. Stale copies are removed if no room exists or emulation is disabled.
|
||||
Each game's persistent anchors stay in `/sdcard/Android/data/<pkg>/files/framebridge-anchors.txt`.
|
||||
- With `scene_emul`, launch.sh starts `_scan_watch <pkg>` after Lepton, closing fd 9; its exit trap stops the
|
||||
watcher, which also exits if the launcher disappears. It polls once a second and logs to `<base>/scan-watch.log`.
|
||||
In `<base>/lepton-data/external/Android/data/<pkg>/files/`, FrameBridge writes `framebridge-scan-request`
|
||||
(`request <id> <unix_seconds>`); the watcher clears stale request/ack/result files at startup, writes
|
||||
`framebridge-scan-ack` (`ack <id>`), reuses the headset capture/build worker, stores the current room and copies
|
||||
it beside settings.conf before atomically writing `framebridge-scan-result` (`ok <id>` or `failed <id> <reason>`).
|
||||
A global `rooms/.scan.lock` excludes concurrent captures, including manual scans; `rooms/.lock` still protects
|
||||
room publication. Launcher exit cancels the scan; a 350-second deadline leaves time for the six-minute protocol timeout.
|
||||
- Frame → Room and `frameport frame room` manage rooms. `from-boundary` creates walls without furniture;
|
||||
`import` / `export` transfer room files; `show` writes an SVG preview; `build <dump.fprs>` syncs the bundled
|
||||
`fp_roomscan` by SHA-256 and runs it on the Frame with `--offline`. `scan` = agent `room_capture`: records the
|
||||
RoomView meshes (`/xrservice/roomview3d/meshData` + `/meshLayout`, `IVRBlockQueue_005`) while in-headset prompts
|
||||
guide the player, then builds the room from that recording.
|
||||
|
||||
## Save backups (Quest games, agent v101, discussion #352)
|
||||
- A Lepton game's saves are two folders of `<base>/lepton-data`: `internal/<pkg>` (= `/data/data/<pkg>`:
|
||||
`shared_prefs/`, `files/`, `databases/`; Unreal's `files/UE4Game|UnrealGame/.../Saved` often lives here) and
|
||||
`external/Android/data/<pkg>` (= `/sdcard/Android/data/<pkg>`: Unity's `files/`, Unreal's external `Saved`). On the
|
||||
@@ -91,6 +138,23 @@
|
||||
files owned by mapped ids are readable and keep their ids, take the game's launch lock (`<base>/.launch.lock`, a
|
||||
Play meanwhile is ignored as a double launch) and refuse while its container runs. `saves_info` sizes a backup and
|
||||
reports the rollback copy, `saves_rollback_delete` removes it.
|
||||
- PC destination: `saves.folder` (empty = `<data>/save-backups`); Settings → Data has an awaited folder picker,
|
||||
a default reset and a write probe. Old destinations (`saves.previous_folders`) stay listed; switching never moves
|
||||
or deletes files. `saves.keep` defaults to 10 and prunes only the selected destination.
|
||||
- `saves.auto` defaults off. Connection and 30 s poll hooks sync its value using `save_snapshot_settings` (flag
|
||||
`~/.local/share/frameport/save-snapshots/enabled`). Updated and newly written Quest launchers call `_snapshot_close`
|
||||
after stopping the container, inside the EXIT trap, with the launch lock still held on fd 9. The worker verifies
|
||||
that fd against `<base>/.launch.lock` and passes it through `podman unshare`; another launch cannot run meanwhile.
|
||||
`timeout -k 5 90` bounds the whole worker. PC VR/Linux launchers have no snapshot step.
|
||||
- Snapshots use the same archive builder, exclusions and 64 MiB/file, 4 GiB/total limits as manual backups.
|
||||
File paths/sizes/content hashes form a fingerprint; unchanged saves skip compression. Complete snapshots are
|
||||
atomically published under `~/.local/share/frameport/save-snapshots/<pkg>/<ns>-<sha256>.tar.gz`, with fingerprint
|
||||
sidecars. The newest three per game are retained; interrupted `.part` files are overwritten on the next attempt.
|
||||
- `save_snapshots` lists immutable snapshots oldest first per game; `_fetch_save_snapshot '<json>'` streams one
|
||||
(archive stdout, result JSON stderr). PC `install/saves.sync_snapshots` runs in the background, verifies the archive
|
||||
hash and package, imports it as a normal backup marked `automatic`, and applies `saves.keep`. A local
|
||||
`<data>/save-snapshots-synced.json` digest ledger prevents reimporting snapshots after pruning or deleting backups.
|
||||
Failures leave snapshots on the Frame for the next poll. This path does no disk/SSH work in rendering.
|
||||
|
||||
## OpenXR runtime (as seen by games through overport's loader)
|
||||
- Instance extensions present include KHR_android_create_instance (must be enabled; the adapter adds it),
|
||||
@@ -107,7 +171,9 @@
|
||||
`ComposeLayerQuad` and `ComposeLayerProjection`; cube/cylinder/equirect/equirect2 exist only as enum names, so
|
||||
there is nothing to switch on. It has a layer limit ("Exceeded the layer limit"; the value is logged by
|
||||
`layer_debug` from `maxLayerCount`). The adapter emulates cylinders (strips) and, per game, equirect layers
|
||||
(`equirect_emul`, an adapter projection layer). Quad layers are drawn above every projection layer regardless of
|
||||
(`equirect_emul`, an adapter projection layer). Refused cube swapchains use GLES/Vulkan stand-ins;
|
||||
`cube_quads` (default on) copies their faces into six quads for frames without a projection
|
||||
(see DEVICE_NOTES "Cube swapchains"). Quad layers are drawn above every projection layer regardless of
|
||||
submission order (seen 2026-10-01: quads placed before 4XVR's projection layer covered it).
|
||||
- Swapchain formats: GLES `GL_SRGB8_ALPHA8` (35907) / `GL_SRGB8` (35905) only, samples = 1. Vulkan: 43 (R8G8B8A8_SRGB)
|
||||
and 50, not 37/44 (UNORM).
|
||||
@@ -118,6 +184,26 @@
|
||||
- Display 72 Hz by default in tests. Head pose is only tracked while the headset is worn; otherwise flags 0x3 and
|
||||
the session stays below FOCUSED.
|
||||
|
||||
### Frame Controller input
|
||||
|
||||
The runtime offers `XR_VALVE_frame_controller_interaction`, with interaction profile
|
||||
`/interaction_profiles/valve/frame_controller_valve`. [Valve's input reference](https://partner.steamgames.com/doc/steamhardware/steamframe/input)
|
||||
describes two gamepad halves. Under `/user/hand/<hand>/input/`:
|
||||
|
||||
- Left: `dpad_up`, `dpad_down`, `dpad_left`, `dpad_right`, `view` (click and touch).
|
||||
- Right: `a`, `b`, `x`, `y`, `menu` (click and touch). The face diamond is A bottom, B right, X left, Y top.
|
||||
- Both: `system`, `bumper`; `squeeze` and `trigger` (click, touch, value); `thumbstick` (vector, x, y, click,
|
||||
touch); `grip/pose`, `aim/pose`. Haptics use `/user/hand/<hand>/output/haptic`.
|
||||
|
||||
SteamVR prefers a game's Frame Controller binding. Without one, its Touch remap sends left d-pad down as Touch X
|
||||
and left/up/right as Touch Y; right A as Touch A and X/Y/B as Touch B. Left View sends Touch Menu.
|
||||
FrameBridge's `quest_buttons` preference (`0` default, `1` Mirror, `2` Quest pair) enables the extension when offered
|
||||
and suggests a Frame binding translated from the game's Touch suggestions. Common pressure/trigger/thumbstick,
|
||||
pose and haptic paths keep their Touch counterparts; thumbrest touch and system click are dropped, bumpers stay
|
||||
unbound. The player's SteamVR custom bindings can override suggestions. This works on both bridge ABIs, without
|
||||
the arm64-only extension shim. See [Controller buttons](CONTROLLERS.md) and the [mapping contract](input/QUEST_BUTTONS.md).
|
||||
Device verification of the new modes is pending integration with the native stream.
|
||||
|
||||
## Lua native pointer compatibility
|
||||
|
||||
Frame's Android processes can map libraries above the lowest 2^47 bytes of address space. The verified xLua API 105
|
||||
@@ -398,14 +484,57 @@ over Quest games.
|
||||
|
||||
Under a Quest game's **Game settings → Picture**, choose **Foveated rendering → Eye-tracked**, then **Save**.
|
||||
Changes apply at the next game start.
|
||||
The full-resolution area selector defaults to **Default (30% radius)**: no radius override, preserving the
|
||||
runtime profile used in the headset experiments. Larger radii retain more detail and save less shading work.
|
||||
Custom radii set `FOVE_INNER`; `FOVE_OUTER` is twice that value, matching the runtime's profile proportions.
|
||||
The inspected runtime's built-in inner/outer pairs are 0.30/0.60, 0.25/0.50 and 0.20/0.40.
|
||||
The shared-texture injection path selects profile 1: 0.30 inner radius and 0.60 outer radius.
|
||||
The area selector now uses Valve's supported `FOVE_LEVEL` presets (GitHub #394; binary analysis
|
||||
2026-10-17). Default leaves the runtime choice alone; on the shared injection path this is `lo`.
|
||||
The old `FOVE_INNER`/`FOVE_OUTER` radius exports had no effect: Lepton does not pass them through,
|
||||
and the layer does not read them. The previously documented 0.30/0.60, 0.25/0.50, 0.20/0.40
|
||||
radius pairs do **not** describe the generated shared-injection density map.
|
||||
|
||||
The inspected arm64 `libVkLayer_VALVE_fdm_injection.so` has these semantics (virtual addresses):
|
||||
- `init_fdm_fove_level` at `0x1cdf4` calls the cached option reader, then
|
||||
`debug_parse_flags_option` (`0x21d74`) with table `0x27e70`: named tokens `lo` → 1,
|
||||
`med` → 2, `hi` → 3. It does not call atoi/strtol. Unset, empty, unknown and numeric
|
||||
strings parse to zero and retain the current profile (initialized to 1 at `0x1ce74`).
|
||||
Tokens are case-sensitive and separated by non-alphanumeric/non-underscore characters;
|
||||
matching values are ORed, so combinations can alias another profile. `all` also yields 3;
|
||||
`help` prints the table and returns zero. Use a single preset token.
|
||||
- Profile selection at `0x1918c–0x19364` uses normalized **rectangles**, not circular radii.
|
||||
`lo` has a full-density center rectangle `(x=.364, y=.367, width=.295, height=.286)`
|
||||
(constant at `0xe440`), then density byte `0x76` outside. `med` uses rectangles from
|
||||
`0xe988`: sizes `.250×.245`, `.295×.286`, `.386×.367`, with density bytes
|
||||
`0xff`, `0xa8`, `0x76` and `0x50` outside. `hi` uses `0xe9b8`: sizes
|
||||
`.246×.211`, `.281×.246`, `.351×.316`, `.386×.351`, with bytes
|
||||
`0xff`, `0xa8`, `0x76`, `0x50`, then `0x38` outside. Each byte is written to both
|
||||
R8G8 density channels. All three retain a full-density center (`0xff`); medium/high make it smaller
|
||||
and reduce peripheral density more. Coordinates are sampled on the density-map grid (`0x19378` onward).
|
||||
- `FDM_DEBUG` flags `lo`/`med`/`hi` also select presets (high takes precedence), but an
|
||||
explicit recognized `FOVE_LEVEL` overrides them (`0x1ce14`). `enable` gates injection;
|
||||
`disable_offsets` fixes the gaze position (`0x1b6c8`), and `yflip` flips the vertical
|
||||
offset (`0x1b9b8`). These three flags do not resize the profile.
|
||||
- `FDM_SWAPCHAIN_SIZE` is parsed by `atoi` (`0x1c518`), clamped to nonnegative, and
|
||||
seeds a known swapchain size (`0x1ccf8`). It is a size/matching hint, not a radius.
|
||||
The synthetic map is square with side `ceil(size/32)` (`0x19048–0x19088`); image
|
||||
association also matches this tile size (`0x16c78`). Changing render resolution changes
|
||||
the pixel extent, tile quantization and potentially the matched render target/map. This can
|
||||
change the visible diagnostic area without making the ignored radius exports effective;
|
||||
a matched device capture is needed to establish which path a particular game used.
|
||||
|
||||
Per-game library preferences now store `level`. Legacy radius choices are translated on read
|
||||
(and written as `level` on Save): nearest old nominal .30 → lo, .25 → med, .20 → hi,
|
||||
with larger values clamped by nearest choice; zero or unknown/invalid values → Default.
|
||||
This preserves approximate intent, not a claim that these presets implement those radii.
|
||||
Explicit `level` wins. Obsolete radius exports are removed from managed launchers.
|
||||
|
||||
For device verification, hold game scene, render resolution and gaze constant; compare explicit
|
||||
`lo`, `med`, `hi` across fresh game starts (Default should match lo absent another runtime override).
|
||||
First use Fixed for a stable comparison, then Eye-tracked to check gaze following. Enable
|
||||
**Darken coarse fragments (test)** and adapter `snapshot=N` to capture submitted eye images headlessly
|
||||
every N seconds (GLES only, left eye at quarter size); compare the undarkened center and peripheral bands. Existing adapter
|
||||
builds need diagnostic support installed. Turn the test and snapshots off before benchmarking.
|
||||
Agent `FOVEATION_ENV_KEYS` must include `"FOVE_LEVEL"` for launcher updates via `set_settings`.
|
||||
|
||||
`device.foveation` enables Valve's shared Vulkan injection and suppresses competing native foveation through
|
||||
`foveation_fix=1`. Game settings stores mode, radius and test selection as a per-game library preference, outside shared recipes. This remains
|
||||
`foveation_fix=1`. Game settings stores mode, profile and test selection as a per-game library preference, outside shared recipes. This remains
|
||||
experimental: complete images were confirmed in RE4, Batman, Deadpool, The Climb 2 and Asgard's Wrath 2,
|
||||
not all Quest games or renderers. RE4 and Batman checks include title/calibration scenes; these are not
|
||||
equivalent to extended gameplay coverage. Asgard gameplay was visually confirmed, without an on/off benchmark.
|
||||
@@ -432,7 +561,7 @@ in Climb the same library also carries the compatibility correction.
|
||||
|
||||
High-resolution Deadpool samples crossed from about 45 to 90 FPS with injection enabled, but temperatures and
|
||||
power rose as it rendered twice as many frames. Foveation does not guarantee lower temperature at uncapped FPS.
|
||||
The new configurable radii have configuration/build coverage; each radius has not been separately play-tested.
|
||||
Preset export/migration has configuration coverage; these preset comparisons have not been device-tested here.
|
||||
|
||||
## Monitoring sources (probed 2026-10-07, SteamOS 0.4.3, kernel 6.18; used by the Monitor tab, agent `_monitor`)
|
||||
|
||||
@@ -441,7 +570,7 @@ All readable by the steamos user without root; the agent reads them directly (no
|
||||
| Metric | Source | Notes |
|
||||
|---|---|---|
|
||||
| CPU | `/proc/stat` deltas; `cpufreq/policy{0,2,5,7}` | 8 cores in 4 clusters: 0-1 / 2-4 / 5-6 / 7 (max 2.27 / 3.15 / 2.96 / 3.05 GHz) |
|
||||
| GPU busy | Sum of `drm-engine-gpu` (ns) deltas in `/proc/<pid>/fdinfo/<fd>` over every render-node fd (msm DRM) | Gives GPU % per process too. A process can hold several render fds: add them all. Lepton games see the node as `/dev/kgsl-3d0` (bind mount of `/dev/dri/renderD128`). `drm-total-memory` reads 0 |
|
||||
| GPU busy | Sum of `drm-engine-gpu` (ns) deltas in `/proc/<pid>/fdinfo/<fd>` over every render-node fd (msm DRM) | Gives GPU % per process too. A process can hold several render fds: add them all. Lepton games see the node as `/dev/kgsl-3d0` (bind mount of `/dev/dri/renderD128`). `drm-total-memory` reads 0. msm can report garbage engine time (SteamVR's vrcompositor: near 2^64, rising ~1000x faster than real time; idle Frame at 100 %, 2026-10-18): a per-fd delta longer than the interval falls back to `drm-cycles-gpu` / GPU clock (agent v102 `gpu_busy_ns`) |
|
||||
| GPU clock | `/sys/class/devfreq/3d00000.gpu/{cur,max}_freq` | 231–903 MHz in 12 steps |
|
||||
| Memory / pressure | `/proc/meminfo`; `/proc/pressure/{cpu,memory,io}` (`some avg10`) | 16 GB RAM, 8 GB swap |
|
||||
| Temperatures | `/sys/class/thermal/thermal_zone*/{type,temp}` | 48 zones: `cpu*`/`cpuss*`, `gpuss-*`, `ddr`, `nsph*` (NPU), `pm8550*`/`pm8010*` (power ICs), `modem*`, `camera*`, `video`, `max1720x_bat*` |
|
||||
@@ -459,7 +588,7 @@ game processes without one), temperatures every 2 s, the CPU-cluster power rails
|
||||
|
||||
### Foveation user preferences (discussion #365)
|
||||
|
||||
Game settings → Picture provides Default / Eye-tracked / Fixed / Off, radius, and an advanced diagnostic switch.
|
||||
Game settings → Picture provides Default / Eye-tracked / Fixed / Off, a runtime profile, and an advanced diagnostic switch.
|
||||
`set_settings` (agent 98) atomically rewrites just the managed launcher exports and records them in deployment.json;
|
||||
no APK rebuild, reinstall or live device intervention is needed. Changes apply at the next start. Default restores
|
||||
recipe/catalog launcher defaults; explicit preferences win. One-time library migration extracts old recipe choices
|
||||
|
||||
@@ -26,6 +26,7 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
|
||||
| Beat Saber | Quest | ✅ Works | |
|
||||
| Beat Saber | PC VR | ✅ Works | |
|
||||
| Beat Saber (co-existence build) | Quest | ✅ Works | |
|
||||
| Black Hole Pool | Quest | ✅ Works | |
|
||||
| Blade & Sorcery: Nomad | Quest | ✅ Works | |
|
||||
| BlazeRush | Quest | ✅ Works | |
|
||||
| BodyCombat | Quest | ✅ Works | |
|
||||
@@ -41,6 +42,7 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
|
||||
| Creed | Quest | ✅ Works | |
|
||||
| Crisis Zone VR | Quest | ✅ Works | |
|
||||
| cubism | Quest | ✅ Works | |
|
||||
| cubism | Quest | ✅ Works | |
|
||||
| Dance Central | Quest | ✅ Works | |
|
||||
| Deep Cuts | Quest | ✅ Works | |
|
||||
| Demeo | Quest | ✅ Works | |
|
||||
@@ -65,6 +67,7 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
|
||||
| Gravity Lab | Quest | ✅ Works | |
|
||||
| Gunjack | Quest | ✅ Works | |
|
||||
| H.U.N.T | Quest | ✅ Works | |
|
||||
| Hitstream | Quest | ✅ Works | |
|
||||
| I Am Cat | Quest | ✅ Works | |
|
||||
| I Am Monkey | Quest | ✅ Works | |
|
||||
| I Expect You To Die 2 | Quest | ✅ Works | |
|
||||
@@ -76,8 +79,10 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
|
||||
| Jurassic World Aftermath Collection | Quest | ✅ Works | |
|
||||
| Keep Talking and Nobody Explodes | Quest | ✅ Works | |
|
||||
| Kill It With Fire VR | Quest | ✅ Works | |
|
||||
| KillerFrequency | Quest | ✅ Works | |
|
||||
| Ladder | Quest | ✅ Works | |
|
||||
| Lambda1VR | Quest | ✅ Works | |
|
||||
| Laser Dance | Quest | ✅ Works | |
|
||||
| LEGO® Bricktales | Quest | ✅ Works | |
|
||||
| Lost Recipes | Quest | ✅ Works | |
|
||||
| Lucky's Tale | Quest | ✅ Works | |
|
||||
@@ -106,6 +111,7 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
|
||||
| Pistol Whip | PC VR | ✅ Works | |
|
||||
| Please Don't Touch Anything | Quest | ✅ Works | |
|
||||
| PowerWash Simulator VR | Quest | ✅ Works | |
|
||||
| Punch Fit | Quest | ✅ Works | |
|
||||
| QuestCraft | Quest | ✅ Works | |
|
||||
| Racket: Nx | Quest | ✅ Works | |
|
||||
| RacketPinball | Quest | ✅ Works | |
|
||||
@@ -117,6 +123,7 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
|
||||
| Riven | Quest | ✅ Works | |
|
||||
| Robo Recall | Quest | ✅ Works | |
|
||||
| RUINSMAGUS | Quest | ✅ Works | |
|
||||
| Scared By Squares | Quest | ✅ Works | |
|
||||
| Sclerosis | Quest | ✅ Works | |
|
||||
| Shadow Point | Quest | ✅ Works | |
|
||||
| Shoot The Zombirds VR | Quest | ✅ Works | |
|
||||
@@ -126,16 +133,22 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
|
||||
| Sniper Elite VR | Quest | ✅ Works | |
|
||||
| Sniper Elite VR: Winter Warrior | Quest | ✅ Works | |
|
||||
| Space Pirate Trainer Quest | Quest | ✅ Works | |
|
||||
| Spectre | Quest | ✅ Works | |
|
||||
| Sports Scramble (Santa Cruz) | Quest | ✅ Works | |
|
||||
| Star Wars Pinball VR | Quest | ✅ Works | |
|
||||
| Star Wars: Tales from the Galaxy's Edge | Quest | ✅ Works | |
|
||||
| Stremio | Quest | ✅ Works | |
|
||||
| Summer Sports | Quest | ✅ Works | |
|
||||
| Superfly | Quest | ✅ Works | |
|
||||
| SUPERHOT VR | PC VR | ✅ Works | |
|
||||
| SUPERHOT VR | Quest | ✅ Works | |
|
||||
| Survival Nation | Quest | ✅ Works | |
|
||||
| Surviving Mars: Pioneer | Quest | ✅ Works | |
|
||||
| SynthRiders | Quest | ✅ Works | |
|
||||
| Table Troopers | Quest | ✅ Works | |
|
||||
| Taskmaster | Quest | ✅ Works | |
|
||||
| Tea For God | Quest | ✅ Works | |
|
||||
| Test Quest Controllers | Quest | ✅ Works | |
|
||||
| TetrisEffect | Quest | ✅ Works | |
|
||||
| The Boys VR | Quest | ✅ Works | |
|
||||
| The Climb | Quest | ✅ Works | |
|
||||
@@ -144,6 +157,7 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
|
||||
| The Room VR | Quest | ✅ Works | |
|
||||
| The Tale of Onogoro | Quest | ✅ Works | |
|
||||
| The Walking Dead: Saints & Sinners | Quest | ✅ Works | |
|
||||
| The Wizards | Quest | ✅ Works | |
|
||||
| Time Crisis II VR (Test) | Quest | ✅ Works | |
|
||||
| Time Crisis VR (Experimental) | Quest | ✅ Works | |
|
||||
| Totally Baseball | Quest | ✅ Works | |
|
||||
@@ -176,13 +190,14 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
|
||||
| ExploreVR | Quest | ⚠️ Works with issues | The 3D world warps and shifts with head movement (wrong perspective). |
|
||||
| FGO VR | Quest | ⚠️ Works with issues | Audio stuttering and popping sounds |
|
||||
| Fisherman | Quest | ⚠️ Works with issues | Loading screens have corruption, but everything works otherwise. |
|
||||
| HardBullet | Quest | ⚠️ Works with issues | Loading screens (a cube overlay) are missing or corrupted up to FramePort 1.2.1; cube_quads (next release) shows them as six panels, not confirmed in the hea… |
|
||||
| Homeworld: Vast Reaches | Quest | ⚠️ Works with issues | The game starts but after selecting to start a new game it seems to crash. |
|
||||
| Lawn Mowing Simulator VR | Quest | ⚠️ Works with issues | Left eye textures flicker only when mowing in specific areas. |
|
||||
| MacacoVR | Quest | ⚠️ Works with issues | 2d loading screens are glitchy, otherwise main gameplay is fine |
|
||||
| Majora's Mask VR | Quest | ⚠️ Works with issues | Audio on the file select screen sounds crunchy. |
|
||||
| Myst | Quest | ⚠️ Works with issues | Minor graphical glitches on some objects. |
|
||||
| Phantom: Covert Ops | Quest | ⚠️ Works with issues | DLC/store button crashes (no Meta store). |
|
||||
| Pinball FX VR | Quest | ⚠️ Works with issues | Plays; mixed reality mode not working yet. |
|
||||
| Pinball FX VR | Quest | ⚠️ Works with issues | Plays; space warp off (fewer stutters). |
|
||||
| Plezy | Quest | ⚠️ Works with issues | Some videos play with strong artifacts and wrong colours; the cause is unknown, possibly hardware video decoding. |
|
||||
| Puzzling Places | Quest | ⚠️ Works with issues | Works fine, but pointers dont show well on main menu, easy to work around. |
|
||||
| Quantum Void | Quest | ⚠️ Works with issues | no hands |
|
||||
@@ -204,3 +219,4 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
|
||||
| GOLF+ | Quest | ❌ Doesn't run | Black screen. |
|
||||
| HITMAN 3 VR: Reloaded | Quest | ❌ Doesn't run | Vulkan driver crash (freedreno), even without Valve layers. |
|
||||
| Roblox | Quest | ❌ Doesn't run | Crashes on its first VR frame on the Frame. |
|
||||
| Thief Simulator | Quest | ❌ Doesn't run | Quits at start with the game's own "installed from unknown source" message. |
|
||||
@@ -133,6 +133,14 @@ installed game keeps its Steam entry, artwork and saves (Steam restarts once).
|
||||
- A game on a card only starts while the card is inserted.
|
||||
- Cards formatted as FAT, exFAT or NTFS can't hold games: format the card in SteamOS first.
|
||||
|
||||
### Why do the controller buttons repeat or do the wrong thing?
|
||||
|
||||
SteamVR maps Quest Touch onto the Frame's controller buttons by default: three top buttons repeat Touch Y/B.
|
||||
Choose **Game settings → Controllers → Controller buttons** to use Mirror or Quest pair for one game, or
|
||||
**Settings → Controllers** for the library default. **Apply to installed games now** sends the default to Quest
|
||||
games without an override. Changes take effect at the next launch.
|
||||
See [Controller buttons](CONTROLLERS.md) for the diagram, mappings and limits.
|
||||
|
||||
### Android apps and Windows programs
|
||||
|
||||
- Android apps without VR are installed unchanged and shown as a window in the headset. If one shows nothing (or a
|
||||
@@ -175,6 +183,29 @@ Website owners: see [Install button](INSTALL_BUTTON.md).
|
||||
so stop it when you're done.
|
||||
- **Screenshots** shows the screenshots you took in the headset, sorted by game and day, to view or download.
|
||||
|
||||
## Your room (mixed reality, experimental)
|
||||
|
||||
Mixed-reality games ask for your room: walls, furniture and its surfaces. **Room** (in the sidebar) keeps it:
|
||||
|
||||
- **Scan room in the headset**, then put the headset on with passthrough and look around for about a minute; the tab
|
||||
shows how far the scan is. **Cancel scan** stops it and keeps your saved room. **Use my play area** makes walls from
|
||||
the SteamVR play area instead (no furniture).
|
||||
- The room shows in 3D: drag to turn it, scroll to zoom, right-drag to move it, double-click to see all of it.
|
||||
**Open full 3D view** shows every detail of the scan in your browser (a page on this PC only).
|
||||
- **Clean-up** tidies every scan (flat walls, no scan behind walls or where you walked, no loose bits). The
|
||||
recommended settings suit most rooms; change them (**More settings** has all of them) and press **Apply** to clean
|
||||
up your saved scan again. Your settings are also used for your next scans.
|
||||
- **Labels** shows what the scanner's boxes became and why. A tall, thin box by a wall can be **Add as door** or
|
||||
**Add as closet**.
|
||||
- **Furniture & screens**: pick a box in the list or click it in the 3D view to change its type, place or size, add
|
||||
or delete boxes, and add doors and windows on a wall. **Save** sends them to the Frame; they stay when you clean up
|
||||
the scan again (**Back to the scan's boxes** under Labels drops them).
|
||||
- The free **Test Quest MR** app (Get Test Quest MR on the tab) shows what a game receives from your scan.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Updating
|
||||
|
||||
FramePort checks for a new version at start and every 6 hours. When there is one, the Library shows **Update now**:
|
||||
|
||||
@@ -64,6 +64,14 @@ Versions compare number by number (`1.10` > `1.9`), and a suffix sorts before th
|
||||
|
||||
To build a link from Python: `frameport.deeplink.make_link(manifest_url="https://…/my-game.json")`.
|
||||
|
||||
**Where to host the manifest.** The install page shows the manifest's name, description and icon only when the
|
||||
manifest's host lets web pages read it (CORS); otherwise it shows "A game or app" (FramePort itself reads any public
|
||||
address). On GitHub, commit the manifest to the repository and link it as a file: `https://raw.githubusercontent.com/
|
||||
<owner>/<repo>/main/my-game.json`, or the `https://github.com/<owner>/<repo>/blob/main/my-game.json` page address,
|
||||
which the page and FramePort turn into the raw address. A manifest attached to a GitHub release
|
||||
(`…/releases/download/…`) installs fine, but web pages can't read it: the page then shows the release's name from
|
||||
GitHub's API instead.
|
||||
|
||||
## On a website
|
||||
|
||||
Image version (any page):
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# Patches
|
||||
|
||||
Every patch and game setting FramePort knows: 182 entries in 5 groups. A game's recipe is its chosen patches and settings; tested games get theirs from the catalog ([GAMES.md](GAMES.md)), and other games get the patches FramePort suggests from its analysis. You change them on the game page under Customize.
|
||||
Every patch and game setting FramePort knows: 191 entries in 5 groups. A game's recipe is its chosen patches and settings; tested games get theirs from the catalog ([GAMES.md](GAMES.md)), and other games get the patches FramePort suggests from its analysis. You change them on the game page under Customize.
|
||||
|
||||
Each entry says what the patch does, whether it changes the APK or only the install, when FramePort suggests it, its options, the catalog games whose recipe names it and the launch-test signatures that suggest it. Patches that are on by default are on for every game, so the catalog rarely names them.
|
||||
|
||||
@@ -11,8 +11,8 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
|
||||
## Groups
|
||||
|
||||
- [Patches from OVRPort](#group-overport) (25)
|
||||
- [Steam Frame patches](#group-frame) (61)
|
||||
- [Game settings](#group-adapter) (73)
|
||||
- [Steam Frame patches](#group-frame) (64)
|
||||
- [Game settings](#group-adapter) (79)
|
||||
- [Files and settings on the Frame](#group-device) (10)
|
||||
- [PC VR (Proton and Revive)](#group-pcvr) (13)
|
||||
|
||||
@@ -39,7 +39,7 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
|
||||
| [Patch Oculus detection for Unity](#patch_oculus_unity) | `patch_oculus_unity` | Patches from OVRPort | default |
|
||||
| [Patch Oculus detection for Unreal](#patch_oculus_unreal) | `patch_oculus_unreal` | Patches from OVRPort | default |
|
||||
| [Remove localized app names](#patch_remove_localized_names) | `patch_remove_localized_names` | Patches from OVRPort | default |
|
||||
| [Remove Unreal's ForceQuit](#patch_remove_unreal_force_quit) | `patch_remove_unreal_force_quit` | Patches from OVRPort | 45 |
|
||||
| [Remove Unreal's ForceQuit](#patch_remove_unreal_force_quit) | `patch_remove_unreal_force_quit` | Patches from OVRPort | 46 |
|
||||
| [Remove uses-library](#patch_remove_uses_library) | `patch_remove_uses_library` | Patches from OVRPort | default |
|
||||
| [Remove VrApi library](#patch_remove_vrapi) | `patch_remove_vrapi` | Patches from OVRPort | |
|
||||
| [Replace application label and icon](#patch_replace_icon_label) | `patch_replace_icon_label` | Patches from OVRPort | default |
|
||||
@@ -52,18 +52,20 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
|
||||
| [Skip Meta avatars](#frame-avatar_stub) | `frame.avatar_stub` | Steam Frame patches | 1 |
|
||||
| [bHaptics: skip the Bluetooth permission request](#frame-bhaptics_permission) | `frame.bhaptics_permission` | Steam Frame patches | 6 |
|
||||
| [BlazeRush: controllers without Meta avatars, own shader cache](#frame-blazerush) | `frame.blazerush` | Steam Frame patches | 1 |
|
||||
| [Cortopia games: don't wait for Meta's org-scoped id](#frame-cortopia_org_id) | `frame.cortopia_org_id` | Steam Frame patches | |
|
||||
| [Cortopia games: don't wait for Meta's org-scoped id](#frame-cortopia_org_id) | `frame.cortopia_org_id` | Steam Frame patches | 1 |
|
||||
| [Epic Online Services: stop the keychain retry loop](#frame-eos_keychain_loop) | `frame.eos_keychain_loop` | Steam Frame patches | |
|
||||
| [OpenGL ES: multiview shaders on flat panels (HUD, menus)](#frame-gl_multiview_fbo) | `frame.gl_multiview_fbo` | Steam Frame patches | 1 |
|
||||
| [GL shim (Mesa GLSL compatibility)](#frame-gl_shim) | `frame.gl_shim` | Steam Frame patches | 2 |
|
||||
| [Play services: Android 11 version checks](#frame-gms_platform_version) | `frame.gms_platform_version` | Steam Frame patches | |
|
||||
| [Play services: Android 11 version checks](#frame-gms_platform_version) | `frame.gms_platform_version` | Steam Frame patches | 1 |
|
||||
| [Godot apps: start without a clipboard service](#frame-godot_clipboard) | `frame.godot_clipboard` | Steam Frame patches | 1 |
|
||||
| [Hardware video decoding (H.264, HEVC, VP9)](#frame-hw_video_decode) | `frame.hw_video_decode` | Steam Frame patches | 31 |
|
||||
| [Hardware video decoding (H.264, HEVC, VP9)](#frame-hw_video_decode) | `frame.hw_video_decode` | Steam Frame patches | 35 |
|
||||
| [Language packs from the game's files](#frame-langpacks) | `frame.langpacks` | Steam Frame patches | |
|
||||
| [Launcher category (INFO → LAUNCHER)](#frame-launcher) | `frame.launcher` | Steam Frame patches | default |
|
||||
| [GL wrapper: OpenGL ES depth formats (Minecraft)](#frame-ltw_depth) | `frame.ltw_depth` | Steam Frame patches | default |
|
||||
| [Fix Lua startup crashes on Frame](#frame-luajit_pointers) | `frame.luajit_pointers` | Steam Frame patches | 1 |
|
||||
| [Declare Meta-only permissions](#frame-meta_permissions) | `frame.meta_permissions` | Steam Frame patches | 4 |
|
||||
| [Start the game in the app's main process](#frame-main_process) | `frame.main_process` | Steam Frame patches | default |
|
||||
| [Unity: disable Meta depth occlusion](#frame-meta_depth_occlusion) | `frame.meta_depth_occlusion` | Steam Frame patches | 1 |
|
||||
| [Declare Meta-only permissions](#frame-meta_permissions) | `frame.meta_permissions` | Steam Frame patches | 7 |
|
||||
| [Meta XR Audio: skip Quest telemetry class](#frame-metaxr_telemetry) | `frame.metaxr_telemetry` | Steam Frame patches | default |
|
||||
| [Clear android:debuggable](#frame-nodebug) | `frame.nodebug` | Steam Frame patches | 3 |
|
||||
| [Oculus OS telemetry class stubs](#frame-oculusos) | `frame.oculusos` | Steam Frame patches | 17 |
|
||||
@@ -73,20 +75,21 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
|
||||
| [Platform compat (ovrMessageType_ToString)](#frame-ovrplatformcompat) | `frame.ovrplatformcompat` | Steam Frame patches | default |
|
||||
| [Stub missing Meta platform functions](#frame-ovrstubs) | `frame.ovrstubs` | Steam Frame patches | default |
|
||||
| [Unpaired pointer-authentication checks](#frame-pac_hints) | `frame.pac_hints` | Steam Frame patches | 1 |
|
||||
| [Photon Voice: skip Android 12 audio routing](#frame-photon_voice_audio_route) | `frame.photon_voice_audio_route` | Steam Frame patches | |
|
||||
| [SDL apps: start without a clipboard service](#frame-sdl_clipboard) | `frame.sdl_clipboard` | Steam Frame patches | 5 |
|
||||
| [Photon Voice: skip Android 12 audio routing](#frame-photon_voice_audio_route) | `frame.photon_voice_audio_route` | Steam Frame patches | 1 |
|
||||
| [SDL apps: start without a clipboard service](#frame-sdl_clipboard) | `frame.sdl_clipboard` | Steam Frame patches | 6 |
|
||||
| [Stress Level Zero: let Unity start Vulkan itself](#frame-slz_vulkan_hooks) | `frame.slz_vulkan_hooks` | Steam Frame patches | 1 |
|
||||
| [Start straight in VR (skip the 2D launcher)](#frame-start_activity) | `frame.start_activity` | Steam Frame patches | |
|
||||
| [Allow swapchains larger than 4096 px](#frame-swapchain_limit) | `frame.swapchain_limit` | Steam Frame patches | default |
|
||||
| [Team Beef ports: treat the Frame as a Meta headset](#frame-tbxr_vendor) | `frame.tbxr_vendor` | Steam Frame patches | 1 |
|
||||
| [Unity App UI: no Android 12 vibrator](#frame-unity_appui_vibrator) | `frame.unity_appui_vibrator` | Steam Frame patches | |
|
||||
| [GL shim for Unity (no multisampled render-to-texture)](#frame-unity_gl_shim) | `frame.unity_gl_shim` | Steam Frame patches | 25 |
|
||||
| [GL shim for Unity (no multisampled render-to-texture)](#frame-unity_gl_shim) | `frame.unity_gl_shim` | Steam Frame patches | 27 |
|
||||
| [Unity: render each eye separately (no multiview)](#frame-unity_multipass) | `frame.unity_multipass` | Steam Frame patches | 2 |
|
||||
| [Unity: disable MSAA](#frame-unity_no_msaa) | `frame.unity_no_msaa` | Steam Frame patches | 10 |
|
||||
| [Unity: disable MSAA](#frame-unity_no_msaa) | `frame.unity_no_msaa` | Steam Frame patches | 11 |
|
||||
| [Unity: skip OVROverlay layers (fades, splash screens)](#frame-unity_no_overlay_copy) | `frame.unity_no_overlay_copy` | Steam Frame patches | 2 |
|
||||
| [Unity: start VR without Meta's system apps](#frame-unity_oculus_check) | `frame.unity_oculus_check` | Steam Frame patches | 14 |
|
||||
| [Unity: keep MSAA off at runtime (OVRManager)](#frame-unity_runtime_msaa_off) | `frame.unity_runtime_msaa_off` | Steam Frame patches | 31 |
|
||||
| [Make Unity text fields work](#frame-unity_text_input) | `frame.unity_text_input` | Steam Frame patches | 111 |
|
||||
| [Unity: keep MSAA off at runtime (OVRManager)](#frame-unity_runtime_msaa_off) | `frame.unity_runtime_msaa_off` | Steam Frame patches | 33 |
|
||||
| [Unity: skip Meta's player-in-room check](#frame-unity_scene_active_rooms) | `frame.unity_scene_active_rooms` | Steam Frame patches | 1 |
|
||||
| [Make Unity text fields work](#frame-unity_text_input) | `frame.unity_text_input` | Steam Frame patches | 122 |
|
||||
| [Unity: share thread keys (games that run out of them)](#frame-unity_tls_key_pool) | `frame.unity_tls_key_pool` | Steam Frame patches | 1 |
|
||||
| [Unity: headset always counts as worn](#frame-unity_user_presence) | `frame.unity_user_presence` | Steam Frame patches | 1 |
|
||||
| [GL shim for Unreal (no multisampled render-to-texture)](#frame-unreal_gl_shim) | `frame.unreal_gl_shim` | Steam Frame patches | 1 |
|
||||
@@ -98,10 +101,10 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
|
||||
| [Unreal: start without the OBB check](#frame-unreal_skip_obb_check) | `frame.unreal_skip_obb_check` | Steam Frame patches | 9 |
|
||||
| [Unreal: skip the OBB delivery check](#frame-unreal_skip_obb_delivery) | `frame.unreal_skip_obb_delivery` | Steam Frame patches | 1 |
|
||||
| [Unreal: bypass the stalled loading splash](#frame-unreal_splash_bypass) | `frame.unreal_splash_bypass` | Steam Frame patches | 1 |
|
||||
| [Unreal: thumb follows the capacitive touches](#frame-unreal_thumb_touch) | `frame.unreal_thumb_touch` | Steam Frame patches | 7 |
|
||||
| [Unreal: thumb follows the capacitive touches](#frame-unreal_thumb_touch) | `frame.unreal_thumb_touch` | Steam Frame patches | 8 |
|
||||
| [USB MIDI: don't poll for USB devices](#frame-usb_midi_watcher) | `frame.usb_midi_watcher` | Steam Frame patches | |
|
||||
| [Vermillion: paint canvases in the scene](#frame-vermillion_scene_canvas) | `frame.vermillion_scene_canvas` | Steam Frame patches | 1 |
|
||||
| [Vivox voice chat: skip Android 12 audio routing](#frame-vivox_audio_route) | `frame.vivox_audio_route` | Steam Frame patches | 2 |
|
||||
| [Vivox voice chat: skip Android 12 audio routing](#frame-vivox_audio_route) | `frame.vivox_audio_route` | Steam Frame patches | 3 |
|
||||
| [Vulkan: drop invalid render-pass pointers](#frame-vk_sanitize) | `frame.vk_sanitize` | Steam Frame patches | default · 1 |
|
||||
| [VrApi → OpenXR bridge](#frame-vrapi_bridge) | `frame.vrapi_bridge` | Steam Frame patches | 4 |
|
||||
| [Quiet Meta's VrApi loader (OpenXR games)](#frame-vrapi_stub) | `frame.vrapi_stub` | Steam Frame patches | 2 |
|
||||
@@ -111,8 +114,9 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
|
||||
| [Pointer angle](#adapter-aim_pitch) | `adapter.aim_pitch` | Game settings | |
|
||||
| [Pointer turn](#adapter-aim_yaw) | `adapter.aim_yaw` | Game settings | |
|
||||
| [Fix inside-out mixed reality](#adapter-alpha_blend_emul) | `adapter.alpha_blend_emul` | Game settings | |
|
||||
| [Use controllers](#adapter-controller_fix) | `adapter.controller_fix` | Game settings | 1 |
|
||||
| [Show Steam Frame controllers](#adapter-controller_models) | `adapter.controller_models` | Game settings | 6 |
|
||||
| [Use controllers](#adapter-controller_fix) | `adapter.controller_fix` | Game settings | 2 |
|
||||
| [Show Steam Frame controllers](#adapter-controller_models) | `adapter.controller_models` | Game settings | 7 |
|
||||
| [Show cube loading screens](#adapter-cube_quads) | `adapter.cube_quads` | Game settings | |
|
||||
| [Stand in for cube layers](#adapter-cube_standin) | `adapter.cube_standin` | Game settings | |
|
||||
| [Show curved menus and screens](#adapter-cylinder_strips) | `adapter.cylinder_strips` | Game settings | |
|
||||
| [Show 360° backgrounds and videos](#adapter-equirect_emul) | `adapter.equirect_emul` | Game settings | 7 |
|
||||
@@ -136,7 +140,7 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
|
||||
| [GL shim: high-precision vertex shaders](#adapter-gl_highp_markers) | `adapter.gl_highp_markers` | Game settings | 1 |
|
||||
| [GL shim: integer vertex inputs](#adapter-gl_int_attribs) | `adapter.gl_int_attribs` | Game settings | 1 |
|
||||
| [Flat panel diagnostics (log)](#adapter-gl_mv_debug) | `adapter.gl_mv_debug` | Game settings | |
|
||||
| [Fix freezes when controllers vibrate](#adapter-haptic_fix) | `adapter.haptic_fix` | Game settings | 96 |
|
||||
| [Fix freezes when controllers vibrate](#adapter-haptic_fix) | `adapter.haptic_fix` | Game settings | 107 |
|
||||
| [Vibration strength](#adapter-haptic_scale) | `adapter.haptic_scale` | Game settings | |
|
||||
| [Turn off Quest Vulkan foveation](#adapter-hide_fb_foveation) | `adapter.hide_fb_foveation` | Game settings | |
|
||||
| [Turn off space warp](#adapter-hide_space_warp) | `adapter.hide_space_warp` | Game settings | 5 |
|
||||
@@ -146,20 +150,23 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
|
||||
| [Vulkan format workaround](#adapter-mutable_fix) | `adapter.mutable_fix` | Game settings | |
|
||||
| [Unity frame loop: wait for skipped frames](#adapter-ovrp_begin_gate) | `adapter.ovrp_begin_gate` | Game settings | 1 |
|
||||
| [Steadier hands (older Unity)](#adapter-ovrp_hold_physics) | `adapter.ovrp_hold_physics` | Game settings | 1 |
|
||||
| [See your room](#adapter-passthrough_emul) | `adapter.passthrough_emul` | Game settings | |
|
||||
| [See your room](#adapter-passthrough_emul) | `adapter.passthrough_emul` | Game settings | 2 |
|
||||
| [Steadier head tracking](#adapter-pose_consistency) | `adapter.pose_consistency` | Game settings | 2 |
|
||||
| [Pose timing diagnostics (log)](#adapter-pose_debug) | `adapter.pose_debug` | Game settings | |
|
||||
| [Fix hands lagging behind controllers](#adapter-pose_time_fix) | `adapter.pose_time_fix` | Game settings | 15 |
|
||||
| [Fix hands lagging behind controllers](#adapter-pose_time_fix) | `adapter.pose_time_fix` | Game settings | 17 |
|
||||
| [Treat newer Quest controllers as Touch](#adapter-profile_remap) | `adapter.profile_remap` | Game settings | |
|
||||
| [Animate fingers from touch](#adapter-proximity_emul) | `adapter.proximity_emul` | Game settings | |
|
||||
| [Controller buttons](#adapter-quest_buttons) | `adapter.quest_buttons` | Game settings | |
|
||||
| [Keep pictures in bounds](#adapter-rect_clamp) | `adapter.rect_clamp` | Game settings | |
|
||||
| [Refresh rate](#adapter-refresh_rate) | `adapter.refresh_rate` | Game settings | 3 |
|
||||
| [Wait for the game's GPU](#adapter-release_wait) | `adapter.release_wait` | Game settings | |
|
||||
| [Re-create play space](#adapter-respace_kick) | `adapter.respace_kick` | Game settings | |
|
||||
| [Render resolution (supersampling)](#adapter-scale) | `adapter.scale` | Game settings | 9 |
|
||||
| [Render resolution (supersampling)](#adapter-scale) | `adapter.scale` | Game settings | 15 |
|
||||
| [Room depth](#adapter-scene_depth) | `adapter.scene_depth` | Game settings | |
|
||||
| [Pretend room layout](#adapter-scene_emul) | `adapter.scene_emul` | Game settings | 12 |
|
||||
| [Use room layout](#adapter-scene_emul) | `adapter.scene_emul` | Game settings | 18 |
|
||||
| [Room height](#adapter-scene_height) | `adapter.scene_height` | Game settings | |
|
||||
| [Hide play-area size](#adapter-scene_hide_bounds) | `adapter.scene_hide_bounds` | Game settings | 1 |
|
||||
| [Scan when requested](#adapter-scene_scan_request) | `adapter.scene_scan_request` | Game settings | |
|
||||
| [Room width](#adapter-scene_width) | `adapter.scene_width` | Game settings | |
|
||||
| [Eye-image snapshots (diagnostics)](#adapter-snapshot) | `adapter.snapshot` | Game settings | |
|
||||
| [Keep menus in place](#adapter-stable_local) | `adapter.stable_local` | Game settings | 1 |
|
||||
@@ -171,6 +178,8 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
|
||||
| [Retry refused picture formats](#adapter-swapchain_fix) | `adapter.swapchain_fix` | Game settings | |
|
||||
| [Steady controller input after focus](#adapter-sync_guard) | `adapter.sync_guard` | Game settings | 3 |
|
||||
| [Hide Vulkan foveation](#adapter-vk_hide_fdm) | `adapter.vk_hide_fdm` | Game settings | 1 |
|
||||
| [Without Steam's shader cache layer](#adapter-vk_no_fossilize) | `adapter.vk_no_fossilize` | Game settings | 1 |
|
||||
| [Skip refused graphics](#adapter-vk_pipeline_fallback) | `adapter.vk_pipeline_fallback` | Game settings | 1 |
|
||||
| [Vulkan pipeline failures (log)](#adapter-vk_pipeline_log) | `adapter.vk_pipeline_log` | Game settings | |
|
||||
| [Stop objects popping in and out](#adapter-vk_query_slots) | `adapter.vk_query_slots` | Game settings | |
|
||||
| [Vulkan shader dump (diagnostics)](#adapter-vk_shader_dump) | `adapter.vk_shader_dump` | Game settings | |
|
||||
@@ -188,7 +197,7 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
|
||||
| [Game language](#device-language) | `device.language` | Files and settings on the Frame | |
|
||||
| [Extra Lepton settings](#device-lepton_env) | `device.lepton_env` | Files and settings on the Frame | 3 |
|
||||
| [Controllers as a gamepad (Steam Input)](#device-steam_gamepad) | `device.steam_gamepad` | Files and settings on the Frame | |
|
||||
| [Show the app's Android window (for typing)](#device-text_input_window) | `device.text_input_window` | Files and settings on the Frame | 114 |
|
||||
| [Show the app's Android window (for typing)](#device-text_input_window) | `device.text_input_window` | Files and settings on the Frame | 122 |
|
||||
| [Launch options (SteamVR or OpenXR)](#pcvr-launch_args) | `pcvr.launch_args` | PC VR (Proton and Revive) | 2 |
|
||||
| [Provide the Oculus runtime from Revive (Frame)](#pcvr-libovr_redirect) | `pcvr.libovr_redirect` | PC VR (Proton and Revive) | |
|
||||
| [No Unreal crash reporter](#pcvr-no_crash_reporter) | `pcvr.no_crash_reporter` | PC VR (Proton and Revive) | |
|
||||
@@ -433,7 +442,7 @@ Stops some Unreal games from closing themselves right after starting.
|
||||
- `patch_remove_unreal_force_quit`
|
||||
- **Changes:** Changes the APK (during the OVRPort conversion).
|
||||
- **Offered (off):** Built as the alternate APK for Unreal games (use it if the game quits itself).
|
||||
- **Catalog games (45):** Asgard's Wrath 2, BARTENDER VR SIMULATOR, Breathedge VR, Carve Snowboarding, Creed, Dance Central, Espire 1: VR Operative (Quest Edition), Espire 2 and 37 more.
|
||||
- **Catalog games (46):** Asgard's Wrath 2, BARTENDER VR SIMULATOR, Breathedge VR, Carve Snowboarding, Creed, Dance Central, Espire 1: VR Operative (Quest Edition), Espire 2 and 38 more.
|
||||
- **Suggested by launch-test signatures:**
|
||||
- `force-quit` (error): The game quit itself right after starting.
|
||||
- **Source:** [patches/overport.py](../src/frameport/patches/overport.py)
|
||||
@@ -524,6 +533,7 @@ FramePort's layer between the game and the Steam Frame: fills in what the Frame
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** Required for every OpenXR/overport build on the Frame.
|
||||
- **Suggested by launch-test signatures:**
|
||||
- `batman-audio-geometry-allocation` (fatal): Batman's Meta XR Audio geometry loader requested tens of gigabytes while loading a scene.
|
||||
- `cube-swapchain-refused` (fatal): The game asked for a cube-map swapchain (for example an OVROverlay skybox or loading cube), which the Frame's runtime doesn't have.
|
||||
- `cylinder-menu-layers-rejected` (info): The runtime rejected a frame's layers once (XR_ERROR_LAYER_INVALID).
|
||||
- `graphics-requirements-missing` (fatal): The game can't start VR on the Frame; rebuild it with the current FramePort.
|
||||
@@ -606,6 +616,7 @@ Gets Cortopia games (for example Escaping Wonderland) past their loading screen.
|
||||
- `frame.cortopia_org_id`
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** Cortopia's Meta platform start-up waits for an org-scoped user id that OVRPort's platform library never answers (stuck loading screen).
|
||||
- **Catalog games (1):** Spectre.
|
||||
- **Suggested by launch-test signatures:**
|
||||
- `cortopia-org-id-wait` (error): The game waits forever on its loading screen for an answer from Meta's platform (an org-scoped user id, used as its analytics id) that OVRPort's platform library doesn't give.
|
||||
- **Source:** [patches/frame/cortopia_org_id.py](../src/frameport/patches/frame/cortopia_org_id.py)
|
||||
@@ -659,6 +670,7 @@ Lets games with Google Play services code start on the Frame (they'd crash at st
|
||||
- `frame.gms_platform_version` · also for 2D apps
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** Google Play services' Android 12 checks were built in as 'always true'; on Lepton (Android 11) the game would crash at start.
|
||||
- **Catalog games (1):** Table Troopers.
|
||||
- **Suggested by launch-test signatures:**
|
||||
- `gms-android12-call` (fatal): Google Play services' code in the game called an Android 12 method (the game was built for Android 12 and newer, so its version checks were built in as "always true") and Lepton runs Android 11; the game crashes on its GoogleApiHandler thread at start (for example Titan Isles).
|
||||
- **Source:** [patches/frame/gms_platform_version.py](../src/frameport/patches/frame/gms_platform_version.py)
|
||||
@@ -686,7 +698,7 @@ Plays the game's videos on the Frame's video hardware, so 4K and 8K video runs s
|
||||
- `frame.hw_video_decode` · also for 2D apps
|
||||
- **Changes:** Install only: written on the Frame when the game is installed; the APK isn't changed.
|
||||
- **Suggested (on):** The game plays video through Android's decoders (MediaCodec, ExoPlayer or VLC): decode it on the Frame's hardware.
|
||||
- **Catalog games (31):** 4XVR Video Player, AugmentedEmpire, Batman: Arkham Shadow, Battlemarked, BattleTalent, Breathedge VR, Dance Central, Exploding Kittens VR and 23 more.
|
||||
- **Catalog games (35):** 4XVR Video Player, AugmentedEmpire, Batman: Arkham Shadow, Battlemarked, BattleTalent, Breathedge VR, Dance Central, Exploding Kittens VR and 27 more.
|
||||
- **Source:** [patches/frame/hw_video_decode.py](../src/frameport/patches/frame/hw_video_decode.py)
|
||||
|
||||
> Decodes the game's videos on the Frame's hardware video decoder instead of Android's software decoders, which stutter or fall behind with 4K/8K video (for example video players, Batman: Arkham Shadow's cutscenes). Adds FramePort's codec plugin (OMX.frameport.avc/hevc/vp9.decoder) to this game's container only; the APK isn't changed. When the hardware is busy (for example Steam's own hardware video decoding is on) the video is decoded in software as before.
|
||||
@@ -741,6 +753,29 @@ Lets games with a verified older Lua engine get past a startup crash on the Fram
|
||||
|
||||
> Older xLua/LuaJIT engines reject native pointers outside their 47-bit range, causing a startup error and sometimes a second crash during Unity shutdown (e.g. BattleTalent). Uses a Lua engine that preserves full pointer values while keeping the game's original xLua bindings and native helpers. Applied only to the verified compatible library build.
|
||||
|
||||
### <a id="frame-main_process"></a>Start the game in the app's main process
|
||||
|
||||
Keeps the game from being closed about a minute after it starts (it runs in a separate Android process the Frame doesn't watch).
|
||||
|
||||
- `frame.main_process` · on by default · also for 2D apps
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** … runs in its own process, which Lepton doesn't watch: it would close the game about a minute in.
|
||||
- **Source:** [patches/frame/manifest_fixes.py](../src/frameport/patches/frame/manifest_fixes.py)
|
||||
|
||||
> Some apps run their game activity in a process of its own (android:process, e.g. Halo CE VR's ":halo_game"), started from a 2D launcher activity. Lepton starts the game activity directly and watches only the app's main process, which then never exists: about a minute in, Lepton decides the app "did not start" and closes it, at whatever point the game has reached. This runs those activities in the main process (only their android:process value changes).
|
||||
|
||||
### <a id="frame-meta_depth_occlusion"></a>Unity: disable Meta depth occlusion
|
||||
|
||||
Keeps objects visible up close in mixed-reality games that would hide them behind your room using Meta's depth sensor, which the Frame lacks (for example Laser Dance).
|
||||
|
||||
- `frame.meta_depth_occlusion`
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Offered (off):** Uses Meta's Depth API occlusion (HARD_OCCLUSION / SOFT_OCCLUSION): turn on if objects vanish when you come close to them.
|
||||
- **Catalog games (1):** Laser Dance.
|
||||
- **Source:** [patches/frame/meta_depth_occlusion.py](../src/frameport/patches/frame/meta_depth_occlusion.py)
|
||||
|
||||
> The Frame has no depth sensor (XR_META_environment_depth), so the environment depth Meta's Depth API reads is never valid. Shaders using it then hide virtual objects near the player behind a room that isn't there (for example Laser Dance's lasers vanish when approached). Rewrites EnvironmentDepthManager's support/availability checks and its occlusion-keyword setter in libil2cpp.so (found with Cpp2IL), and renames the HARD_OCCLUSION / SOFT_OCCLUSION shader keywords in the game's asset files that contain them, so occlusion is never turned on. For Laser Dance's known build it also keeps the laser beams' materials and close-range LODs visible. Only for games with this symptom.
|
||||
|
||||
### <a id="frame-meta_permissions"></a>Declare Meta-only permissions
|
||||
|
||||
Declares Meta's permissions some mixed-reality games ask for, so they don't stop.
|
||||
@@ -748,7 +783,8 @@ Declares Meta's permissions some mixed-reality games ask for, so they don't stop
|
||||
- `frame.meta_permissions`
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** Mixed-reality game that needs the room model: its 'use spatial data' permission must be granted (for example Demeter).
|
||||
- **Catalog games (4):** Arkio, BattleTalent, Demeter, Real VR Fishing.
|
||||
- **Suggested (on):** Uses Meta's room or anchor APIs: their permissions must be granted, or the game never asks for the room (for example NOPE's MR mode).
|
||||
- **Catalog games (7):** Arkio, BattleTalent, Demeter, Laser Dance, Punch Fit, Real VR Fishing, Test Quest MR.
|
||||
- **Source:** [patches/frame/manifest_fixes.py](../src/frameport/patches/frame/manifest_fixes.py)
|
||||
|
||||
> Declares com.oculus.permission.* / horizonos.permission.* that the app uses, so Android grants them at install (for example the 'use spatial data' permission of mixed-reality games like Demeter).
|
||||
@@ -881,6 +917,7 @@ Stops games with Photon Voice chat from crashing on the Frame when voice chat st
|
||||
- `frame.photon_voice_audio_route` · also for 2D apps
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** Photon Voice calls Android 12 audio methods, which Lepton (Android 11) lacks, and crashes when voice chat starts.
|
||||
- **Catalog games (1):** Table Troopers.
|
||||
- **Suggested by launch-test signatures:**
|
||||
- `photon-voice-api31` (fatal): The game's Photon Voice microphone plugin calls Android 12 audio-routing methods (AudioManager communication devices) without checking the Android version; Lepton runs Android 11, so the app crashes when voice chat starts (for example Table Troopers at the tutorial).
|
||||
- **Source:** [patches/frame/photon_voice_audio_route.py](../src/frameport/patches/frame/photon_voice_audio_route.py)
|
||||
@@ -894,7 +931,7 @@ Lets SDL and LÖVE games start on the Frame (they'd crash looking for a clipboar
|
||||
- `frame.sdl_clipboard` · also for 2D apps
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** SDL app: it crashes at start in Lepton, which has no clipboard service.
|
||||
- **Catalog games (5):** Crisis Zone VR, Majora's Mask VR, Time Crisis II VR (Test), Time Crisis VR (Experimental), WiiCompiled VR.
|
||||
- **Catalog games (6):** Crisis Zone VR, Majora's Mask VR, PrimedGun v2, Time Crisis II VR (Test), Time Crisis VR (Experimental), WiiCompiled VR.
|
||||
- **Suggested by launch-test signatures:**
|
||||
- `sdl-no-clipboard` (fatal): The app crashed at start because the Frame's Android has no clipboard.
|
||||
- **Source:** [patches/frame/sdl_clipboard.py](../src/frameport/patches/frame/sdl_clipboard.py)
|
||||
@@ -973,7 +1010,7 @@ Stops a gray or frozen screen in Unity games that turn on anti-aliasing while ru
|
||||
- `frame.unity_gl_shim`
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** Unity GLES game that may switch to MSAA at runtime: hide multisampled render-to-texture, which crashes the Frame's GL driver (for example The Room VR).
|
||||
- **Catalog games (25):** Audica, BattleSisters, Beat Saber, COMPOUND, Cosmodread, Down the Rabbit Hole, Drunkn Bar Fight, End Space and 17 more.
|
||||
- **Catalog games (27):** Audica, BattleSisters, Beat Saber, COMPOUND, Cosmodread, Down the Rabbit Hole, Drunkn Bar Fight, End Space and 19 more.
|
||||
- **Suggested by launch-test signatures:**
|
||||
- `unreal-msrtt-crash` (fatal): The game's graphics crashed a few seconds after start.
|
||||
- **Source:** [patches/frame/vrapi_bridge.py](../src/frameport/patches/frame/vrapi_bridge.py)
|
||||
@@ -1000,7 +1037,7 @@ Turns off a smoothing setting that crashes some older Unity games on the Frame.
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** Legacy VrApi Unity game on GLES with MSAA (… quality levels): multisampled render-to-texture hangs the Frame's GL driver (for example Sniper Elite VR).
|
||||
- **Offered (off):** GLES Unity game with MSAA on (… quality levels); enable if it hangs the GPU ('zink: DEVICE LOST').
|
||||
- **Catalog games (10):** Audica, BattleSisters, Eye of the Temple, I Expect You To Die 2, IEYTD3, Jurassic World Aftermath Collection, Mini Motor Racing X, Sniper Elite VR and 2 more.
|
||||
- **Catalog games (11):** Audica, BattleSisters, Eye of the Temple, I Expect You To Die 2, IEYTD3, Jurassic World Aftermath Collection, KillerFrequency, Mini Motor Racing X and 3 more.
|
||||
- **Suggested by launch-test signatures:**
|
||||
- `zink-device-lost` (fatal): The GPU hung, so the game froze or closed.
|
||||
- **Source:** [patches/frame/unity_no_msaa.py](../src/frameport/patches/frame/unity_no_msaa.py)
|
||||
@@ -1042,7 +1079,7 @@ Keeps a smoothing setting off that the game turns on itself and that can freeze
|
||||
- `frame.unity_runtime_msaa_off` · revision 2
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** GLES Unity game whose OVRManager turns 4x MSAA on at runtime: multisampled render-to-texture can hang the Frame's GPU.
|
||||
- **Catalog games (31):** Audica, BattleSisters, BattleTalent, Beat Saber, COMPOUND, Cosmodread, Down the Rabbit Hole, Drunkn Bar Fight and 23 more.
|
||||
- **Catalog games (33):** Audica, BattleSisters, BattleTalent, Beat Saber, COMPOUND, Cosmodread, Down the Rabbit Hole, Drunkn Bar Fight and 25 more.
|
||||
- **Suggested by launch-test signatures:**
|
||||
- `unity-render-crash` (fatal): The game's graphics crashed, so the picture freezes or stays gray.
|
||||
- `unity-runtime-msaa` (info): The game turns MSAA on at runtime, which can hang the Frame's GPU.
|
||||
@@ -1051,6 +1088,18 @@ Keeps a smoothing setting off that the game turns on itself and that can freeze
|
||||
|
||||
> Meta's OVRManager switches Unity to the headset's recommended MSAA level (4x) while the game runs, whatever its quality settings say (log: "Switching to the recommended level"). Multisampled render-to-texture on GLES can hang the Frame's GPU, up to a restart of the whole headset (for example Lucky's Tale in a menu) or a crash of its GL driver (for example The Room VR, whose own code sets 4x). Rewrites OVRDisplay.recommendedMSAALevel -> 0 and makes QualitySettings.antiAliasing's setter do nothing in libil2cpp.so (found with Cpp2IL), so MSAA stays off.
|
||||
|
||||
### <a id="frame-unity_scene_active_rooms"></a>Unity: skip Meta's player-in-room check
|
||||
|
||||
Lets a mixed-reality game load the room even if it thinks you stand outside it (for example Laser Dance asking again and again to scan your room).
|
||||
|
||||
- `frame.unity_scene_active_rooms`
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Catalog games (1):** Laser Dance.
|
||||
- **Source:** [patches/frame/unity_runtime.py](../src/frameport/patches/frame/unity_runtime.py)
|
||||
|
||||
> Meta's OVRSceneManager (ActiveRoomsOnly) loads only the room whose floor outline contains the player (IsUserInRoom). If the player is outside the emulated room's floor, loading stops with 'No scene model found' (for example Laser Dance with the play-area room). Rewrites OVRSceneManager.IsUserInRoom and PointInPolygon2D -> true in libil2cpp.so (found with Cpp2IL), so the room loads wherever the player stands. Only for games with this symptom: a scanned room around the player usually passes the check by itself.
|
||||
|
||||
### <a id="frame-unity_text_input"></a>Make Unity text fields work
|
||||
|
||||
Lets you type into this app's text fields on the Frame (they'd close at once).
|
||||
@@ -1058,7 +1107,7 @@ Lets you type into this app's text fields on the Frame (they'd close at once).
|
||||
- `frame.unity_text_input` · also for 2D apps
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** Unity app with text fields (…): they close at once on the Frame because it has no system keyboard.
|
||||
- **Catalog games (111):** AgeOfJoy, AllInOneSports, Ancient_Dungeon, Arcsmith, Arkio, Assassin's Creed Nexus, Attack on Titan VR: Unbreakable, Audica and 103 more.
|
||||
- **Catalog games (122):** AgeOfJoy, AllInOneSports, Ancient_Dungeon, Arcsmith, Arkio, Assassin's Creed Nexus, Attack on Titan VR: Unbreakable, Audica and 114 more.
|
||||
- **Source:** [patches/frame/unity_text_input.py](../src/frameport/patches/frame/unity_text_input.py)
|
||||
|
||||
> Unity's TMP_InputField / InputField wait for Android's on-screen keyboard and close themselves a frame later when there is none (Lepton has no on-screen keyboard; Meta's system keyboard does this on a Quest), so a selected text field only flashes a caret. Rewrites TouchScreenKeyboardShouldBeUsed -> false and isKeyboardUsingEvents (TMP) / InPlaceEditing (uGUI) -> true in libil2cpp.so, found per game with Cpp2IL (downloaded on first use), so fields stay selected and take key presses: Steam's keyboard (with "Show the app window") or Type on Frame from the PC.
|
||||
@@ -1216,7 +1265,7 @@ Makes the hands' thumbs follow what your thumb touches (they always pointed up:
|
||||
- `frame.unreal_thumb_touch`
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Catalog games (7):** Phantom: Covert Ops, Robo Recall, Star Wars: Tales from the Galaxy's Edge, Time Stall, Vader Immortal: Episode I, Vader Immortal: Episode II, Vader Immortal: Episode III.
|
||||
- **Catalog games (8):** Phantom: Covert Ops, Robo Recall, Star Wars: Tales from the Galaxy's Edge, The Wizards, Time Stall, Vader Immortal: Episode I, Vader Immortal: Episode II, Vader Immortal: Episode III.
|
||||
- **Source:** [patches/frame/unreal_thumb_touch.py](../src/frameport/patches/frame/unreal_thumb_touch.py)
|
||||
|
||||
> Unreal 4's Oculus input sets the ThumbUp axis (games animate the hand's thumb with it) from Meta's near-touch sensing, which the Frame never reports: the thumb always points up. Uses the touches instead (left X/Y/stick/thumb rest, right A/B/stick/thumb rest). Changes three instructions in OculusInput::SendControllerEvents, only where the code matches exactly (UE4 Oculus games of one engine generation, for example Vader Immortal, Robo Recall). Found by Klownicle (GitHub #49).
|
||||
@@ -1253,7 +1302,7 @@ Lets games with Vivox voice chat start on the Frame (they'd crash on an Android
|
||||
- `frame.vivox_audio_route` · also for 2D apps
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** Vivox voice chat: it calls Android 12 audio methods, which Lepton (Android 11) lacks, and crashes.
|
||||
- **Catalog games (2):** Dungeons of Eternity, Real VR Fishing.
|
||||
- **Catalog games (3):** Black Hole Pool, Dungeons of Eternity, Real VR Fishing.
|
||||
- **Suggested by launch-test signatures:**
|
||||
- `vivox-api31` (fatal): The game's Vivox voice-chat SDK calls Android 12 audio-routing methods (AudioManager communication devices) without checking the Android version; Lepton runs Android 11, so the app crashes (for example Green Hell VR).
|
||||
- **Source:** [patches/frame/vivox_audio_route.py](../src/frameport/patches/frame/vivox_audio_route.py)
|
||||
@@ -1317,7 +1366,7 @@ Repairs shaders that hang the graphics chip at certain effects, from the game's
|
||||
- **Catalog games (2):** Star Wars: Tales from the Galaxy's Edge, Vader Immortal: Episode I.
|
||||
- **Source:** [patches/frame/zink_shader_fix.py](../src/frameport/patches/frame/zink_shader_fix.py)
|
||||
|
||||
> Loads FramePort's shader-fix Vulkan layer into an OpenGL ES game. On the Frame, OpenGL ES runs on Zink (Mesa's GL on Vulkan); the layer sits between Zink and the driver and inserts the words of the adapter setting zink_shader_fix into the SPIR-V modules whose size and SHA-256 match (every other module is untouched), for example stores that set loop counters and accumulators a shader reads before writing, which hang the GPU (Vader Immortal's lightspeed jump; found by Klownicle, GitHub #49). Does nothing without zink_shader_fix (or zink_shader_dump) in the recipe. The layer adds itself to Android's Vulkan debug layer list for the game's process only.
|
||||
> Loads FramePort's shader-fix Vulkan layer into an OpenGL ES game. On the Frame, OpenGL ES runs on Zink (Mesa's GL on Vulkan); the layer sits between Zink and the driver and inserts the words of the adapter setting zink_shader_fix into the SPIR-V modules whose size and SHA-256 match (every other module is untouched), for example stores that set loop counters and accumulators a shader reads before writing, which hang the GPU (Vader Immortal's lightspeed jump; found by Klownicle, GitHub #49). Does nothing without zink_shader_fix (or zink_shader_dump) in the recipe, and either setting brings it in by itself. The layer adds itself to Android's Vulkan debug layer list for the game's process only.
|
||||
|
||||
## <a id="group-adapter"></a>Game settings
|
||||
|
||||
@@ -1403,7 +1452,7 @@ Turn off only for games you play with your hands instead of controllers.
|
||||
- **Options:**
|
||||
- Value: int, default `1` (always written).
|
||||
- Game settings → Controllers → "Use controllers".
|
||||
- **Catalog games (1):** Silhouette.
|
||||
- **Catalog games (2):** Laser Dance, Silhouette.
|
||||
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
|
||||
|
||||
> Reports Frame controllers as Oculus Touch and hides synthetic hand tracking. Set 0 for games that require hand tracking (for example Silhouette).
|
||||
@@ -1418,13 +1467,27 @@ For games that show the headset's own controllers. Takes effect after a reinstal
|
||||
- **Options:**
|
||||
- Value: int, default `0`.
|
||||
- Game settings → Controllers → "Show Steam Frame controllers".
|
||||
- **Catalog games (6):** Neko Atsume Purrfect, Ocean Rift, Real VR Fishing, SynthRiders, VR HOT Quest, Walkabout Mini Golf.
|
||||
- **Catalog games (7):** Hitstream, Neko Atsume Purrfect, Ocean Rift, Real VR Fishing, SynthRiders, VR HOT Quest, Walkabout Mini Golf.
|
||||
- **Suggested by launch-test signatures:**
|
||||
- `render-model-requested` (info): The game shows Quest controllers (or none) because the Frame doesn't provide controller models.
|
||||
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
|
||||
|
||||
> Games that ask the headset for its controller models (Meta's runtime controller models, XR_FB_render_model) get the Steam Frame controllers instead of Quest Touch controllers. The models come from the Frame's own SteamVR and are converted on the Frame at install time. Games that ship their own controller meshes aren't affected. Turning it on needs a rebuild (it adds a small library in front of OVRPort's loader).
|
||||
|
||||
### <a id="adapter-cube_quads"></a>Show cube loading screens
|
||||
|
||||
Shows 360° cube loading screens as six panels around you. Requires stand-in cube layers. Skipped while a 3D scene is submitted.
|
||||
|
||||
- `adapter.cube_quads`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
- Value: int, default `1`.
|
||||
- Game settings → Troubleshooting → "Show cube loading screens" (advanced).
|
||||
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
|
||||
|
||||
> Copies cube-map loading screens into six panels around the viewer (GLES and Vulkan). Mixed projection frames are skipped because the Frame draws panels above the scene. Requires cube_standin.
|
||||
|
||||
### <a id="adapter-cube_standin"></a>Stand in for cube layers
|
||||
|
||||
Keeps games running that use a 360° cube layer the Frame lacks (for example Budget Cuts Ultimate). Leave it on.
|
||||
@@ -1437,7 +1500,7 @@ Keeps games running that use a 360° cube layer the Frame lacks (for example Bud
|
||||
- Game settings → Troubleshooting → "Stand in for cube layers" (advanced).
|
||||
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
|
||||
|
||||
> The Frame's runtime has no cube-map layers and refuses their swapchains; OVRPlugin then crashes when it submits the next frame (for example Budget Cuts Ultimate). FrameBridge serves such a swapchain itself (a GL cube map, GLES games) and drops its layers, so the game runs on without that layer.
|
||||
> The Frame's runtime has no cube-map layers and refuses their swapchains; OVRPlugin then crashes when it submits the next frame (for example Budget Cuts Ultimate). FrameBridge serves such a swapchain itself on GLES and Vulkan; cube_quads displays its loading screens.
|
||||
|
||||
### <a id="adapter-cylinder_strips"></a>Show curved menus and screens
|
||||
|
||||
@@ -1772,7 +1835,7 @@ For games that freeze the Frame when a controller vibrates (for example Lucky's
|
||||
- **Options:**
|
||||
- Value: int, default `0`.
|
||||
- Game settings → Controllers → "Fix freezes when controllers vibrate" (advanced).
|
||||
- **Catalog games (96):** AgeOfJoy, Ancient_Dungeon, Attack on Titan VR: Unbreakable, Audica, AugmentedEmpire, Battlemarked, BattleSisters, BattleTalent and 88 more.
|
||||
- **Catalog games (107):** AgeOfJoy, Ancient_Dungeon, Attack on Titan VR: Unbreakable, Audica, AugmentedEmpire, Battlemarked, BattleSisters, BattleTalent and 99 more.
|
||||
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
|
||||
|
||||
> Routes OVRPlugin through FramePort's extension shim, which turns Meta's amplitude-envelope vibrations into plain ones: OVRPort's loader reads their nanosecond duration as seconds and allocates gigabytes, freezing the Frame when the game vibrates a controller (for example Lucky's Tale's save slots, GitHub #9). Needs a rebuild.
|
||||
@@ -1920,6 +1983,7 @@ Shows the Frame's cameras where the game expects passthrough.
|
||||
- **Options:**
|
||||
- Value: int, default `1`.
|
||||
- Game settings → Mixed reality → "See your room".
|
||||
- **Catalog games (2):** Laser Dance, Punch Fit.
|
||||
- **Suggested by launch-test signatures:**
|
||||
- `passthrough-missing` (error): The game needs Meta passthrough (the camera view), which the Frame lacks; the adapter emulates it.
|
||||
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
|
||||
@@ -1966,7 +2030,7 @@ For games whose hands trail the controllers when you move them (for example Batt
|
||||
- **Options:**
|
||||
- Value: int, default `0`.
|
||||
- Game settings → Controllers → "Fix hands lagging behind controllers" (advanced).
|
||||
- **Catalog games (15):** Battlemarked, BattleSisters, Beat Saber, Dance Central, Dungeons of Eternity, Mini Motor Racing X, OrbusVR, Phantom: Covert Ops and 7 more.
|
||||
- **Catalog games (17):** Battlemarked, BattleSisters, Beat Saber, Dance Central, Dungeons of Eternity, Mini Motor Racing X, OrbusVR, Phantom: Covert Ops and 9 more.
|
||||
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
|
||||
|
||||
> Poses a game asks for at a CLOCK_MONOTONIC time, or more than 0.5 s before the frame's display time, are located at the same moment in the runtime's time (or now). Meta's OVRPlugin passes the monotonic clock on as the OpenXR time, which on the Frame runs seconds behind the runtime's clock, so its "now" poses (Unity's physics step, controller poses) came from the past: hands trailing the controllers (for example BattleSisters, Sniper Elite VR).
|
||||
@@ -1999,6 +2063,20 @@ For games whose hands' thumbs never move when you touch the buttons or thumbstic
|
||||
|
||||
> Meta's OVRPlugin reports a thumb (or index finger) resting near the controller through XR_FB_touch_controller_proximity, which the Frame's runtime lacks: "near" stays false and games that animate the hands from it show a thumb that never moves (for example Vader Immortal; found by Klownicle, GitHub #49). Binds the game's proximity actions to the capacitive touch inputs instead: 1 = thumb (thumbstick, face buttons or thumb rest touched), 2 = also the index finger (trigger touched). Only when the runtime lacks the extension.
|
||||
|
||||
### <a id="adapter-quest_buttons"></a>Controller buttons
|
||||
|
||||
Choose which Quest Touch buttons the Frame's face buttons send. Takes effect at the next launch.
|
||||
|
||||
- `adapter.quest_buttons` · experimental
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
- Value: int, default `0`.
|
||||
- Game settings → Controllers → "Controller buttons": SteamVR default (`0`), Mirror (`1`), Quest pair (`2`).
|
||||
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
|
||||
|
||||
> Choose how the Frame's face buttons send Quest Touch inputs: 0 = SteamVR default, 1 = Mirror, 2 = Quest pair. Used at the next launch; SteamVR custom bindings can override it. Requires the runtime's XR_VALVE_frame_controller_interaction extension. Mapping: https://github.com/spoopyghosty0/frameport/blob/main/docs/CONTROLLERS.md
|
||||
|
||||
### <a id="adapter-rect_clamp"></a>Keep pictures in bounds
|
||||
|
||||
Stops games from hanging when their eye picture is a little too big or empty (for example PowerWash Simulator). Leave it on.
|
||||
@@ -2066,7 +2144,7 @@ Changes the game's render resolution. Higher is sharper but slower; 1.0× is the
|
||||
- **Options:**
|
||||
- Value: float, default `1.0` (always written).
|
||||
- Game settings → Picture → "Render resolution (supersampling)": slider 0.5–2 in steps of 0.1.
|
||||
- **Catalog games (9):** 4XVR Video Player, BlazeRush, CrazyTaxiVR, cubism, FGO VR, I Expect You To Die 2, The 7th Guest VR, Vampire: The Masquerade - Justice and 1 more.
|
||||
- **Catalog games (15):** 4XVR Video Player, BlazeRush, CrazyTaxiVR, cubism, FGO VR, I Expect You To Die 2, KillerFrequency, Spectre and 7 more.
|
||||
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
|
||||
|
||||
> Multiplies the recommended eye-buffer width and height (0.5–2.0). 1.5 ≈ 2.25× pixels.
|
||||
@@ -2085,23 +2163,23 @@ Changes the game's render resolution. Higher is sharper but slower; 1.0× is the
|
||||
|
||||
> Override the guardian depth (0 = use guardian, min 1.5 m).
|
||||
|
||||
### <a id="adapter-scene_emul"></a>Pretend room layout
|
||||
### <a id="adapter-scene_emul"></a>Use room layout
|
||||
|
||||
Mixed-reality games that build their level from your room get a simple room with walls.
|
||||
Scene games get your saved room. Without one, they get a simple room from the play area.
|
||||
|
||||
- `adapter.scene_emul`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested (on), value `1`:** Mixed-reality game that builds its level from the room model: emulate a guardian-sized room (for example Demeter).
|
||||
- **Suggested (on), value `1`:** The game asks Meta's system for your room layout (scene), which the Frame doesn't have: emulate a room from the play area (for example VR HOT's room setup).
|
||||
- **Suggested (on), value `1`:** The game uses Meta's scene or spatial anchor APIs: serve the saved room on the Frame, or a simple play-area room until one is scanned.
|
||||
- **Options:**
|
||||
- Value: int, default `0`.
|
||||
- Game settings → Mixed reality → "Pretend room layout".
|
||||
- **Catalog games (12):** Arkio, AugmentedEmpire, Battlemarked, BattleTalent, Demeo, Demeter, Ghostbusters: Rise of the Ghost Lord, Neko Atsume Purrfect and 4 more.
|
||||
- Game settings → Mixed reality → "Use room layout".
|
||||
- **Catalog games (18):** Arkio, AugmentedEmpire, Battlemarked, BattleTalent, cubism, Demeo, Demeter, Ghostbusters: Rise of the Ghost Lord and 10 more.
|
||||
- **Suggested by launch-test signatures:**
|
||||
- `mr-discovery-unsupported` (warning): The game cannot discover a Meta room.
|
||||
- `scene-missing` (warning): The game wants Meta's room data (scene API), which the Frame lacks.
|
||||
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
|
||||
|
||||
> Fake XR_FB_scene/spatial entities: a guardian-sized room with floor, ceiling and four walls, for mixed-reality games that build their level from the room (for example Demeter).
|
||||
> Serve Meta scene and spatial entity APIs from the saved room (walls, furniture and mesh). Without a saved room, use a simple play-area box (for example Demeter).
|
||||
|
||||
### <a id="adapter-scene_height"></a>Room height
|
||||
|
||||
@@ -2117,6 +2195,35 @@ Ceiling height of the pretend room.
|
||||
|
||||
> Ceiling height for scene_emul.
|
||||
|
||||
### <a id="adapter-scene_hide_bounds"></a>Hide play-area size
|
||||
|
||||
For games that reject the room because of the play area.
|
||||
|
||||
- `adapter.scene_hide_bounds`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
- Value: int, default `0`.
|
||||
- Game settings → Mixed reality → "Hide play-area size" (advanced); shown while `adapter.scene_emul` is on.
|
||||
- **Catalog games (1):** Demeter.
|
||||
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
|
||||
|
||||
> With scene_emul, the game's play-area (STAGE bounds) query answers "no play area". The Frame reports a 1x1 m area whose outline OVRPlugin can't give, and some Unity MR games then fail their guardian check (for example Demeter). Off by default: other games stalled without a play area.
|
||||
|
||||
### <a id="adapter-scene_scan_request"></a>Scan when requested
|
||||
|
||||
Scan your room in the headset when the game asks for a room scan.
|
||||
|
||||
- `adapter.scene_scan_request`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
- Value: int, default `1`.
|
||||
- Game settings → Mixed reality → "Scan when requested" (advanced); shown while `adapter.scene_emul` is on.
|
||||
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
|
||||
|
||||
> With scene_emul, let FramePort scan your room in the headset when the game requests a scan. If no scanner answers, use the saved room. Turn off to always use the saved room.
|
||||
|
||||
### <a id="adapter-scene_width"></a>Room width
|
||||
|
||||
0 = the size of your play area.
|
||||
@@ -2282,6 +2389,40 @@ For Unreal Engine 5 games that crash on the Frame (for example Metro Awakening).
|
||||
|
||||
> Vulkan shim (frame.vk_sanitize): the game doesn't see VK_EXT_fragment_density_map(2). Unreal Engine 5 turns on fragment-density-map foveation when the driver offers it and expects the density map from the headset, which the Frame doesn't provide: image views and barriers for a missing image, then a crash in the Frame's Vulkan driver (for example Metro Awakening). Valve's own foveation layer isn't affected.
|
||||
|
||||
### <a id="adapter-vk_no_fossilize"></a>Without Steam's shader cache layer
|
||||
|
||||
Some games can't create any graphics with it (for example Transformers Beyond Reality).
|
||||
|
||||
- `adapter.vk_no_fossilize`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
- Value: int, default `0`.
|
||||
- Game settings → Troubleshooting → "Without Steam's shader cache layer" (advanced).
|
||||
- **Catalog games (1):** Transformers Beyond Reality: Redux.
|
||||
- **Suggested by launch-test signatures:**
|
||||
- `vulkan-pipeline-refused` (fatal): The Frame's graphics driver refused to build some of the game's shaders, and Unreal ends the game at the first one (for example Transformers Beyond Reality).
|
||||
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
|
||||
|
||||
> Vulkan shim (frame.vk_sanitize): takes Lepton's Fossilize shader-cache layer out of the game's Vulkan layers (Android's debug layer list) when the engine loads its Vulkan library. With Fossilize loaded every graphics pipeline of Transformers Beyond Reality failed (VK_ERROR_OUT_OF_HOST_MEMORY), which Unreal treats as fatal (GitHub #349). Lepton has no switch for it; instances created before the engine loads the shim keep it.
|
||||
|
||||
### <a id="adapter-vk_pipeline_fallback"></a>Skip refused graphics
|
||||
|
||||
When the Frame's driver refuses one of the game's shaders, that object is left out instead of the game ending.
|
||||
|
||||
- `adapter.vk_pipeline_fallback`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
- Value: int, default `0`.
|
||||
- Game settings → Troubleshooting → "Skip refused graphics" (advanced).
|
||||
- **Catalog games (1):** Transformers Beyond Reality: Redux.
|
||||
- **Suggested by launch-test signatures:**
|
||||
- `vulkan-pipeline-refused` (fatal): The Frame's graphics driver refused to build some of the game's shaders, and Unreal ends the game at the first one (for example Transformers Beyond Reality).
|
||||
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
|
||||
|
||||
> Vulkan shim (frame.vk_sanitize): a graphics pipeline the Frame's driver refuses is created again with a fragment shader that writes zero, then with a vertex shader that only sets the position as well, so the draw shows nothing instead of Unreal ending with "Shader compilation failures are Fatal." (for example a few multiview vertex shaders in Transformers Beyond Reality, GitHub #349). Logged as "retried with a blank".
|
||||
|
||||
### <a id="adapter-vk_pipeline_log"></a>Vulkan pipeline failures (log)
|
||||
|
||||
Logs graphics pipelines the Frame's driver refuses. No effect on the game.
|
||||
@@ -2456,12 +2597,12 @@ Render less detail outside your gaze; adjust the full-resolution area or darken
|
||||
- **Options:**
|
||||
- `support`: bool, default `False` — Internal APK compatibility support
|
||||
- `mode`: str, default empty — Catalog default: tracked, fixed or off (empty = Valve's default)
|
||||
- `radius`: float, default `0.0`, 0–0.45 — Full-resolution radius; 0 keeps the current runtime default
|
||||
- `level`: str, default empty — Runtime profile: lo, med, hi (empty = Default)
|
||||
- `test`: bool, default `False` — Darken lower-resolution regions (diagnostic)
|
||||
- **Catalog games (5):** Ocean Rift, Quantum Void, Tea For God, The 7th Guest VR, Vampire: The Masquerade - Justice.
|
||||
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
|
||||
|
||||
> Reduces shading away from the full-resolution area, following each eye's gaze. Default area size keeps the runtime's current profile unchanged. Custom radius is a fraction of the shorter eye-image dimension; the transition ends at twice that radius. Larger areas retain more detail but save less GPU work. Fixed stops the area following your eyes. The optional test darkens actual coarse-shaded fragments. Runtime choices are in Game settings. Tested on RE4, Deadpool and Asgard's Wrath 2. The Climb 2 gets a version-guarded private bridge at 1.0 scale; Batman gets its hash-verified allocation fix (unsupported OVRPlugin versions are refused). Other games may need additional fixes; turn this off if artifacts appear.
|
||||
> Reduces shading away from the full-resolution area, following each eye's gaze. Default keeps the runtime profile unchanged. Low has the largest full-resolution center; medium and high use smaller centers and stronger peripheral detail reduction. Larger areas retain more detail but save less GPU work. Fixed stops the area following your eyes. The optional test darkens actual coarse-shaded fragments. Runtime choices are in Game settings. Tested on RE4, Deadpool and Asgard's Wrath 2. The Climb 2 gets a version-guarded private bridge at 1.0 scale; Batman gets its hash-verified allocation fix (unsupported OVRPlugin versions are refused). Other games may need additional fixes; turn this off if artifacts appear.
|
||||
|
||||
### <a id="device-headset_model"></a>Headset model
|
||||
|
||||
@@ -2520,9 +2661,13 @@ Lets the game use your controllers as a gamepad instead of a pointer, for 2D gam
|
||||
- `device.steam_gamepad` · also for 2D apps
|
||||
- **Changes:** Install only: written on the Frame when the game is installed; the APK isn't changed.
|
||||
- **Suggested (on):** 2D app that declares gamepad support (android.hardware.gamepad or Android TV's launcher): pass the controllers to it as a gamepad.
|
||||
- **Options:**
|
||||
- `swap_sticks`: bool, default `False` — Swap the sticks: the right stick moves through menus like the left one (for using one controller)
|
||||
- `swap_ab`: bool, default `False` — Swap A and B
|
||||
- `keys`: str, default empty — Own key layout lines (Android .kl), separated by ';', e.g. 'key 304 DPAD_DOWN' (A presses D-pad down) or 'key 305 BACK'
|
||||
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
|
||||
|
||||
> Lepton's Android only gets a keyboard, a pointer and touch from the Frame, so a 2D game never sees a controller (the controllers only move the pointer; SDL games show their touch controls). This passes Steam Input's virtual gamepad ("Microsoft X-Box 360 pad", made by Steam for the Frame's controllers and paired Bluetooth pads) into this app's container with an Xbox 360 key layout, and tells SDL games to use it (SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD). Steam Input must be on for the game's shortcut; the controllers must be on when the game starts (a pad Steam makes later isn't seen until the next start). The APK isn't changed. Off for one start: FRAMEPORT_NO_GAMEPAD=1 in the game's launch options.
|
||||
> Lepton's Android only gets a keyboard, a pointer and touch from the Frame, so a 2D game never sees a controller (the controllers only move the pointer; SDL games show their touch controls). This passes Steam Input's virtual gamepad ("Microsoft X-Box 360 pad", made by Steam for the Frame's controllers and paired Bluetooth pads) into this app's container with an Xbox 360 key layout, and tells SDL games to use it (SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD). Steam Input must be on for the game's shortcut; the controllers must be on when the game starts (a pad Steam makes later isn't seen until the next start). The APK isn't changed. Off for one start: FRAMEPORT_NO_GAMEPAD=1 in the game's launch options. Steam Input's own per-game remapping may not reach the app (GitHub #162); the options below change the buttons inside the app instead (applied with Update on Frame).
|
||||
|
||||
### <a id="device-text_input_window"></a>Show the app's Android window (for typing)
|
||||
|
||||
@@ -2531,7 +2676,7 @@ Lets typing reach this app from Steam's on-screen keyboard or your PC's keyboard
|
||||
- `device.text_input_window`
|
||||
- **Changes:** Install only: written on the Frame when the game is installed; the APK isn't changed.
|
||||
- **Suggested (on):** App with text fields: key presses only reach a shown Android window.
|
||||
- **Catalog games (114):** AFFECTED: The Manor, AgeOfJoy, AllInOneSports, Ancient_Dungeon, Arcsmith, Arkio, Assassin's Creed Nexus, Attack on Titan VR: Unbreakable and 106 more.
|
||||
- **Catalog games (122):** AFFECTED: The Manor, AgeOfJoy, AllInOneSports, Ancient_Dungeon, Arcsmith, Arkio, Assassin's Creed Nexus, Attack on Titan VR: Unbreakable and 114 more.
|
||||
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
|
||||
|
||||
> Lepton runs VR apps headless: their Android window is never shown, so it never gets keyboard focus and no key press (Steam's on-screen keyboard, a USB/Bluetooth keyboard, Type on Frame) reaches the app's text fields. With Lepton's lepton-show-flatscreen marker the window is shown (behind the VR view, which keeps working) and Steam's keyboard opens for a selected text field. Steam lists the shown window as "Gamescope" (the Frame's compositor). Pairs with "Make Unity text fields work without a system keyboard".
|
||||
|
||||
@@ -20,6 +20,7 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
|
||||
| Conversion fails with `NoSuchElementException: Sequence is empty` in `PatcherContext.appName` (GitHub #269, Accounting+) | OVRPort assumes an application label exists; some APKs only label their activity | FramePort retries that exact failure once using a temporary APK whose application inherits the activity's label; existing labels and the original APK are kept |
|
||||
| Updating a flat Android app after saving its recipe fails with `no OVRPort patches selected` (GitHub #271, Plezy) | older FramePort loads the saved recipe without restoring its unchanged-install setting or applying the flat-app build rules, then tries an empty VR conversion | update FramePort: saved recipes keep `as_is`, and flat apps still skip OVRPort; existing catalog-derived recipes refresh at the next app update. Until then, enable "Install as is" for an app whose APK needs no fixes |
|
||||
| A launch test suggests a patch (for example triage `sdl-no-clipboard` → `frame.sdl_clipboard`) but the game page doesn't show it / lists it as not applicable; or a game added long ago lacks warnings newer ones get (Android version, web wrapper, missing OBB) (GitHub #104, Dramatic Shape) | the library entry was analyzed by an older FramePort: the analysis field the patch reads (`sdl_java`, `min_sdk`, `web_wrapper`, `expects_obb`, …) is missing, so `applies()` is false | entries whose `analysis.extra.analysis_version` is below `detect.ANALYSIS_VERSION` are analyzed again at the GUI's start (background) and before a build (`pipeline.refresh_analyses`); by hand: game menu → "Analyze again", then Update on Frame. An unreadable APK is marked `analysis_failed` (retried after the next version bump). Adding an analysis field: bump `ANALYSIS_VERSION` |
|
||||
| Foveation full-resolution area looks identical at 10% and 45% (GitHub #394), while render resolution changes it | `FOVE_INNER`/`FOVE_OUTER` are neither passed through by Lepton nor read by Valve's layer; shared injection uses rectangular density presets | Game settings → Picture now exports named `FOVE_LEVEL=lo/med/hi`; Default preserves the runtime choice. Medium/high shrink the center and reduce peripheral density further. Legacy radius preferences map to the nearest old nominal preset. Compare Fixed + Darken coarse fragments with adapter `snapshot=N` at a fixed render resolution; see FRAME_RUNTIME.md for addresses and profile data. Agent launcher allowlist needs `FOVE_LEVEL`. |
|
||||
| FDM layer loads but diagnostic logging reports `fdm_injection_CreateInstance: skipped` | Layer loading alone does not enable injection on the tested SteamOS 0.4.5 build; a skipped helper instance alone also does not establish game-wide inactivity | Opt-in `device.foveation` mode `tracked`: loads Valve's layer and sets `FDM_DEBUG=enable`. Batman probe reached foveated render passes with changing per-eye offsets. Validate graphics and matched gameplay GPU time; do not enable globally. Use `debug` only for brief diagnostics, preserving existing flags; never benchmark with debug spam. See FRAME_RUNTIME.md. |
|
||||
| One eye (usually the right) shimmers or jitters in the 3D view while menus and panels look steady; pacing is clean (GitHub #69, e.g. RTCWQuest, Moss) | Valve's eye-tracked foveation layer (`VALVE_fdm_injection`) moves its low-density regions with each eye's gaze, and the right eye's gaze filter restarts often (`CEyePoseUKF R: Large dt … Bootstrapped` in `~/.local/share/Steam/logs/eyetracking.txt`) | Game settings → Picture → "Foveated rendering (Valve)": **Fixed** (`FDM_DEBUG=disable_offsets`; verify injection remains active on the installed runtime) or **Off** (`VK_INSTANCE_LAYERS=""`); catalog field `foveation:` |
|
||||
| With `device.foveation` Eye-tracked/Fixed, the sharp area looks right but everything around it is black with scattered coloured blocks (menus on quads too); pacing fine (GitHub #303, Medieval Dynasty; Vampire Survivors reported the same) | Unity's Oculus XR plugin asks for SUBSAMPLED eye images (`XR_META_vulkan_swapchain_create_info`, `VK_IMAGE_CREATE_SUBSAMPLED_BIT_EXT`); under Valve's FDM their coarse areas stay in the compact subsampled layout, which the compositor reads as an ordinary image. `foveation_fix=1` only dropped the `XrSwapchainCreateInfoFoveationFB` nodes and passed the META node on. With `foveation_fix=0` + `foveation_storage_fix=1` the adapter logs `foveation_storage_fix: shared allocation SUBSAMPLED cleared` for the eye swapchains | `device.foveation` (tracked/fixed) now also sets `foveation_storage_fix=1`, which clears SUBSAMPLED whatever `foveation_fix` is (generic version of Batman's OVRPlugin allocation fix). Rebuild + reinstall (new adapter). Headless-checked only (72 fps, bit cleared); picture needs the headset |
|
||||
@@ -31,6 +32,7 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
|
||||
| A Unity Quest game stays on a loading screen or doesn't go on; `An unknown error occurred. Request failed.` from `Oculus.Platform.Request`1:OnComplete` (GitHub #346 Eleven, #360 Spectre, #368 Thief Simulator; triage `meta-request-failed`) | A Meta platform call the game waits for got no usable answer: OVRPort's platform library answers some calls without Meta's services (request id 0) and Meta's Unity wrapper drops the callback | Find the call in the stack (the game's own coroutine after `OnComplete`); a per-game fix that skips it when it isn't needed to play (e.g. an analytics id request). Calls the game needs online stay unsupported |
|
||||
| Game aborts at start: `OVRPlatform-Loader: DisplayErrorAndExit: Failed to launch SystemActivities` after an `ActivityNotFoundException` stack (GitHub #357 Breachers; triage `meta-system-activities`) | The game ships its own renamed copy of Meta's platform loader (e.g. `libovr_custom.so`), which bypasses OVRPort's dispatcher and tries to open Meta's system activities (Horizon OS) | None in general: such games are online games built on Meta's services. Serving OVRPort's dispatcher under that name is possible but unlikely to make them playable |
|
||||
| Missing sound or extra content in a game whose OBB folder has separate `*.pak` files (GitHub #61, Star Wars: Tales from the Galaxy's Edge: seasons + Wwise banks) | the game asks Meta's platform for these asset files (`ovr_AssetFile_GetList` / `DownloadById`); OVRPort's loader doesn't list them | copy every content/sound-bank PAK and use `frame.asset_files` (native/langpack lists the data's `*.pak` as installed `default` assets, IAP `free`, and answers DownloadById at once with the path; off: env FRAMEPORT_ASSET_FILES=0). Tales also needs the splash and depth-size fixes below |
|
||||
| Quest saves need a cloud folder or backups after playing (discussion #352) | Steam does not sync Quest saves | Settings → Data: choose a writable local cloud-sync folder and optionally enable **Back up saves when a game closes**, then connect to apply it. The Frame keeps three changed snapshots per game; FramePort imports them on connection/poll. Manual **Back up saves** is next to Play; Restore stays in the menu. Old backup folders remain available and are never moved silently |
|
||||
| A SideQuest backup has `apk/` and a sibling `obb/` with content/sound-bank PAKs, but the Frame only has the main OBB (GitHub #61, Tales from the Galaxy's Edge) | older scans treated that layout as loose expansion files and selected only the named OBBs; the asset-file patch then found no PAKs | the scanner chooses the OBB folder by version first, then keeps the whole sibling data folder when all its expansion files are selected for this APK; app updates repair affected library entries and refresh asset/language-pack detection. Reinstall to copy the missing files, or scan the backup again first. Folders with other packages or unselected OBB versions still send only the selected OBBs |
|
||||
| Tales from the Galaxy's Edge stops during map loading (GitHub #61): game thread in `FlushRenderingCommands` / `UEngine::LoadMap`, render thread in `FSplash::RenderFrame_RenderThread` / `ovrp_WaitToBeginFrame` / SteamVR `WaitFrame` | the separate Oculus loading splash waits for an unbegun frame while map travel waits for rendering | `frame.unreal_splash_bypass`: returns from the splash renderer, with a checked symbol, size and prologue. The last image stays visible during loading; the normal renderer and menu layers continue. Headset test with the depth-size fix: gameplay, both eyes, controller input and sound work |
|
||||
| Upper part of the intro/environment is black or clipped, but a separate menu panel is intact (GitHub #61, Tales from the Galaxy's Edge) | `FSceneRenderTargets::AllocMobileMultiViewDepth` caches the initial Android window dimensions (1920x1016/1020), leaving the depth attachment shorter than the eye color texture (1728x1728); drawing is limited by the smaller attachment | `frame.unreal_multiview_depth`: refreshes both cached depth dimensions from the current scene `BufferSize` before allocation. Checks the exported symbol, function size and surrounding instructions, then replaces only the branch that skips refresh. Keeps multiview and multisampling. Headset test: full view and gameplay work |
|
||||
@@ -58,6 +60,7 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
|
||||
| The game quits right after start: `Something failed to initialize. Quitting!` / `We don't have write permission to …/files/cloud/data` (for example SUPERHOT); or stays at loading with `UnauthorizedAccessException` in `StartupLoading.Start` (GitHub #239, Jurassic World Aftermath) | The game created a cloud-save folder with mode 1700; inside Lepton the app writes through the folder's group. Its startup routine can fail before the permission watchdog repairs the newly created folder | launch.sh `fix_perms` adds owner + group write to every folder in the game's storage (agent 41). Close and start the game again without uninstalling: the folders are now repaired (Aftermath confirmed in the headset). Agent 84 creates `files/cloud/data` before `fix_perms` and startup, including in upgraded launchers, to prevent the first-start race. Triage `startup-save-permission` fails the launch test despite rendering frames |
|
||||
| The whole Frame slows to a halt, then the game is killed (`Out of memory: Killed process … anon-rss:10+ GB`), for example Lucky's Tale | The game's own memory grows ~1 GB/s on Unity's `Loading.PreloadManager` thread (perf page-fault profile); not the flat window, not the text-input patches | none yet: mark unsupported; profile with `perf record -p <pid> -e page-faults --call-graph fp` (works as steamos, `perf_event_paranoid` 2) |
|
||||
| A Linux app exits at once: `cannot execute binary file: Exec format error` in launch.log | an x86_64 program started directly (installed before FramePort ran x86 builds through FEX, or by hand) | reinstall it: x86_64 Linux apps install FEX on the Frame and start through it (agent v61, `kind: linux_x86`; FEX also needs `STEAM_COMPAT_DATA_PATH`: "No compat data path?" in launch.log = an agent v60 launcher, reinstall); triage `linux-x86-no-fex`. Prefer an arm64 build when the app offers one |
|
||||
| The game closes about a minute after starting, at a different spot each time; `Start proc …:<pkg>:<name> for pre-top-activity`, then `Target app (<pkg>) did not start` (GitHub #178) | The started activity runs in its own process (`android:process`); Lepton watches only the main process (triage `app-separate-process`) | `frame.main_process` (automatic) |
|
||||
| `APP_ACTIVITY is empty`, nothing starts | Manifest has category INFO only; Lepton needs LAUNCHER | `frame.launcher` (automatic) |
|
||||
| PC VR game on the Frame shows as a flat window / Revive: `Unable to load LibOVRRT DLL` / `LoaderInstance::CreateInstance chained CreateInstance call failed` | Frame SteamVR runtime rejects OpenXR apiVersion 1.1 (`XR_ERROR_API_VERSION_UNSUPPORTED`), which Proton 11's VR helper requests | `pcvr.xr_timefix` (Frame OpenXR layer, default on): retries xrCreateInstance as 1.0 |
|
||||
| Unreal PC VR game on the Frame runs as a flat window although OpenXR works (no `LogHMD` OVRPlugin lines; Unreal logs nothing when it skips the Oculus plugin) / launch.log: `FramePort oculushmd: could not create the OculusHMDConnected event` | UE's OculusHMD (and LibOVR's `ovr_Detect`) only start when the Windows event `OculusHMDConnected` exists and is signaled; on a PC the Oculus service creates it. Revive hooks `OpenEventW` for it, but that relies on Detours patching Wine's (ARM64EC) kernelbase | `pcvr.oculus_unreal` (default for Unreal Rift games; PC VR counterpart of overport's `patch_oculus_unreal`): launch.sh runs the injector through `fp_oculushmd.exe`, which provides the real event until the game exits |
|
||||
@@ -107,6 +110,7 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
|
||||
| `xrConvert…TimeKHR` FUNCTION_UNSUPPORTED spam; VrApi bridge stalls before recenter | runtime lacks timespec conversion | current adapter emulates it |
|
||||
| Crash on the first frame, backtrace `libVkLayer_fossilize.so` ← `FVulkanRenderPass::FVulkanRenderPass` (crash logcat) | the engine leaves the pNext of unused Vulkan attachment references uninitialized; Lepton always loads Steam's Fossilize layer (guest is userdebug, so even a non-debuggable APK gets it), which follows the pointer | `frame.vk_sanitize` (default on for Unreal; Vulkan shim `libfp_vk.so`; triage `fossilize-renderpass`), for example Deadpool VR |
|
||||
| SIGABRT right after start, backtrace `libopenxr_loader.so (xrCreateSwapchain+…)` | overport's dispatcher aborts on swapchains > 4096 px ("Wrong createInfo size"); video players use 7680×3840 theater textures | `frame.swapchain_limit` (default on; guard → 16384 px; triage `swapchain-size-abort`), for example 4XVR |
|
||||
| Cube loading screen missing/frozen, `cube_standin: dropped a layer (type=1000006000)` (HardBullet #385, Budget Cuts #107) | Runtime refuses cube swapchains and composites only quad/projection layers | Rebuild with `cube_standin=1`, `cube_quads=1` (defaults): mip-0 cube faces become six inward quads. Pools reuse six swapchains per size/format, with array slices for distinct cubes. Cubes are conservatively skipped in any frame containing a projection, since Frame draws quads above it. Allocation/copy failure falls back to dropping with `cube_quads:` diagnostics. Host-tested; headset visuals pending |
|
||||
| Video/menu panel missing, log `dropped N unusable layer(s)` with cylinder (1000017000) or equirect2 (1000091000) layers | the Frame runtime lacks XR_KHR_composition_layer_cylinder/equirect(2)/cube (its SteamVR runtime only composites quad + projection layers) | adapter shows cylinders as flat strips (setting `cylinder_strips`, default on); 360° equirect layers (theaters, 360° videos): `adapter.equirect_emul` (GLES only, per game) |
|
||||
| 360° theater / 360° video black or missing (for example 4XVR) | equirect layers dropped | `adapter.equirect_emul=1`: a worker thread with a shared GLES context converts each 360° image to a cube map when it changes and draws one adapter projection layer from it for every frame with exactly that frame's views; it replaces the 360° layers in place, so the game's own projection layer (balcony, controllers, 360° videos it draws itself) stays on top. Quads can't be used: the Frame draws quad layers above every projection layer whatever the order. Log: `first 360 view ready`, `in 5 s: N 360 image update(s) … M view redraw(s)`. Upside down / mirrored / behind you: `equirect_flip` 1/2/4 (`frameport settings <pkg> equirect_flip=1`, no rebuild). Never Vulkan (a Vulkan renderer hung the Frame's GPU in AC Nexus) |
|
||||
| Fades to black don't show: you see the level loading behind a loading screen (Batman: Arkham Shadow, GitHub #197); with `layer_debug=1` launch.log shows `color scale 0.00 0.00 0.00 …` on the projection layer | the game fades by dimming its eye image with XR_KHR_composition_layer_color_scale_bias; whether the Frame's runtime applies it isn't confirmed in the headset yet | `adapter.fade_quad=1` (opt-in, `native/adapter/fade_quad.c`): FrameBridge adds a black, alpha-blended quad right after the dimmed projection layer (VIEW space, 1 m ahead, 6×6 m; opacity = 1 − scale in 16 steps from one 8×8 swapchain refilled only when the step changes: Vulkan clear / GLES clear; one swapchain per step hit XR_ERROR_LIMIT_REACHED in Batman), log `fade_quad: drawing fades over the picture`; `fade_quad=2` also removes the layer's own scale (if both show, the fade is doubled) |
|
||||
@@ -122,11 +126,12 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
|
||||
| PC VR game judders/stutters although the GPU keeps up; vrcompositor.txt "Timed out. N total" high or frames dropped | the game misses the headset's refresh (for example 96 Hz = 10.4 ms; Stormland's slow frames take 10.7 ms) | `pcvr.steamvr_tuning` (default on): next Play sets SteamVR per-app `preferredRefreshRate` + `motionSmoothingOverride` via `fp_vrsettings.exe` |
|
||||
| Quest app keeps recentering / snapping the view when you turn your head; log shows session state 5→4→3→4→5 within a second | the Frame briefly takes focus; the app recenters on focus changes | adapter `focus_hold` (on by default; an earlier global debounce made AC Nexus stay black and was removed) |
|
||||
| Video player / app finds no local videos; MediaProvider "Requested path /home/steamos/... doesn't appear under ..." | Lepton's /sdcard is a symlink to a host path, so Android's media index rejects every file | upload in the Files tab (Videos = /sdcard/Movies) and browse folders in the app |
|
||||
| Batman: Arkham Shadow closes during smoke-bomb effects; later input-tree corruption | Meta XR Audio Wwise deletes queued metadata still referenced by the current audio frame, then writes through the stale pointer | FrameBridge automatically retires metadata after current-frame references disappear, for the verified AArch64 SDK build only; see [AUDIO_METADATA.md](AUDIO_METADATA.md) |
|
||||
| Batman: Arkham Shadow closes during smoke-bomb effects; later input-tree corruption | Meta XR Audio Wwise deletes queued metadata still referenced by the current audio frame, then writes through the stale pointer | FrameBridge automatically retires metadata after current-frame references disappear, for the verified AArch64 SDK build only, using a single-word entry branch and nearby stub (hook skipped if no stub fits); see [AUDIO_METADATA.md](AUDIO_METADATA.md) |
|
||||
| Batman: Arkham Shadow aborts loading a save/scene, with a 26–147 GB allocation through `MetaXRAudioUnityMemoryAllocator.cpp` | The asynchronous audio geometry task retains a native view without its owning UnityWebRequest; source bytes can be reused before parsing | Update Batman on Frame: the adapter copies geometry before background dispatch and releases it after the original worker completes, only for the verified Batman IL2CPP/Meta SDK builds; see [AUDIO_GEOMETRY.md](AUDIO_GEOMETRY.md) |
|
||||
| Game stays on a loading or "Waiting" box although it reached FOCUSED; log `FrameBridge: xrEndFrame failed -25` then no more `pacing:` lines (for example PowerWash Simulator on some Frames, GitHub #39) | The eye image rect ends a few pixels past its swapchain (Unity rounds the swapchain width, the runtime's recommended size differs per Frame); SteamVR rejects every frame with XR_ERROR_SWAPCHAIN_RECT_INVALID | FrameBridge clamps every projection/quad image rect to its swapchain (`rect_clamp`, default on since 0.11.0; log `rect_clamp: view N image rect clamped`): rebuild + reinstall the game |
|
||||
| Black from the start, `projection view 0: ... rect=0,0 0x0` then `xrEndFrame failed -25` twice and OVRPlugin "Unable to submit Oculus frame" (BattleTalent, GitHub #293; right after Unity's "Switching to the recommended level" MSAA line) | The game submits its first projection views with an empty image rect; SteamVR rejects them and OVRPlugin gives up submitting | `rect_clamp` replaces an empty rect with the whole swapchain image (log `rect_clamp: view N empty image rect -> full swapchain WxH`): rebuild + reinstall. Check the build output for a WARN from the Unity IL2CPP fixes (`frame.unity_runtime_msaa_off` etc. "code not found in this game"): turned on, they may still change nothing |
|
||||
| Black flash, then the SteamVR home environment while the game keeps running (sound plays); vrserver.txt `[OpenXR] SetXrInputConfig failed. Failed to load manifest provided by SetXrInputConfig`, then the app "has exited" and reconnects as `VRApplication_OpenXRInstance`; headless launch tests pace at 10 fps (for example Scared By Squares, GitHub #323) | Two controller actions in one action set share a name (Unreal: an action mapping and an axis mapping both called "Turn"). The Frame's runtime accepts the duplicate but can't load the input manifest it builds and drops the app as a scene app | FrameBridge gives the later duplicate a free name (`action_dedup`, default on; log `action_dedup: action "turn" ("Turn") already exists in its set: created as "turn_2"`): rebuild + reinstall the game. `input_diag=1` logs every action set and action the game creates |
|
||||
| Unity game (OVRPlugin, GLES or Vulkan) crashes ~10 s in: `FrameBridge: xrCreateSwapchain … faces=6 … result=-2`, OVRPlugin `CreateSwapchain for eye 0: 0x0, 0 stages`, then render-thread SIGSEGV in memset ← libOVRPlugin ← `ovrp_EndFrame4` (libgallium/ANR follow; for example Budget Cuts Ultimate, GitHub #107); triage `cube-swapchain-refused` | The game shows a cube-map layer (OVROverlay cubemap); the Frame's runtime has no cube layers and refuses cube swapchains (XR_ERROR_RUNTIME_FAILURE); OVRPlugin doesn't check and writes into the image list it never got | FrameBridge `cube_standin` (default on): a refused cube swapchain is served as a GL cube map in the game's context or a device-local Vulkan cube image transitioned to COLOR_ATTACHMENT_OPTIMAL, and its layers are dropped (log `cube_standin: runtime refused …`): rebuild + reinstall. Vulkan path covers UNDERDOGS (GitHub #296); host-tested, headset verification pending |
|
||||
| Unity game (OVRPlugin, GLES or Vulkan) crashes ~10 s in: `FrameBridge: xrCreateSwapchain … faces=6 … result=-2`, OVRPlugin `CreateSwapchain for eye 0: 0x0, 0 stages`, then render-thread SIGSEGV in memset ← libOVRPlugin ← `ovrp_EndFrame4` (libgallium/ANR follow; for example Budget Cuts Ultimate, GitHub #107); triage `cube-swapchain-refused` | The game shows a cube-map layer (OVROverlay cubemap); the Frame's runtime has no cube layers and refuses cube swapchains (XR_ERROR_RUNTIME_FAILURE); OVRPlugin doesn't check and writes into the image list it never got | FrameBridge `cube_standin` (default on): a refused cube swapchain is served as a GL cube map in the game's context or a device-local Vulkan cube image transitioned to COLOR_ATTACHMENT_OPTIMAL, and `cube_quads` displays cube-only loading screens as six quads (log `cube_standin: runtime refused …`): rebuild + reinstall. Vulkan path covers UNDERDOGS (GitHub #296); host-tested, headset verification pending |
|
||||
| Game (Unity, GLES) freezes, `zink: DEVICE LOST` | multisampled render-to-texture hangs the GPU | `frame.unity_no_msaa`; if it persists: unfixable → PC version |
|
||||
| Unreal GLES game crashes ~3 s after start: `Fatal signal 11 … fault addr 0x10000 in tid … (RHIThread)`, `#01 … libgallium_dri.so` (GitHub #83 Star Wars Pinball VR, triage `unreal-msrtt-crash`) | Unreal's mobile MSAA renders through multisampled render-to-texture; Zink's `find_rp_state` then indexes `rendering_state_cache[6]` one past its end (sample count ≥ 32) and calls a junk hash function (0x10000). Mesa bug (no bounds check) | `frame.unreal_gl_shim` (GL shim with `gl_hide_msrtt`, multiview kept for Unreal: Unreal 4.25 only enables multiview with GL_OVR_multiview, GL_OVR_multiview2 **and** GL_OVR_multiview_multisampled_render_to_texture, so the shim keeps the last one visible for Unreal and maps glFramebufferTextureMultisampleMultiviewOVR to the single-sampled glFramebufferTextureMultiviewOVR; log `multiview multisampled render-to-texture (N samples) drawn single-sampled`) |
|
||||
| Crash with `SIGILL (ILL_ILLOPN)` and `*pc=0xd50323bf` (autiasp), for example on Unreal's HttpManager thread (GitHub #83 Star Wars Pinball VR, triage `pac-unpaired`) | The engine's OpenSSL ARMv8 assembly has autiasp without a matching paciasp (libUE4.so: 38 paciasp, 40 autiasp; Poly1305 NEON). Quest CPUs treat PAC hints as NOPs, the Frame's CPU checks them | `frame.pac_hints`: in a library with unpaired counts every paciasp/autiasp becomes a NOP (in place) |
|
||||
@@ -157,7 +162,7 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
|
||||
| Controllers do nothing (for example stuck on a setup screen); log: `xrSuggestInteractionProfileBindings … XR_ERROR_PATH_UNSUPPORTED` | the game only suggests bindings for Meta's newer profiles (Touch Plus / Touch Pro); the Frame's Android runtime knows `oculus/touch_controller`, not those | adapter `profile_remap` (default on): the bindings are suggested again for Touch (log: `controller profile … -> oculus/touch_controller`) |
|
||||
| Controllers "don't sync": virtual hands/controllers lie on the floor below you, no buttons (Unity OpenXR games that only suggest Touch Plus/Pro, e.g. Hotel Infinity #327); log with `input_diag=1`: only `meta/touch_controller_plus` (or Pro) suggested, `controller profile … -> oculus/touch_controller` | the runtime then reports `oculus/touch_controller` for each hand, a profile the game never suggested: Unity's OpenXR input creates no device for it | adapter `profile_remap` (default on) reports the game's own profile for such hands (log: `profile_remap: /user/hand/left reported as meta/touch_controller_plus`) |
|
||||
| Controllers "don't sync": hands on the floor below you, no buttons, in a Unity OpenXR game with Meta's OpenXR features (e.g. Spatial Zombies #328); log: `xrCreateAction: XR_ERROR_PATH_UNSUPPORTED` (input_diag: `xrCreateAction -> XR_ERROR_PATH_UNSUPPORTED (thumbstick)`), then `xrGetCurrentInteractionProfile: XR_ERROR_ACTIONSET_NOT_ATTACHED` | Meta's "Detached … Controller" devices add `/user/detached_controller_meta/left|right` to Unity's merged actions; the Frame has no XR_META_detached_controllers, the action fails and Unity attaches no action set (OVRPlugin's input goes with it) | adapter `profile_remap` (default on) leaves those paths out of actions and bindings (log: `profile_remap: action thumbstick without 2 detached-controller path(s)`) |
|
||||
| Some buttons do nothing or the wrong thing (for example a Quest game's X/Y on the Frame's left controller) | the game binds only Quest Touch and SteamVR maps Touch onto the Frame controllers with its own remap (Touch left X/Y land on the d-pad, right X and Y repeat B), or the runtime rejected a profile or an input call | turn on adapter `input_diag` (diagnostics, off by default; `frameport settings <pkg> input_diag=1`) and look for `input_diag:` lines: `bindings: <profile> accepted` or `unsupported: interaction profile <profile> -> <result>` with the rejected paths, `bindings: /user/hand/<hand> uses <profile>`, `unsupported: function <name>`, `unsupported: xrCreateAction -> <result> (<action>)` |
|
||||
| Some buttons do nothing or the wrong thing (for example a Quest game's X/Y on the Frame's left controller) | the game binds only Quest Touch and SteamVR maps Touch onto the Frame controllers with its own remap (Touch left X/Y land on the d-pad, right X and Y repeat B), or the runtime rejected a profile or an input call | choose **Game settings → Controllers → Controller buttons**: Mirror (`quest_buttons=1`) or Quest pair (`2`); `0` restores SteamVR default. **Settings → Controllers** sets the global default and can apply it to installed games without overrides. Used from the next launch; [mapping and limits](CONTROLLERS.md). For rejected bindings, turn on adapter `input_diag` (`frameport settings <pkg> input_diag=1`) and inspect `input_diag:` lines: accepted profiles, rejected paths and input calls. With a mode enabled, expect `quest_buttons: frame_controller bindings (mode N, K bindings, D dropped): <XrResult>` |
|
||||
| Unity game hangs or the whole Frame restarts (for example in a menu); log: `The current MSAA level is 0, but the recommended MSAA level is 4. Switching to the recommended level.` (for example Lucky's Tale) | Meta's OVRManager (`useRecommendedMSAALevel`) turns 4x MSAA on at runtime, past QualitySettings; multisampled render-to-texture on GLES/Zink hangs the GPU | `frame.unity_runtime_msaa_off` (Cpp2IL: `OVRDisplay.get_recommendedMSAALevel` → 0) |
|
||||
| Unity game: one eye shows only effects or gray, the other is fine (for example I Am Cat, Oculus XR Plugin on GLES) | the game's single-pass multiview rendering goes wrong for array slice 1 (the adapter submits both slices correctly) | `frame.unity_multipass` (Cpp2IL: `OculusSettings.GetStereoRenderingMode` → MultiPass); `swap_eyes=1` tells whether the game or the adapter is at fault |
|
||||
| Setup/intro loops every launch (Espire 2) | save folders created without write permission | launcher repairs permissions every 2 s (built in) |
|
||||
@@ -195,7 +200,16 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
|
||||
| Mixed reality inside out: the room shows on the game's objects, black where the room should be (Unreal, for example Star Wars: Beyond Victory, GitHub #329) | Unreal keeps 1 - coverage in the scene alpha and OVRPlugin chains XrCompositionLayerAlphaBlendFB (colour factors ONE_MINUS_SRC_ALPHA / SRC_ALPHA, `layer_debug` shows `n=1000041001(3,2/3,2)`) to the eye layer; the Frame's runtime ignores it | adapter `alpha_blend_emul=1` (default, Vulkan): inverts the alpha of the eye images in place while passthrough is emulated; logs `alpha_blend: inverting the alpha of swapchain=…` |
|
||||
| Quest Touch controllers (or none) shown in a game that uses Meta's runtime controller models | XR_FB_render_model missing on the Frame | adapter `controller_models=1`: the agent converts the Frame's SteamVR controller models to glb at install, the adapter serves them. Games with their own controller meshes aren't affected (none of the 34 catalog games use runtime models) |
|
||||
| `overportOXR: Unknown proc addr: xr…` for a function the adapter emulates | overport's dispatcher only forwards functions in its own table | route the plugin's lookups through `native/xrshim` (done for XR_FB_render_model when `controller_models=1`) |
|
||||
| MR game stuck waiting for room data (Demeter) | no Meta scene API | adapter `scene_emul=1` (+ `frame.meta_permissions`) |
|
||||
| MR game asks for a room scan | The game requested a new room through Meta scene capture | FramePort scans in the headset and loads the new room. `scene_scan_request=0` turns it off and uses the stored room. |
|
||||
| MR game stuck waiting for room data (e.g. Demeter) | No Meta scene API | `adapter.scene_emul=1` (+ `frame.meta_permissions`); Frame → Room → Use my play area, or import a scanned room |
|
||||
| Unity MR game rejects every room / its "use guardian" fallback does nothing; `ovrpBoundary_Outer is not supported`, `IndexOutOfRangeException` in a guardian check (e.g. Demeter's `RemoveObjectNotInGuardian`) | OVRPlugin calls the Frame's 1x1 m STAGE rect a configured boundary but can't give its outline | `adapter.scene_hide_bounds=1` (with `scene_emul`): FrameBridge then answers the app's STAGE `xrGetReferenceSpaceBoundsRect` with `XR_SPACE_BOUNDS_UNAVAILABLE` (log `scene: STAGE bounds hidden`). Per game only: NOPE stalled without a play area. Demeter also needs a floor of at least 2.5 x 2.5 m, as on a Quest |
|
||||
| MR game with `scene_emul` never asks for the room (no `scene: query`/`discovery` lines), black screen or MR mode with no room (e.g. NOPE, Pinball FX); `m_XR_META_boundary_visibility extension is not supported` every frame | Meta's scene/anchor permissions undeclared (Lepton can't grant them); boundary suppression unsupported | `frame.meta_permissions` (now with every `scene_emul` recipe of a USE_SCENE/USE_ANCHOR_API game, migration `scene_permissions`); FrameBridge emulates `XR_META_boundary_visibility` (log `scene: boundary suppressed`). `passthrough:` lines show the app's passthrough calls |
|
||||
| Build says `Unity text fields: not fixed: Cpp2IL couldn't start on this PC` (older versions: `Cpp2IL couldn't read this game's code: at Cpp2IL.Program.Main`) | .NET aborts without libicu (fresh WSL/Linux) | FramePort runs Cpp2IL with `DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1`; such failures aren't cached, so the next build retries |
|
||||
| MR uses an empty/simple room; FrameBridge `scene: capture requested -> guardian box` / `using guardian room`, `room from guardian`, or fallback box dimensions (`mr-no-room`) | No saved room, so FrameBridge uses its guardian box | Save a room in Frame → Room. A real SteamVR boundary supplies walls; Valve's default circle is refused. Import a scanner-built room for furniture/mesh; headset capture is not implemented yet |
|
||||
| `Unknown proc addr: xrDiscoverSpacesMETA` or MRUK `NoRoomsFound` (`mr-discovery-unsupported`) | Scene discovery is unavailable or no room was discovered | Enable `adapter.scene_emul`, rebuild/reinstall to get the updated extension shim, then save a room. All games declaring USE_SCENE or USE_ANCHOR_API now get this suggestion |
|
||||
| MR game: virtual objects (e.g. Laser Dance's lasers) vanish when you come close to them | Meta's Depth API occlusion: the Frame has no depth sensor (XR_META_environment_depth), so the environment depth the occlusion shaders read is never valid and near objects count as hidden behind the room | `frame.meta_depth_occlusion` (Unity IL2CPP; offered, not turned on, when the game has the HARD_OCCLUSION / SOFT_OCCLUSION keywords; analysis `meta_depth_occlusion`): EnvironmentDepthManager's checks -> false and its keyword setter does nothing (Cpp2IL), the keywords renamed in the asset files that contain them; Laser Dance's known build also gets its laser materials/LODs fixed. From PR #246; not checked in the owner's headset yet |
|
||||
| Unity MR game keeps asking for a room scan / `No scene model found` although FrameBridge served a room (OVRSceneManager with ActiveRoomsOnly) | OVRSceneManager loads only the room whose floor outline contains the player (IsUserInRoom); with the play-area room the player can stand outside it (FrameBridge logs the head against the floor outline: `scene: head in STAGE ... OUTSIDE the floor outline`) | Scan the room first (Frame → Room). Still stuck: `frame.unity_scene_active_rooms` (catalog or by hand, Unity IL2CPP scene games): IsUserInRoom / PointInPolygon2D -> true (Cpp2IL). From PR #246 (Laser Dance) |
|
||||
| MR game with many quads (21+): frames refused with `XR_ERROR_LAYER_LIMIT_EXCEEDED` (e.g. Punch Fit) | The runtime takes at most maxLayerCount (16) layers | FrameBridge keeps the projection layers and its own fade quad, then the app's other layers in submission order up to the limit (log `xrEndFrame: N layers exceed the runtime limit 16`). From PR #246 |
|
||||
| Hand-tracking game janky (Silhouette) | Frame synthesizes hands from controllers | `controller_fix=0` passes hands through; not really fixable |
|
||||
| Eye distortion while moving (Arcsmith, Time Stall) | unknown (not eye swap, tracking, Valve layers, depth or pacing) | unresolved |
|
||||
| The Climb 666.0: hand outlines ripple in both eyes while menus and gameplay work (GitHub #238) | Unknown. Sessions paced around 72 fps, controller tracking reported valid, and SteamVR motion smoothing was already off; these checks do not establish the cause | Unresolved after wearer comparisons on 2026-10-12: disabling Valve's Vulkan layers, setting `r_AntialiasingMode = 1`, and setting `r_AntialiasingMode = 0` with `r_MotionBlur = 0` each gave the same or worse result. These were config comparisons; runtime cvar values were not verified. Restored the playable configuration (`r_vr_foveated = 0` with the default Vulkan layers), retaining the startup/rendering crash fixes above. Do not keep the unsuccessful experimental settings as a fix |
|
||||
@@ -207,7 +221,7 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
|
||||
| Unity game shows a picture but the player body is frozen: no head tracking, the controllers stay on the model, no buttons, although OpenXR input works (BONELAB 1.2068, Oculus XR Plugin) | OVRPlugin reports the headset as not worn (`ovrp_GetUserPresent2` → 0) a few seconds after start, and the game's rig only follows a present user (Marrow `XRHMD.IsUserPresent` = Unity's HMD `UserPresence`) | `frame.unity_user_presence` (the Oculus XR Plugin's lookup → `libfp_ovrp.so`, reports present; log `user presence: OVRPlugin 0 …`) |
|
||||
| Unreal 5 game crashes at its first render pass, backtrace `vulkan.freedreno.so` ← `libVkLayer_fossilize.so` ← `libfp_vk.so (create_render_pass2)` (Metro Awakening) | a subpass without a depth attachment names a depth/stencil resolve attachment; the Frame's Turnip reads the missing depth attachment (NULL) | the Vulkan shim drops that resolve (always, FramePort after 0.11.0; log `dropped the depth resolve of subpass N (no depth attachment)`); triage `unreal-vulkan-driver-crash` |
|
||||
| Unreal game crashes before its first frame: SIGSEGV `fault addr 0x10` on the main thread, backtrace all `libUnreal.so` above the linker's `soinfo::call_constructors` (inside `System.loadLibrary`), right after `Parsing commandline` (GitHub #349, Transformers Beyond Reality) | The game is built with Unreal's Binned3 allocator, which reserves its whole range while the library loads: one `mmap(NULL, 76 GiB, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS)`. Quest grants it; the Frame's kernel (heuristic overcommit, mode 0) refuses a writable private mapping larger than RAM + swap, and Unreal's out-of-memory handler ("Ran out of memory allocating ...") dereferences its not-yet-created log | `frame.unreal_noreserve` (suggested from analysis `unreal_binned3`): every such anonymous read/write mmap in the engine library gets `MAP_NORESERVE` (one instruction each: `mov w3, #0x22` → `#0x4022`); triage `unreal-reserve-refused`. Found by disassembling the crash pc (the OOM handler; its UTF-16 format string) and the caller's `mov x0, #0x1300000000` |
|
||||
| Unreal Vulkan game fails on its first frames and OVRPlugin reports `foveationImage = 0x0, width = 0, height = 0` (for example Transformers Beyond Reality: Redux, #349) | Quest FB foveation remains advertised by OVRPort's dispatcher despite `foveation_fix=1`. This is a hypothesis for the failure, not a confirmed cause: Redux's alternate no-ForceQuit build dereferences a null graphics-pipeline-state object during Vulkan pipeline binding, not a density image | Diagnostic `adapter.hide_fb_foveation=1` (off by default, Vulkan): filters the dispatcher and runtime extension lists/instance requests, removes foveation create/image chains and refuses profile creation/updates. Rebuild with the adapter **and xrshim** artifacts, then compare first-frame survival and Unreal's original fatal log. Does not disable Valve FDM injection. Device verification pending |
|
||||
| Unreal Vulkan game ends on its first frames with "Shader compilation failures are Fatal." (tombstone abort message; `System.exit` / ForceQuit; the no-ForceQuit build dereferences a null pipeline instead), `vk_pipeline_log=1` shows `vkCreateGraphicsPipelines failed: -1` (for example Transformers Beyond Reality: Redux, #349) | With Lepton's Fossilize layer loaded **every** graphics pipeline failed with VK_ERROR_OUT_OF_HOST_MEMORY, even a constant-colour one; retries without the pipeline cache, without the app's CPU allocator, and without Valve's FDM/RPO layers all failed too. Without Fossilize a few multiview vertex shaders still fail in Turnip (a blank fragment shader alone doesn't help, blank vertex + fragment does) | `adapter.vk_no_fossilize=1` (Vulkan shim removes `VK_LAYER_fossilize` from GraphicsEnv's debug layer list when the engine loads it) + `adapter.vk_pipeline_fallback=1` (refused pipelines are made with blank shaders; logged "retried with a blank"); triage `vulkan-pipeline-refused`. Headless: runs at 72 fps; headset check pending. The `foveationImage = 0x0` warning (`hide_fb_foveation`) was ruled out |
|
||||
| Unreal 5 game crashes in `vulkan.freedreno.so` called straight from `libUnreal.so` right after its swapchains, or a few seconds into the first frames (Metro Awakening) | Unreal turns on fragment-density-map foveation because Turnip offers VK_EXT_fragment_density_map, then records barriers (to `FRAGMENT_DENSITY_MAP_OPTIMAL`) and image views (2D array R8G8) for a density map the Frame never provides (VK_NULL_HANDLE); Turnip dereferences it | adapter `vk_hide_fdm=1` (the game doesn't see the extension) + `vk_spec_fixes=1` (leaves out null-image barriers, refuses null-image views); triage `unreal-fdm-missing`. Found by disassembling the driver at the crash pc: the struct offsets (0x58 = VkSubpassDescription2, 0x48/0x60 = image barriers, viewType/format in registers) name the call |
|
||||
| Own-engine game crashes in `je_free` from its own library on the first frames (Roblox) | the game frees memory it doesn't own; not the Vulkan shim or the format fallback | unsupported |
|
||||
|
||||
|
||||
@@ -117,6 +117,10 @@ Hooks (`steps.HOOKS`) cover what a real Frame would do:
|
||||
- `pairing_done`: the Frame ran the setup command, so FramePort connects.
|
||||
- `live_stream`: a test picture through the real relay.
|
||||
- `monitor_details`, `type_tab`, `select_files` and `settings_section`.
|
||||
- `open_room` (the Room tab with the pretend Frame's room drawn) and `room_editing` (its editing cards: `cleanup`,
|
||||
`more`, `box`, `doors`, `suggestion`). The room is a made-up bedroom built in code (`fakes.fake_room`, never a real
|
||||
scan): a bed under a wall-mounted TV, a desk with two monitors under a window, a closet, a door, a lamp and a plant,
|
||||
with a noisy scanned-surface mesh and a clean-up report and labels that match it.
|
||||
|
||||
In a video, clicking Install runs the scripted install, and the game is then installed on the pretend Frame.
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 133 KiB |
@@ -0,0 +1,150 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1080" height="864" viewBox="0 0 1080 864" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Quest Touch buttons on Steam Frame controllers</title>
|
||||
<desc id="desc">Quest Touch reference and three Frame mappings: SteamVR default, Mirror, and Quest pair. Each Steam Frame controller button is labelled with the Quest Touch button it sends. A dash means it sends nothing. Left Touch buttons are X and Y; right are A and B.</desc>
|
||||
<rect width="1080" height="864" rx="16" fill="#0D0E12"/>
|
||||
<g font-family="DejaVu Sans,Arial,sans-serif">
|
||||
<text x="20" y="32" fill="#EDEEF2" font-size="24" font-weight="600" text-anchor="start">Controller buttons</text>
|
||||
<text x="20" y="57" fill="#A7ABB7" font-size="14" font-weight="400" text-anchor="start">Blue: X / A · Orange: Y / B or Menu · —: sends nothing</text>
|
||||
<g transform="translate(16 72)">
|
||||
<rect x="0" y="0" width="520" height="380" rx="12" fill="#16181E" stroke="#2A2D37"/>
|
||||
<text x="20" y="31" fill="#EDEEF2" font-size="20" font-weight="600" text-anchor="start">Quest Touch reference</text>
|
||||
<text x="20" y="55" fill="#A7ABB7" font-size="13" font-weight="400" text-anchor="start">Two face buttons per hand, plus Menu on the left</text>
|
||||
<text x="140" y="84" fill="#A7ABB7" font-size="13" font-weight="400" text-anchor="middle">Left</text>
|
||||
<circle cx="117" cy="206" r="25" fill="#1D1F27" stroke="#3AA8FF" stroke-width="1.5"/>
|
||||
<text x="117" y="212.84" fill="#3AA8FF" font-size="19" font-weight="700" text-anchor="middle">X</text>
|
||||
<circle cx="163" cy="151" r="25" fill="#1D1F27" stroke="#FF8A1F" stroke-width="1.5"/>
|
||||
<text x="163" y="157.84" fill="#FF8A1F" font-size="19" font-weight="700" text-anchor="middle">Y</text>
|
||||
<rect x="83" y="251" width="114" height="32" rx="8" fill="#1D1F27"/>
|
||||
<text x="140" y="272" fill="#A7ABB7" font-size="12" font-weight="400" text-anchor="middle">Menu</text>
|
||||
<text x="380" y="84" fill="#A7ABB7" font-size="13" font-weight="400" text-anchor="middle">Right</text>
|
||||
<circle cx="403" cy="206" r="25" fill="#1D1F27" stroke="#3AA8FF" stroke-width="1.5"/>
|
||||
<text x="403" y="212.84" fill="#3AA8FF" font-size="19" font-weight="700" text-anchor="middle">A</text>
|
||||
<circle cx="357" cy="151" r="25" fill="#1D1F27" stroke="#FF8A1F" stroke-width="1.5"/>
|
||||
<text x="357" y="157.84" fill="#FF8A1F" font-size="19" font-weight="700" text-anchor="middle">B</text>
|
||||
<text x="260.0" y="366" fill="#A7ABB7" font-size="12" font-weight="400" text-anchor="middle">Lower: X / A · Upper: Y / B</text>
|
||||
</g>
|
||||
<g transform="translate(544 72)">
|
||||
<rect x="0" y="0" width="520" height="380" rx="12" fill="#16181E" stroke="#2A2D37"/>
|
||||
<text x="20" y="31" fill="#EDEEF2" font-size="20" font-weight="600" text-anchor="start">SteamVR default</text>
|
||||
<text x="20" y="55" fill="#A7ABB7" font-size="13" font-weight="400" text-anchor="start">Steam Frame controllers: each button shows the Quest button it sends</text>
|
||||
<text x="140" y="78" fill="#A7ABB7" font-size="13" font-weight="400" text-anchor="middle">Left</text>
|
||||
<path d="M102,256 C104,314 114,334 136,338 C158,342 178,326 194,256 Z" fill="#0D0E12" stroke="#2A2D37" stroke-width="2"/>
|
||||
<circle cx="140" cy="186" r="100" fill="#0D0E12" stroke="#2A2D37" stroke-width="2"/>
|
||||
<circle cx="174" cy="204" r="24" fill="#16181E" stroke="#2A2D37"/>
|
||||
<circle cx="174" cy="204" r="15" fill="#1D1F27" stroke="#2A2D37"/>
|
||||
<circle cx="126" cy="248" r="10" fill="#1D1F27" stroke="#2A2D37"/>
|
||||
<text x="126" y="272" fill="#868A96" font-size="9" font-weight="400" text-anchor="middle">Steam</text>
|
||||
<rect x="131" y="135" width="26" height="10" rx="5" fill="#1D1F27" stroke="#FF8A1F"/>
|
||||
<text x="144" y="130" fill="#FF8A1F" font-size="10" font-weight="600" text-anchor="middle">View: Menu</text>
|
||||
<path d="M84,144 h28 v30 h30 v28 h-30 v30 h-28 v-30 h-30 v-28 h30 Z" fill="#1D1F27" stroke="#2A2D37"/>
|
||||
<text x="98" y="164.04" fill="#FF8A1F" font-size="14" font-weight="700" text-anchor="middle">Y</text>
|
||||
<text x="69" y="193.04" fill="#FF8A1F" font-size="14" font-weight="700" text-anchor="middle">Y</text>
|
||||
<text x="127" y="193.04" fill="#FF8A1F" font-size="14" font-weight="700" text-anchor="middle">Y</text>
|
||||
<text x="98" y="222.04" fill="#3AA8FF" font-size="14" font-weight="700" text-anchor="middle">X</text>
|
||||
<text x="380" y="78" fill="#A7ABB7" font-size="13" font-weight="400" text-anchor="middle">Right</text>
|
||||
<path d="M326,256 C328,314 338,334 360,338 C382,342 402,326 418,256 Z" fill="#0D0E12" stroke="#2A2D37" stroke-width="2"/>
|
||||
<circle cx="380" cy="186" r="100" fill="#0D0E12" stroke="#2A2D37" stroke-width="2"/>
|
||||
<circle cx="346" cy="204" r="24" fill="#16181E" stroke="#2A2D37"/>
|
||||
<circle cx="346" cy="204" r="15" fill="#1D1F27" stroke="#2A2D37"/>
|
||||
<circle cx="394" cy="248" r="10" fill="#1D1F27" stroke="#2A2D37"/>
|
||||
<text x="394" y="272" fill="#868A96" font-size="9" font-weight="400" text-anchor="middle">Steam</text>
|
||||
<rect x="363" y="135" width="26" height="10" rx="5" fill="#1D1F27" stroke="#868A96"/>
|
||||
<text x="376" y="130" fill="#868A96" font-size="10" font-weight="600" text-anchor="middle">Menu: —</text>
|
||||
<circle cx="422" cy="159" r="14" fill="#1D1F27" stroke="#FF8A1F" stroke-width="1.5"/>
|
||||
<text x="422" y="164.04" fill="#FF8A1F" font-size="14" font-weight="700" text-anchor="middle">B</text>
|
||||
<text x="437" y="149" fill="#868A96" font-size="9" font-weight="400" text-anchor="middle">Y</text>
|
||||
<circle cx="393" cy="188" r="14" fill="#1D1F27" stroke="#FF8A1F" stroke-width="1.5"/>
|
||||
<text x="393" y="193.04" fill="#FF8A1F" font-size="14" font-weight="700" text-anchor="middle">B</text>
|
||||
<text x="408" y="178" fill="#868A96" font-size="9" font-weight="400" text-anchor="middle">X</text>
|
||||
<circle cx="451" cy="188" r="14" fill="#1D1F27" stroke="#FF8A1F" stroke-width="1.5"/>
|
||||
<text x="451" y="193.04" fill="#FF8A1F" font-size="14" font-weight="700" text-anchor="middle">B</text>
|
||||
<text x="466" y="178" fill="#868A96" font-size="9" font-weight="400" text-anchor="middle">B</text>
|
||||
<circle cx="422" cy="217" r="14" fill="#1D1F27" stroke="#3AA8FF" stroke-width="1.5"/>
|
||||
<text x="422" y="222.04" fill="#3AA8FF" font-size="14" font-weight="700" text-anchor="middle">A</text>
|
||||
<text x="437" y="207" fill="#868A96" font-size="9" font-weight="400" text-anchor="middle">A</text>
|
||||
<text x="260.0" y="366" fill="#A7ABB7" font-size="12" font-weight="400" text-anchor="middle">Sticks, triggers and grips unchanged · bumpers send nothing</text>
|
||||
</g>
|
||||
<g transform="translate(16 468)">
|
||||
<rect x="0" y="0" width="520" height="380" rx="12" fill="#16181E" stroke="#2A2D37"/>
|
||||
<text x="20" y="31" fill="#EDEEF2" font-size="20" font-weight="600" text-anchor="start">Mirror</text>
|
||||
<text x="20" y="55" fill="#A7ABB7" font-size="13" font-weight="400" text-anchor="start">Steam Frame controllers: each button shows the Quest button it sends</text>
|
||||
<text x="140" y="78" fill="#A7ABB7" font-size="13" font-weight="400" text-anchor="middle">Left</text>
|
||||
<path d="M102,256 C104,314 114,334 136,338 C158,342 178,326 194,256 Z" fill="#0D0E12" stroke="#2A2D37" stroke-width="2"/>
|
||||
<circle cx="140" cy="186" r="100" fill="#0D0E12" stroke="#2A2D37" stroke-width="2"/>
|
||||
<circle cx="174" cy="204" r="24" fill="#16181E" stroke="#2A2D37"/>
|
||||
<circle cx="174" cy="204" r="15" fill="#1D1F27" stroke="#2A2D37"/>
|
||||
<circle cx="126" cy="248" r="10" fill="#1D1F27" stroke="#2A2D37"/>
|
||||
<text x="126" y="272" fill="#868A96" font-size="9" font-weight="400" text-anchor="middle">Steam</text>
|
||||
<rect x="131" y="135" width="26" height="10" rx="5" fill="#1D1F27" stroke="#FF8A1F"/>
|
||||
<text x="144" y="130" fill="#FF8A1F" font-size="10" font-weight="600" text-anchor="middle">View: Menu</text>
|
||||
<path d="M84,144 h28 v30 h30 v28 h-30 v30 h-28 v-30 h-30 v-28 h30 Z" fill="#1D1F27" stroke="#2A2D37"/>
|
||||
<text x="98" y="164.04" fill="#FF8A1F" font-size="14" font-weight="700" text-anchor="middle">Y</text>
|
||||
<text x="69" y="193.04" fill="#3AA8FF" font-size="14" font-weight="700" text-anchor="middle">X</text>
|
||||
<text x="127" y="193.04" fill="#FF8A1F" font-size="14" font-weight="700" text-anchor="middle">Y</text>
|
||||
<text x="98" y="222.04" fill="#3AA8FF" font-size="14" font-weight="700" text-anchor="middle">X</text>
|
||||
<text x="380" y="78" fill="#A7ABB7" font-size="13" font-weight="400" text-anchor="middle">Right</text>
|
||||
<path d="M326,256 C328,314 338,334 360,338 C382,342 402,326 418,256 Z" fill="#0D0E12" stroke="#2A2D37" stroke-width="2"/>
|
||||
<circle cx="380" cy="186" r="100" fill="#0D0E12" stroke="#2A2D37" stroke-width="2"/>
|
||||
<circle cx="346" cy="204" r="24" fill="#16181E" stroke="#2A2D37"/>
|
||||
<circle cx="346" cy="204" r="15" fill="#1D1F27" stroke="#2A2D37"/>
|
||||
<circle cx="394" cy="248" r="10" fill="#1D1F27" stroke="#2A2D37"/>
|
||||
<text x="394" y="272" fill="#868A96" font-size="9" font-weight="400" text-anchor="middle">Steam</text>
|
||||
<rect x="363" y="135" width="26" height="10" rx="5" fill="#1D1F27" stroke="#868A96"/>
|
||||
<text x="376" y="130" fill="#868A96" font-size="10" font-weight="600" text-anchor="middle">Menu: —</text>
|
||||
<circle cx="422" cy="159" r="14" fill="#1D1F27" stroke="#FF8A1F" stroke-width="1.5"/>
|
||||
<text x="422" y="164.04" fill="#FF8A1F" font-size="14" font-weight="700" text-anchor="middle">B</text>
|
||||
<text x="437" y="149" fill="#868A96" font-size="9" font-weight="400" text-anchor="middle">Y</text>
|
||||
<circle cx="393" cy="188" r="14" fill="#1D1F27" stroke="#FF8A1F" stroke-width="1.5"/>
|
||||
<text x="393" y="193.04" fill="#FF8A1F" font-size="14" font-weight="700" text-anchor="middle">B</text>
|
||||
<text x="408" y="178" fill="#868A96" font-size="9" font-weight="400" text-anchor="middle">X</text>
|
||||
<circle cx="451" cy="188" r="14" fill="#1D1F27" stroke="#3AA8FF" stroke-width="1.5"/>
|
||||
<text x="451" y="193.04" fill="#3AA8FF" font-size="14" font-weight="700" text-anchor="middle">A</text>
|
||||
<text x="466" y="178" fill="#868A96" font-size="9" font-weight="400" text-anchor="middle">B</text>
|
||||
<circle cx="422" cy="217" r="14" fill="#1D1F27" stroke="#3AA8FF" stroke-width="1.5"/>
|
||||
<text x="422" y="222.04" fill="#3AA8FF" font-size="14" font-weight="700" text-anchor="middle">A</text>
|
||||
<text x="437" y="207" fill="#868A96" font-size="9" font-weight="400" text-anchor="middle">A</text>
|
||||
<text x="260.0" y="366" fill="#A7ABB7" font-size="12" font-weight="400" text-anchor="middle">Sticks, triggers and grips unchanged · bumpers send nothing</text>
|
||||
</g>
|
||||
<g transform="translate(544 468)">
|
||||
<rect x="0" y="0" width="520" height="380" rx="12" fill="#16181E" stroke="#2A2D37"/>
|
||||
<text x="20" y="31" fill="#EDEEF2" font-size="20" font-weight="600" text-anchor="start">Quest pair</text>
|
||||
<text x="20" y="55" fill="#A7ABB7" font-size="13" font-weight="400" text-anchor="start">Steam Frame controllers: each button shows the Quest button it sends</text>
|
||||
<text x="140" y="78" fill="#A7ABB7" font-size="13" font-weight="400" text-anchor="middle">Left</text>
|
||||
<path d="M102,256 C104,314 114,334 136,338 C158,342 178,326 194,256 Z" fill="#0D0E12" stroke="#2A2D37" stroke-width="2"/>
|
||||
<circle cx="140" cy="186" r="100" fill="#0D0E12" stroke="#2A2D37" stroke-width="2"/>
|
||||
<circle cx="174" cy="204" r="24" fill="#16181E" stroke="#2A2D37"/>
|
||||
<circle cx="174" cy="204" r="15" fill="#1D1F27" stroke="#2A2D37"/>
|
||||
<circle cx="126" cy="248" r="10" fill="#1D1F27" stroke="#2A2D37"/>
|
||||
<text x="126" y="272" fill="#868A96" font-size="9" font-weight="400" text-anchor="middle">Steam</text>
|
||||
<rect x="131" y="135" width="26" height="10" rx="5" fill="#1D1F27" stroke="#FF8A1F"/>
|
||||
<text x="144" y="130" fill="#FF8A1F" font-size="10" font-weight="600" text-anchor="middle">View: Menu</text>
|
||||
<path d="M84,144 h28 v30 h30 v28 h-30 v30 h-28 v-30 h-30 v-28 h30 Z" fill="#1D1F27" stroke="#2A2D37"/>
|
||||
<text x="98" y="164.04" fill="#868A96" font-size="14" font-weight="700" text-anchor="middle">—</text>
|
||||
<text x="69" y="193.04" fill="#868A96" font-size="14" font-weight="700" text-anchor="middle">—</text>
|
||||
<text x="127" y="193.04" fill="#FF8A1F" font-size="14" font-weight="700" text-anchor="middle">Y</text>
|
||||
<text x="98" y="222.04" fill="#3AA8FF" font-size="14" font-weight="700" text-anchor="middle">X</text>
|
||||
<text x="380" y="78" fill="#A7ABB7" font-size="13" font-weight="400" text-anchor="middle">Right</text>
|
||||
<path d="M326,256 C328,314 338,334 360,338 C382,342 402,326 418,256 Z" fill="#0D0E12" stroke="#2A2D37" stroke-width="2"/>
|
||||
<circle cx="380" cy="186" r="100" fill="#0D0E12" stroke="#2A2D37" stroke-width="2"/>
|
||||
<circle cx="346" cy="204" r="24" fill="#16181E" stroke="#2A2D37"/>
|
||||
<circle cx="346" cy="204" r="15" fill="#1D1F27" stroke="#2A2D37"/>
|
||||
<circle cx="394" cy="248" r="10" fill="#1D1F27" stroke="#2A2D37"/>
|
||||
<text x="394" y="272" fill="#868A96" font-size="9" font-weight="400" text-anchor="middle">Steam</text>
|
||||
<rect x="363" y="135" width="26" height="10" rx="5" fill="#1D1F27" stroke="#868A96"/>
|
||||
<text x="376" y="130" fill="#868A96" font-size="10" font-weight="600" text-anchor="middle">Menu: —</text>
|
||||
<circle cx="422" cy="159" r="14" fill="#1D1F27" stroke="#868A96" stroke-width="1.5"/>
|
||||
<text x="422" y="164.04" fill="#868A96" font-size="14" font-weight="700" text-anchor="middle">—</text>
|
||||
<text x="437" y="149" fill="#868A96" font-size="9" font-weight="400" text-anchor="middle">Y</text>
|
||||
<circle cx="393" cy="188" r="14" fill="#1D1F27" stroke="#FF8A1F" stroke-width="1.5"/>
|
||||
<text x="393" y="193.04" fill="#FF8A1F" font-size="14" font-weight="700" text-anchor="middle">B</text>
|
||||
<text x="408" y="178" fill="#868A96" font-size="9" font-weight="400" text-anchor="middle">X</text>
|
||||
<circle cx="451" cy="188" r="14" fill="#1D1F27" stroke="#868A96" stroke-width="1.5"/>
|
||||
<text x="451" y="193.04" fill="#868A96" font-size="14" font-weight="700" text-anchor="middle">—</text>
|
||||
<text x="466" y="178" fill="#868A96" font-size="9" font-weight="400" text-anchor="middle">B</text>
|
||||
<circle cx="422" cy="217" r="14" fill="#1D1F27" stroke="#3AA8FF" stroke-width="1.5"/>
|
||||
<text x="422" y="222.04" fill="#3AA8FF" font-size="14" font-weight="700" text-anchor="middle">A</text>
|
||||
<text x="437" y="207" fill="#868A96" font-size="9" font-weight="400" text-anchor="middle">A</text>
|
||||
<text x="260.0" y="366" fill="#A7ABB7" font-size="12" font-weight="400" text-anchor="middle">Sticks, triggers and grips unchanged · bumpers send nothing</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 421 KiB After Width: | Height: | Size: 425 KiB |
|
Before Width: | Height: | Size: 505 KiB After Width: | Height: | Size: 506 KiB |
|
Before Width: | Height: | Size: 663 KiB After Width: | Height: | Size: 665 KiB |
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 217 KiB |
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 599 KiB |
|
Before Width: | Height: | Size: 594 KiB After Width: | Height: | Size: 596 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 91 KiB |
@@ -0,0 +1,47 @@
|
||||
# Quest controller mode: contract (`quest_buttons`)
|
||||
|
||||
This file is the contract between the native (FrameBridge) and app/docs halves of the feature. The mapping itself
|
||||
lives in [`quest_buttons.json`](quest_buttons.json); everything else follows it.
|
||||
|
||||
## Problem
|
||||
Quest games suggest bindings only for `/interaction_profiles/oculus/touch_controller`. The Steam Frame controllers
|
||||
are two gamepad halves (Valve: https://partner.steamgames.com/doc/steamhardware/steamframe/input):
|
||||
- left: `dpad_up|dpad_down|dpad_left|dpad_right` (click, touch), `view` (click, touch), `system`, `bumper`,
|
||||
`squeeze` (click, touch, value), `trigger` (click, touch, value), `thumbstick` (vector, x, y, click, touch),
|
||||
`grip/pose`, `aim/pose`, `output/haptic`
|
||||
- right: `a|b|x|y` (click, touch), `menu` (click, touch), `system`, `bumper`, and the same squeeze/trigger/thumbstick/
|
||||
poses/haptic.
|
||||
|
||||
SteamVR uses a game's Frame Controller binding first; without one it remaps Touch: Touch B/Y -> the three top buttons
|
||||
(left d-pad left/up/right, right X/Y/B), Touch A/X -> the bottom one, Touch menu -> View (mode `0` in the JSON).
|
||||
|
||||
## Setting
|
||||
FrameBridge adapter setting `quest_buttons` (int, read from settings like every other key; numbers only):
|
||||
- `0` = off (default): FrameBridge does nothing new, SteamVR's remap applies.
|
||||
- `1` = Mirror, `2` = Quest pair: see `modes` in the JSON.
|
||||
|
||||
Library setting `input.quest_buttons` (0/1/2, default 0) = the global default, used at install for games whose recipe
|
||||
doesn't name `adapter.quest_buttons`.
|
||||
|
||||
## Native behaviour (FrameBridge)
|
||||
1. At `xrCreateInstance`, when `quest_buttons` != 0 and the runtime lists `XR_VALVE_frame_controller_interaction`,
|
||||
add it to the enabled extensions (the adapter already rewrites this list). Not listed -> log and stay off.
|
||||
2. When the game's suggestion for the Touch profile succeeds (directly, or through `profile_remap`'s
|
||||
`remap_to_touch`), suggest a translated set for `/interaction_profiles/valve/frame_controller_valve`:
|
||||
- each binding `/user/hand/<hand>/input/<touch input>[/<component>]` maps by the JSON: `common` 1:1, button
|
||||
`x|y|a|b` + `/click` or `/touch` -> each listed Frame button + the same component, `menu/click` -> listed
|
||||
Frame button + `/click`; `output/haptic` 1:1; anything in `dropped` or unknown is left out.
|
||||
- a later Touch suggestion by the same game replaces the Frame set (suggest again; the last call per profile
|
||||
wins in OpenXR).
|
||||
- a failed Frame suggestion is logged and ignored: the game's own call result is never changed.
|
||||
3. `xrGetCurrentInteractionProfile`: with `quest_buttons` != 0, a `/valve/` answer is reported as Touch even when
|
||||
`controller_fix` is off.
|
||||
4. Log lines (logcat tag of the adapter):
|
||||
- `quest_buttons: extension XR_VALVE_frame_controller_interaction enabled` / `... not offered by the runtime`
|
||||
- `quest_buttons: frame_controller bindings (mode N, K bindings, D dropped): <XrResult>`
|
||||
- with `input_diag=1`: `input_diag: quest_buttons <touch path> -> <frame path>` per added binding.
|
||||
|
||||
## Owners
|
||||
- native stream: `native/adapter/*`, `tests/test_quest_buttons.py`, rebuilt adapter artifact.
|
||||
- app stream: `src/frameport/**` (settings, install fallback, UI, help), `docs/**` except this file and the JSON,
|
||||
`scripts/controller_diagram.py`, `README.md` link, `agent/frameport_agent.py` comment only, Python tests.
|
||||
@@ -0,0 +1,249 @@
|
||||
# App stream: Quest controller buttons
|
||||
|
||||
## Implementation
|
||||
|
||||
- Added `adapter.quest_buttons` (integer, default 0), common Controllers choice with SteamVR default / Mirror /
|
||||
Quest pair. It is available on both bridge ABIs, is not an XR shim setting and has no detection suggestion.
|
||||
No agent, patch, setting or analysis revision was changed.
|
||||
- `patches.settings.adapter_settings(..., defaults=...)` and the headset identity wrapper stay pure.
|
||||
`core.controllers.effective_adapter_settings` supplies the library preference at build, install, live suggestion
|
||||
and dialog boundaries. Recipes override it, including an explicit 0. APK `libframe_settings.so` and installed
|
||||
`settings.conf` receive the effective value.
|
||||
- Recipe/APK/configuration fingerprints do not read the global preference. The existing agent updates its
|
||||
configuration fingerprint during `set_settings`; `game_update.configuration_matches` therefore accepts all
|
||||
three inherited controller values, while still checking explicit overrides and every other configuration field.
|
||||
An offline test exercises the real agent handler to prevent a global apply making the library outdated.
|
||||
- Game settings has a **Use global default (mode)** option. Saving unchanged inherited settings keeps the recipe
|
||||
unchanged; choosing SteamVR default stores an explicit 0. Reset restores the effective global value.
|
||||
- Settings → Controllers stores `input.quest_buttons`, shows a diagram for the choice, and offers **Apply to
|
||||
installed games now** while connected. The existing FIFO Frame tool job pushes only `quest_buttons` through
|
||||
`target.set_settings`. It skips non-Quest games, flat Android apps known to the library, and recipe overrides.
|
||||
A library-less install with an override listed in its deployment is also skipped. Cancellation is checked per game.
|
||||
- The CLI already has `settings <pkg> quest_buttons=N` (runtime only) and `recipe <pkg> --set quest_buttons=N`
|
||||
(persistent recipe). No global CLI option was added: there is no existing general library configuration command,
|
||||
and `settings` specifically writes an installed game's settings. The global preference is available in the GUI.
|
||||
- `scripts/controller_diagram.py` reads the unmodified JSON contract and literal Portal theme colours. It generates
|
||||
the full four-panel SVG, three bundled compact SVGs and the marked Markdown table. `--check` checks all outputs;
|
||||
`--compact --mode N [--out path]` generates just one pair. Bundled images use the existing glyph asset copy and
|
||||
URL path, with no image bytes or filesystem reads in the diagram render helper.
|
||||
- Added the user guide and README/install/playbook/runtime links, regenerated the patch docs, site map and
|
||||
translation template. The only agent edit corrects the old D-pad comment. No shared smoke script was edited.
|
||||
|
||||
## Offline checks and visual review
|
||||
|
||||
Commands use the existing venv without resolving packages. The default uv cache is read-only in this sandbox;
|
||||
plain `uv run` attempted an editable metadata refresh requiring unavailable cached build dependencies.
|
||||
|
||||
```sh
|
||||
export UV_CACHE_DIR=/tmp/frameport-qb-uv
|
||||
export UV_NO_SYNC=1
|
||||
uv run pytest -q
|
||||
uv run python scripts/patch_docs.py --check
|
||||
uv run python scripts/controller_diagram.py --check
|
||||
uv run python scripts/i18n_extract.py --check
|
||||
```
|
||||
|
||||
The focused settings, update, translation, patch and UI tests passed (147 passed, 2 skipped before the final
|
||||
agent-fingerprint regression; the follow-up controller/update/UI-state run passed 89 tests, with the existing
|
||||
socket test deselected). Final full suite: **2015 passed, 45 skipped, 12 failed, 16 setup errors** (67.76 s). All 28 remaining failures/errors
|
||||
are existing local-socket tests rejected by the sandbox (deeplink, devkit, install links, link updates, livestream,
|
||||
setup URL and pairing). The translation failure from the first run was fixed. The changed feature's tests pass;
|
||||
the full suite cannot be reported as passing in this environment. The log is local at `/tmp/qb-app-final-pytest.log`.
|
||||
Patch docs, diagram and translation generation checks pass.
|
||||
|
||||
The real Flet smoke command was attempted:
|
||||
|
||||
```sh
|
||||
FRAMEPORT_NO_UPDATE_CHECK=1 FRAMEPORT_NO_ANNOUNCEMENTS=1 \
|
||||
uv run python scripts/ui_smoke.py --out .shots --fake-frame \
|
||||
--game com.BithellGames.Arcsmith --only tools,game-settings-dialog
|
||||
```
|
||||
|
||||
It cannot start here: the sandbox rejects even creation/binding of loopback sockets (`PermissionError: [Errno 1]
|
||||
Operation not permitted`). Flet reports it cannot bind its server on port 8557. Consequently no Settings or Game
|
||||
settings PNG was produced or inspected. This is an outstanding maintainer validation step, not a passed UI check.
|
||||
No Frame or external network connection was attempted.
|
||||
|
||||
The diagrams were rendered offline with ffmpeg and visually inspected. Both are readable, with correct left/right
|
||||
positions and dimmed unbound buttons. Local, uncommitted review PNGs:
|
||||
|
||||
- `.shots/quest-buttons.png` — all four panels.
|
||||
- `.shots/quest-buttons-compact.png` — the app's Mirror image.
|
||||
|
||||
The Impeccable mechanical detector returned no findings on the changed UI files; targeted Ruff and whitespace
|
||||
checks passed.
|
||||
|
||||
## Capture the actual UI on a machine that permits loopback
|
||||
|
||||
Use a test library in `FRAMEPORT_HOME` containing a Quest game. `ui_smoke.py` has no Controllers section option,
|
||||
so this temporary wrapper reuses its real browser/driver and scrolls the two relevant surfaces. It does not change
|
||||
tracked files. Set `pkg` to a Quest game in that test library first.
|
||||
|
||||
```sh
|
||||
pkg=com.example.questgame
|
||||
cat > .home/capture_controllers.py <<'PY'
|
||||
import sys
|
||||
import time
|
||||
sys.path.insert(0, 'scripts')
|
||||
import ui_smoke as smoke
|
||||
|
||||
original_driver = smoke.driver
|
||||
|
||||
def driver(app, steps, *args):
|
||||
adjusted = []
|
||||
for name, action in steps:
|
||||
if name == 'tools':
|
||||
def controllers(a, original=action):
|
||||
original(a)
|
||||
time.sleep(1)
|
||||
a.page.run_task(a.settings_view.show_section, 'controllers')
|
||||
adjusted.append(('settings-controllers', controllers))
|
||||
elif name == 'game-settings-dialog':
|
||||
def game_settings(a, original=action):
|
||||
original(a)
|
||||
time.sleep(1)
|
||||
dialog = next(d for d in reversed(a.page._dialogs.controls)
|
||||
if d.open and type(d).__name__ == 'AlertDialog')
|
||||
a.page.run_task(dialog.content.content.scroll_to, offset=400, duration=0)
|
||||
adjusted.append(('game-settings-controllers', game_settings))
|
||||
else:
|
||||
adjusted.append((name, action))
|
||||
return original_driver(app, adjusted, *args)
|
||||
|
||||
smoke.driver = driver
|
||||
raise SystemExit(smoke.main())
|
||||
PY
|
||||
FRAMEPORT_NO_UPDATE_CHECK=1 FRAMEPORT_NO_ANNOUNCEMENTS=1 \
|
||||
uv run python .home/capture_controllers.py --out .shots/controllers \
|
||||
--fake-frame --game "$pkg" --only tools,game-settings-dialog
|
||||
```
|
||||
|
||||
Inspect `.shots/controllers/01-settings-controllers.png` and
|
||||
`.shots/controllers/02-game-settings-controllers.png` at 1280×820; repeat with `--viewport 1024x768`. Verify the
|
||||
choice text, diagram, explanation and apply button fit; scroll a little in the dialog if the game's relevant
|
||||
Picture settings make offset 400 miss the row. Also choose all three modes and the global option in the actual
|
||||
GUI, checking that the diagram changes and re-opening preserves inheritance or the explicit override.
|
||||
|
||||
## Exact device verification after native integration
|
||||
|
||||
These steps are for the maintainer; none were executed in this stream. Merge the native stream and its adapter
|
||||
artifacts first. Use a Quest game that binds all four Touch face buttons (and, separately, an available 32-bit
|
||||
bridge game). Set `pkg` to its real library package; keep the same `FRAMEPORT_HOME` used for that test library and
|
||||
its paired Frame. Close the game before rebuilding/installing.
|
||||
|
||||
1. Build/install the integrated adapter once. With global mode Mirror and the game's **Use global default**
|
||||
selected, run:
|
||||
|
||||
```sh
|
||||
pkg=com.example.questgame
|
||||
uv run frameport build "$pkg"
|
||||
uv run frameport install "$pkg" --force --apk-only
|
||||
uv run frameport settings "$pkg" input_diag=1
|
||||
```
|
||||
|
||||
Confirm the build's `lib/<abi>/libframe_settings.so` contains `quest_buttons=1`. The following PC-side inspection
|
||||
reads the APK path recorded by the build:
|
||||
|
||||
```sh
|
||||
uv run python - "$pkg" <<'PY'
|
||||
import sys, zipfile
|
||||
from frameport.core import library
|
||||
with zipfile.ZipFile(library.game(sys.argv[1])['build']['apk']) as apk:
|
||||
for name in apk.namelist():
|
||||
if name.endswith('/libframe_settings.so'):
|
||||
print(name, apk.read(name).decode())
|
||||
PY
|
||||
```
|
||||
|
||||
2. For each mode, write it and launch from Steam/FramePort Play while wearing the headset:
|
||||
|
||||
```sh
|
||||
uv run frameport settings "$pkg" quest_buttons=1 input_diag=1
|
||||
# Close the game, then repeat with quest_buttons=2, then quest_buttons=0.
|
||||
```
|
||||
|
||||
On the Frame, set `pkg` to the same package and locate its actual base (works for internal storage or another
|
||||
drive). After each launch inspect:
|
||||
|
||||
```sh
|
||||
pkg=com.example.questgame
|
||||
base=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["base"])' \
|
||||
"$HOME/Applications/quest-frame/$pkg/deployment.json")
|
||||
grep '^quest_buttons=' "$base/settings.conf"
|
||||
grep -E 'quest_buttons:|input_diag: quest_buttons' "$base/launch.log"
|
||||
```
|
||||
|
||||
Expected for modes 1/2:
|
||||
|
||||
```text
|
||||
quest_buttons: extension XR_VALVE_frame_controller_interaction enabled
|
||||
quest_buttons: frame_controller bindings (mode N, K bindings, D dropped): <XrResult>
|
||||
input_diag: quest_buttons <touch path> -> <frame path>
|
||||
```
|
||||
|
||||
`N` is 1 or 2, `K` is nonzero, and the Frame suggestion result must indicate success. Input diagnostics must
|
||||
match the JSON paths. For mode 0 there should be no new FrameBridge Frame suggestion. Check the physical face
|
||||
buttons against every row of `docs/CONTROLLERS.md`: Mirror repeats X/A on bottom+inner, Y/B on top+outer;
|
||||
Quest pair leaves left up/right and right X/Y unbound. Left View sends Menu in every mode. Check touch actions
|
||||
where the game exposes them, triggers, grip pressure, sticks, poses and haptics. Bumpers are unbound. Repeat on
|
||||
the 32-bit bridge ABI. Existing custom SteamVR bindings should still take precedence.
|
||||
|
||||
3. Verify global apply with two installed games: one following the global choice, one explicitly set to SteamVR
|
||||
default (0). Set Settings → Controllers to Quest pair; click **Apply to installed games now**. Expect one FIFO
|
||||
Frame tool job, `quest_buttons=2` only on inherited games, and the explicit game's 0 retained. Check that neither
|
||||
game gains an Update ready badge solely from the global change/apply. Re-open Game settings: the inherited
|
||||
choice says **Use global default (Quest pair)**. Relaunch to verify the mode; a running game keeps its old mode.
|
||||
Also test changing an explicit mode and then resetting to the global choice.
|
||||
|
||||
4. If an extension-less runtime is available, modes 1/2 must log
|
||||
`quest_buttons: extension XR_VALVE_frame_controller_interaction not offered by the runtime` and retain the
|
||||
SteamVR default mapping without a launch failure. Otherwise leave this case to the native offline tests.
|
||||
|
||||
5. Undo: set the global choice to SteamVR default and apply to inherited installed games. Restore any pre-test
|
||||
per-game choices in Game settings, and disable diagnostics:
|
||||
|
||||
```sh
|
||||
uv run frameport settings "$pkg" quest_buttons=0 input_diag=0
|
||||
```
|
||||
|
||||
A persistent test override made with the recipe CLI can be removed with
|
||||
`uv run frameport recipe "$pkg" --disable adapter.quest_buttons`; then select the global choice and save in
|
||||
Game settings to push its current effective value.
|
||||
|
||||
## Contract questions / remaining integration work
|
||||
|
||||
- The contract translates successful Touch suggestions. A game suggesting only its own Frame bindings is
|
||||
unaffected. If a game suggests both Touch and its own Frame profile, ordering/priority is not specified by the
|
||||
contract; verify with the native stream before claiming both-profile games are unaffected. The user guide
|
||||
qualifies this limit accordingly.
|
||||
- No device claims were added. The runtime reference marks new-mode verification pending integration.
|
||||
- A direct CLI `settings` write is intentionally temporary relative to the recipe/global preference. Use
|
||||
`recipe --set quest_buttons=N` or the dialog for a persistent per-game override.
|
||||
- Actual UI screenshots and the socket-dependent full-suite tests require a less restricted local environment.
|
||||
|
||||
## Round 2
|
||||
|
||||
- Global apply still writes every inherited Quest install, but separates games ready for the next launch from
|
||||
games that need **Update on Frame first**. The job result supplies the completion toast, so both show the same
|
||||
counts. The Settings explanation includes the one-time update requirement.
|
||||
- `_build_inputs_changed` also checks the converter and source APK, so it cannot decide adapter support alone.
|
||||
Its manifest reader was extracted into `build.native_checksums` and is shared by `_build_inputs` and the
|
||||
controller check. Only adapter entries are compared. Missing/different checksums or an unknown/different
|
||||
installed APK are reported as needing an update; unrelated artifact changes do not affect this count. This is
|
||||
deliberately conservative when an older adapter already supports the feature but differs from the bundled one.
|
||||
- The section reads its default once into a local snapshot, uses that for the dropdown and preview, and updates it
|
||||
on selection. Regression tests check the single read and that the existing preview control changes in place.
|
||||
- The user guide now covers game-owned Frame bindings/profile preservation, bare binding paths and continued
|
||||
proximity emulation. This supersedes the earlier question about games suggesting both Frame and Touch profiles.
|
||||
The maintainer's triangle arrows and shared JSON were left unchanged.
|
||||
- Focused tests: **135 passed, 2 skipped** (`test_controller_preferences.py`, `test_patches.py`,
|
||||
`test_game_update.py`). Patch-doc, translation-template and controller-diagram checks pass; the translation
|
||||
template was regenerated. Targeted Ruff and whitespace checks pass.
|
||||
- Device follow-up: on an old/unknown install, choose Mirror and apply globally. Expect its setting to be pushed but
|
||||
the result/toast to count it under **needs Update on Frame first**. Update that game once, apply again, then
|
||||
relaunch: expect it in the next-launch count and the existing mode-1 binding logs described above. Keep a second
|
||||
game with an explicit override and verify it is skipped throughout. Undo by choosing SteamVR default and applying
|
||||
again; restore any per-game test overrides. No device or external network access was used in this round.
|
||||
- Final whole suite: **2029 passed, 116 skipped, 12 failed, 16 setup errors** (67.52 s). The remaining failures
|
||||
and errors are the existing local-socket tests blocked by the sandbox, as in Round 1. Full log:
|
||||
`/tmp/qb-app-round2-pytest.log`. No test skips or socket workarounds were added.
|
||||
@@ -0,0 +1,192 @@
|
||||
# Quest controller mode: native stream
|
||||
|
||||
## Original implementation
|
||||
|
||||
- `native/adapter/quest_buttons.c` parses the numeric adapter setting. Only 0, 1 and 2 are accepted; malformed,
|
||||
fractional and out-of-range values select 0 and log `quest_buttons: invalid setting; using 0`.
|
||||
- `xrCreateInstance` reuses the existing next-layer extension enumeration. Modes 1 and 2 enable
|
||||
`XR_VALVE_frame_controller_interaction` only when offered, without adding a second copy when the game requested
|
||||
it. An unavailable/failed enumeration disables this feature. Failed instance creation clears the enabled flag.
|
||||
- `suggest_frame()` in `session_fixes.c` translates successful Touch suggestions using the tables corresponding to
|
||||
`quest_buttons.json`. It preserves action handles and click/touch components, supports both hands, poses,
|
||||
thumbstick vector/components and haptics, and counts unknown/dropped source bindings. Each input produces at most
|
||||
two Frame bindings. Mode 0 adds no runtime calls or feature logs.
|
||||
- Touch Pro/Plus fallback translates the filtered Touch-shaped copy after Touch succeeds. Proximity-emulation
|
||||
additions are deliberately excluded. A later Touch suggestion submits a fresh Frame set. Frame suggestion errors
|
||||
are logged without changing the game's result. A successful game-owned Frame suggestion prevents subsequent
|
||||
automatic Frame suggestions; if it arrives after an automatic suggestion, its runtime call replaces that set.
|
||||
- With the setting and extension enabled, `xrGetCurrentInteractionProfile` translates Valve profiles to Touch even
|
||||
with `controller_fix=0`. Existing `profile_remap` then still reports the game's Touch Pro/Plus profile when needed.
|
||||
Input diagnostics continue to show the runtime's original profile and additionally log every translated binding.
|
||||
- Both shipped adapter architectures were rebuilt, with updated `artifacts/SHA256SUMS`. No agent, analysis, patch
|
||||
or setting revision was changed. Contract files and the VrApi bridge are unchanged.
|
||||
|
||||
## Original offline validation
|
||||
|
||||
Commands run with `UV_CACHE_DIR=/tmp/frameport-qb-native-uv` to keep uv cache writes inside the writable sandbox;
|
||||
`--offline --no-sync` uses the existing worktree environment without fetching dependencies.
|
||||
|
||||
```sh
|
||||
FRAMEPORT_NATIVE_TESTS=1 uv run --offline --no-sync pytest -q tests/test_quest_buttons.py tests/test_profile_report.py
|
||||
uv run --offline --no-sync python native/build.py --only adapter
|
||||
uv run --offline --no-sync python scripts/patch_docs.py --check
|
||||
uv run --offline --no-sync pytest -q
|
||||
```
|
||||
|
||||
The new host harness includes `quest_buttons.c`, `input_diag.c`, and the complete `session_fixes.c`, plus verbatim
|
||||
copies of the adapter's exported create-instance/current-profile functions. It compiles with host `cc -Werror`.
|
||||
Python derives expected Frame bindings from the unchanged JSON contract, independently of the C mapping table.
|
||||
The targeted run passed 73 tests, including the existing profile-report tests. Two consecutive adapter builds
|
||||
produced identical SHA-256 hashes for both architectures; the manifest matches both files. Documentation checks pass.
|
||||
|
||||
The cached NDK/OpenXR inputs were already present. During the builds, the worktree's ignored `native/.cache` was
|
||||
temporarily a local directory containing input symlinks, so shader intermediates were written only inside this
|
||||
worktree. The original shared-cache symlink was restored afterward. No inputs were fetched or changed and no device
|
||||
was contacted.
|
||||
|
||||
Additional native baseline issue: `FRAMEPORT_NATIVE_TESTS=1 ... tests/test_input_diag.py` has an existing failure in
|
||||
`test_logs_each_finding_once_and_nothing_when_off`. Its harness requests `input_diag_hook("xrCreateAction")`, which
|
||||
the baseline implementation no longer exposes: action diagnostics moved to `session_fixes.c`. Compiling the harness
|
||||
against pristine `25e282d`'s `input_diag.c` reproduced the same line-85 failure. That unrelated behavior/test was left
|
||||
unchanged.
|
||||
|
||||
Full-suite result: **1,993 passed, 116 skipped, 12 failed, 16 errors**. All 28 failing/erroring cases stop at socket
|
||||
creation with `PermissionError: [Errno 1] Operation not permitted`: local HTTP fixtures in deep links, link updates,
|
||||
devkit, livestream, setup URL, and pairing UI tests. The sandbox disallows even loopback sockets. No feature
|
||||
assertion failed. The required unfiltered command was run; a green unfiltered run still requires a host that permits
|
||||
local sockets. The socket restriction was not relaxed.
|
||||
|
||||
An additional run deselecting exactly those 28 unsupported cases passed: **1,993 passed, 116 skipped, 28 deselected**.
|
||||
|
||||
## Integration questions and limits
|
||||
|
||||
- The contract has no missing mapping needed by this implementation. The right controller's Menu button has no
|
||||
mapping in modes 1/2; only left Touch `menu/click` maps to Frame `view/click`. Unknown paths, including menu touch
|
||||
and proximity, are intentionally dropped.
|
||||
- The game's own Frame bindings win only after its Frame suggestion succeeds. A rejected Frame suggestion does
|
||||
not suppress automatic translation. This avoids disabling working input because the game submitted an invalid set.
|
||||
- Headless/host tests cannot establish SteamVR's actual preference for these suggested bindings, physical button
|
||||
layout, capacitive touch behavior, haptics, or whether the installed Frame runtime offers the extension. These
|
||||
require the maintainer's checks below. An empty translated set is submitted and its result logged/ignored like
|
||||
any other Frame suggestion; the runtime can reject it.
|
||||
|
||||
## Maintainer device checks (not executed)
|
||||
|
||||
Use an isolated `.home` already containing a test library entry, its signing key/toolchain, and a paired test Frame.
|
||||
Use a normal Quest OpenXR game and a VrApi game using the bridge. Repeat with a game that suggests only Touch
|
||||
Pro/Plus if available. Replace the sample package name with each game's actual package. Stop other games first.
|
||||
|
||||
1. Rebuild/reinstall the test game so the new adapter is actually inside the APK. A setting update alone cannot
|
||||
upgrade an already installed adapter. Keep the original `controller_fix` and `input_diag` values for restoration.
|
||||
|
||||
```sh
|
||||
export FRAMEPORT_HOME="$PWD/.home"
|
||||
export PKG=com.example.game
|
||||
uv run frameport install "$PKG" --force
|
||||
```
|
||||
|
||||
2. Collect a headless startup log for each mode, with `controller_fix=0` and diagnostics enabled. Each launch test
|
||||
stops its game before the next iteration. The script uses the remembered Frame in this isolated home.
|
||||
|
||||
```sh
|
||||
for MODE in 0 1 2; do
|
||||
export MODE
|
||||
uv run frameport settings "$PKG" quest_buttons="$MODE" input_diag=1 controller_fix=0
|
||||
uv run python - <<'PY'
|
||||
import os
|
||||
from pathlib import Path
|
||||
from frameport.cli import _target
|
||||
from frameport.core.events import Reporter
|
||||
|
||||
target = _target(None)
|
||||
try:
|
||||
result, log = target.launch_test(os.environ["PKG"], Reporter(), seconds=30)
|
||||
output = Path(os.environ["FRAMEPORT_HOME"]) / f"quest-buttons-mode-{os.environ['MODE']}.log"
|
||||
output.write_text(log)
|
||||
print(output)
|
||||
for line in log.splitlines():
|
||||
if "quest_buttons" in line or "controller profile" in line:
|
||||
print(line)
|
||||
finally:
|
||||
target.frame.close()
|
||||
PY
|
||||
done
|
||||
```
|
||||
|
||||
Expected mode 0: no `quest_buttons:` or `input_diag: quest_buttons` lines. Modes 1/2, if offered:
|
||||
|
||||
```text
|
||||
quest_buttons: extension XR_VALVE_frame_controller_interaction enabled
|
||||
quest_buttons: frame_controller bindings (mode 1, K bindings, D dropped): 0
|
||||
input_diag: quest_buttons /user/hand/left/input/x/click -> /user/hand/left/input/dpad_down/click
|
||||
input_diag: quest_buttons /user/hand/left/input/x/click -> /user/hand/left/input/dpad_right/click
|
||||
input_diag: quest_buttons /user/hand/left/input/menu/click -> /user/hand/left/input/view/click
|
||||
```
|
||||
|
||||
`K`/`D` depend on the game. Mode 2 reports `mode 2` and only the first X mapping above. Face-button touch paths
|
||||
keep `/touch`. Frame suggestions must return `0`; a negative result indicates runtime rejection to investigate.
|
||||
A Touch Pro/Plus-only game should also log `controller profile ... -> oculus/touch_controller` and a Frame binding
|
||||
summary. If the runtime lacks the extension, expect
|
||||
`quest_buttons: extension XR_VALVE_frame_controller_interaction not offered by the runtime`, with no translated
|
||||
bindings; input should retain its previous behavior.
|
||||
|
||||
3. For each mode, set it with the command below, then Play from the FramePort library or Steam while wearing the
|
||||
headset. Exit through Steam before changing modes; settings are read at process startup.
|
||||
|
||||
```sh
|
||||
uv run frameport settings "$PKG" quest_buttons=1 input_diag=1 controller_fix=0
|
||||
```
|
||||
|
||||
Mirror: left down/right = Touch X; left up/left = Touch Y; right A/X = Touch A; right B/Y = Touch B. Quest pair:
|
||||
left down = X, left left = Y, right A = A, right B = B; the unused face buttons should not activate those actions.
|
||||
In both modes left View activates Touch Menu. Check thumbstick movement/click/touch, trigger/squeeze values,
|
||||
trigger touch, tracking/aim, and haptic feedback. Verify both hands remain connected with `controller_fix=0`.
|
||||
Mode 0 should match the previous SteamVR mapping. Capacitive-touch verification needs a game that exposes finger
|
||||
animation or an input sample; absence of a suggested touch action is not a failure of this mapping.
|
||||
|
||||
4. Restore settings after stopping the game. The example restores the usual defaults; substitute recorded original
|
||||
values if different. Reinstalling the prior known-good APK also removes the new adapter if an artifact rollback
|
||||
is needed; setting 0 alone disables the feature on the next launch.
|
||||
|
||||
```sh
|
||||
uv run frameport settings "$PKG" quest_buttons=0 input_diag=0 controller_fix=1
|
||||
```
|
||||
|
||||
For explicit current-profile verification in an OpenXR input sample, call `xrGetCurrentInteractionProfile` for
|
||||
`/user/hand/left` and `/user/hand/right`, then `xrPathToString` on each non-null result after action-set attachment.
|
||||
With modes 1/2 and `controller_fix=0`, expect `/interaction_profiles/oculus/touch_controller`, or the game's own
|
||||
Touch Pro/Plus profile when its binding was remapped. Existing `input_diag` profile lines intentionally show the raw
|
||||
runtime answer, so they cannot alone verify the profile returned to the game.
|
||||
|
||||
## Round 2
|
||||
|
||||
The review fixes supersede the original proximity exclusion, broad Valve-profile rewrite, empty-set submission,
|
||||
and failed-instance state handling described above:
|
||||
|
||||
1. Added bare `grip`/`aim` to the JSON common mapping and `FRAME_COMMON`; no other contract fields changed.
|
||||
2. Current-profile rewriting with `controller_fix=0` requires the exact Frame profile and a successful generated
|
||||
binding set still in effect. A successful game-owned Frame suggestion ends that ownership; failed suggestions
|
||||
leave the last accepted set in effect. Existing `controller_fix=1` behavior is retained.
|
||||
3. Frame bindings are generated at the point a Touch set succeeds, including proximity-emulation touch bindings.
|
||||
Direct Touch, Touch Pro/Plus remapping, and rejected-expansion fallback all use the effective accepted set.
|
||||
4. An all-dropped translation makes no Frame suggestion and logs
|
||||
`quest_buttons: frame_controller bindings (mode N, 0 bindings, D dropped): not suggested`. An earlier accepted
|
||||
generated set can remain stale; it is intentionally retained because an empty replacement is invalid.
|
||||
5. Extension decisions remain local until instance creation succeeds. Only successful creation publishes the active
|
||||
instance and resets binding ownership/Touch-remap state; a failed second attempt preserves the live first state.
|
||||
6. One mutex serializes each complete suggestion transaction, including fallback/remapping, Frame generation, and
|
||||
flags. Instance-state publication and current-profile reporting use the same mutex. Internal helpers call the
|
||||
next layer directly and never reacquire it.
|
||||
|
||||
Regression tests reproduced items 1–5 before the fixes (21 failed cases). Afterward, the requested host run passed
|
||||
97 tests, including timeout-bounded nested proximity fallback and Touch Pro remapping. Diagram regeneration made no
|
||||
changes; controller-diagram and patch-documentation checks pass. Both adapter architectures were rebuilt twice with
|
||||
identical hashes matching `SHA256SUMS`. The full suite finished with 2,015 passed, 140 skipped, 12 failed and 16 errors;
|
||||
all 28 failures/errors are the same sandbox-denied local socket creation described above.
|
||||
|
||||
Device follow-up: repeat the install/log/physical checks above, adding `proximity_emul=2` to the mode-1/mode-2 settings
|
||||
command for a game exposing finger proximity. Expect translated proximity actions on face-button/thumbstick touch
|
||||
and trigger touch, and working finger animation. In an OpenXR sample with `controller_fix=0`, also query profiles
|
||||
before any generated set, after a successful generated set, and after the app's own successful Frame suggestion:
|
||||
expect Frame, Touch, Frame respectively. Another Valve profile must stay unchanged. Restore the recorded original
|
||||
`proximity_emul` setting along with the other settings. No device operations were performed in this worktree.
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"_comment": "Single source of the quest_buttons mapping (FrameBridge adapter setting). Keys are Touch input names under /user/hand/<hand>/input/, values the Frame Controller inputs the binding is also suggested for. A button entry covers its bare path (e.g. input/x, which OpenXR resolves to click) and its click and touch components; menu covers the bare path and click only. native/adapter's table, tests/test_quest_buttons.py and scripts/controller_diagram.py follow this file. See docs/input/QUEST_BUTTONS.md.",
|
||||
"extension": "XR_VALVE_frame_controller_interaction",
|
||||
"touch_profile": "/interaction_profiles/oculus/touch_controller",
|
||||
"frame_profile": "/interaction_profiles/valve/frame_controller_valve",
|
||||
"source": "https://partner.steamgames.com/doc/steamhardware/steamframe/input",
|
||||
"common": {
|
||||
"squeeze": ["squeeze"],
|
||||
"squeeze/value": ["squeeze/value"],
|
||||
"trigger": ["trigger"],
|
||||
"trigger/value": ["trigger/value"],
|
||||
"trigger/touch": ["trigger/touch"],
|
||||
"thumbstick": ["thumbstick"],
|
||||
"thumbstick/x": ["thumbstick/x"],
|
||||
"thumbstick/y": ["thumbstick/y"],
|
||||
"thumbstick/click": ["thumbstick/click"],
|
||||
"thumbstick/touch": ["thumbstick/touch"],
|
||||
"grip": ["grip"],
|
||||
"grip/pose": ["grip/pose"],
|
||||
"aim": ["aim"],
|
||||
"aim/pose": ["aim/pose"]
|
||||
},
|
||||
"outputs": {"haptic": ["haptic"]},
|
||||
"dropped": ["thumbrest/touch", "system/click"],
|
||||
"buttons": {
|
||||
"left": ["x", "y"],
|
||||
"right": ["a", "b"]
|
||||
},
|
||||
"modes": {
|
||||
"0": {
|
||||
"name": "SteamVR default",
|
||||
"note": "FrameBridge suggests nothing for the Frame profile; SteamVR remaps Touch itself (documented by Valve). Listed for the diagram only.",
|
||||
"left": {"x": ["dpad_down"], "y": ["dpad_left", "dpad_up", "dpad_right"], "menu": ["view"]},
|
||||
"right": {"a": ["a"], "b": ["x", "y", "b"]}
|
||||
},
|
||||
"1": {
|
||||
"name": "Mirror",
|
||||
"note": "Every face button works; the left controller mirrors the right: bottom + outer = X/A, top + inner = Y/B, as on Quest controllers.",
|
||||
"left": {"x": ["dpad_down", "dpad_left"], "y": ["dpad_up", "dpad_right"], "menu": ["view"]},
|
||||
"right": {"a": ["a", "b"], "b": ["x", "y"]}
|
||||
},
|
||||
"2": {
|
||||
"name": "Quest pair",
|
||||
"note": "Two buttons per controller, placed like Quest Touch's (the second one up and toward the other hand); the other face buttons do nothing.",
|
||||
"left": {"x": ["dpad_down"], "y": ["dpad_right"], "menu": ["view"]},
|
||||
"right": {"a": ["a"], "b": ["x"]}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
# Phase 0: reading Valve's RoomView 3D mesh (dev Frame, SteamOS 0.4.5 build 20261007, 2026-10-16)
|
||||
|
||||
Result: **a third-party OpenVR client can read the passthrough depth mesh, and it carries enough of the room for
|
||||
a scan** (floor, table-height surfaces, orthogonal walls; the ceiling only weakly).
|
||||
|
||||
## Access
|
||||
- Background OpenVR app (`VR_InitInternal2(..., VRApplication_Background)`, never starts SteamVR) through
|
||||
`/opt/steamvr/bin/linuxarm64/libopenvr_api.so`.
|
||||
- `FnTable:IVRBlockQueue_005`: `Connect("/xrservice/roomview3d/meshData")` and `.../meshLayout` work (created by
|
||||
Valve's cv driver; no exclusivity seen next to the compositor's own reader). Read with
|
||||
`WaitAndAcquireReadOnlyBlock(queue, &block, &buffer, EBlockQueueReadType_New, timeout_ms)` + `ReleaseReadOnlyBlock`.
|
||||
- Metadata through the C++ interface `IVRPaths_002` (vtable slot 0 `ReadPathBatch(root, PathRead_t*, n)`, slot 2
|
||||
`StringToHandle(&handle, path)`; `FnTable:IVRPaths_001` doesn't exist), root = the **block** handle, except
|
||||
`/meshData/meshIsUpToDate` which is a property of the **queue** handle:
|
||||
|
||||
| path | tag | meaning |
|
||||
|---|---|---|
|
||||
| `/meshLayout/layoutId` | 3 (u64) | id of the index layout (static while the mesh resolution stays) |
|
||||
| `/meshLayout/vertexCount` | 2 (i32) | 16896 = 132 x 128 |
|
||||
| `/meshLayout/indexCount` | 2 | 99822 (u32 triangle indices into the grid, row width 132) |
|
||||
| `/meshData/meshId` | 3 | frame counter (~30 per second while passthrough runs) |
|
||||
| `/meshData/layoutId` | 3 | matches the layout block |
|
||||
| `/meshData/elementCount` | 2 | floats in the data block (vertexCount x 3) |
|
||||
| `/meshData/rawFromMeshPose` | 20 (HmdMatrix34) | mesh -> SteamVR raw tracking space |
|
||||
| `/meshData/posePredictionBaseTime` | 7 (f64) | seconds (system uptime clock) |
|
||||
| `/meshData/meshIsUpToDate` (queue) | 4 (bool) | false in standby / headset off |
|
||||
|
||||
- STAGE (= standing universe) = `IVRSystem_023` vtable slot 13 `GetRawZeroPoseToStandingAbsoluteTrackingPose()` x
|
||||
rawFromMeshPose. Check: the mesh origin is 6.7 cm from `GetDeviceToAbsoluteTrackingPose(Standing)`'s head
|
||||
position in every frame (camera vs head centre). **The raw->standing transform changes during a session** (its
|
||||
height went 1.60 -> 1.07 m while recording: Valve's floor estimate) -> capture raw poses and convert all frames
|
||||
with the transform at the end of the capture.
|
||||
|
||||
## The mesh
|
||||
- A fixed fan of view directions, +Z forward, rows top->bottom (row 0 = up in the image, -Y), ~126 deg across the
|
||||
corners (corner directions ~(+-0.67, +-0.66, 0.34)); one depth per grid point. Not head-locked in world terms:
|
||||
its pose is the passthrough camera pose at capture.
|
||||
- Range: reliable ~0.3-4 m; far values are inpainted (up to ~10 m) -> drop > 4 m. Strongly smoothed (soft edges,
|
||||
small objects lost); locally planar: plane-fit residual of 12x12 patches median 5-17 mm.
|
||||
- First frames after tracking starts can have NaN poses -> skip non-finite frames.
|
||||
- Fused normals (60 s of looking around, ~3 frames/s used): floor clearly at y ~= 0, a table-height surface at
|
||||
0.7 m, a lower one at 0.5 m; vertical surfaces in two orientation clusters ~85 deg apart (orthogonal walls), wall
|
||||
offsets readable; ceiling: few down-facing points (people rarely look straight up; prompt for it).
|
||||
- Data rate: 16896 x 12 B vertices + 99822 x 4 B indices per frame (the dump repeats the indices per frame).
|
||||
|
||||
## Consequences for the design
|
||||
- Capture = stdlib Python on the Frame (ctypes), as above; the C scanner works offline on the dump.
|
||||
- Scanner input quality: smooth, noisy at depth discontinuities; walls/floor via normals + RANSAC are robust;
|
||||
furniture only as coarse boxes; ceiling needs a guided "look up" step, else the default height.
|
||||
@@ -0,0 +1,113 @@
|
||||
# FramePort room files (mixed reality)
|
||||
|
||||
Quest MR games read a room model (floor, ceiling, walls, furniture, a global mesh) through Meta's scene APIs. The
|
||||
Steam Frame has no such data for apps (see docs/FRAME_RUNTIME.md "Room data"), so FramePort scans the room itself
|
||||
(`fp_roomscan`) and FrameBridge (`native/adapter/scene_emu.c`) serves it to games. This file is the contract between
|
||||
the scanner, the Frame agent, the PC app and FrameBridge.
|
||||
|
||||
## Coordinates
|
||||
- Everything is in **STAGE space** = SteamVR's standing universe = OpenXR `XR_REFERENCE_SPACE_TYPE_STAGE`:
|
||||
metres, right-handed, +Y up, floor at y = 0, -Z = the universe's forward.
|
||||
- A room belongs to one chaperone universe (`universe` line). The Frame sometimes starts a new universe without
|
||||
relocalizing; then the stored room may be misaligned (the agent reports `aligned: false`, nothing breaks).
|
||||
|
||||
## room.txt (UTF-8 text, one record per line, `#` starts a comment, fields separated by spaces)
|
||||
```
|
||||
frameport-room 1 # magic + format version (required, first line)
|
||||
universe <decimal u64> # chaperone universe id the room was scanned in (0 = unknown)
|
||||
source scan|boundary|box|manual # how it was made
|
||||
time <unix seconds>
|
||||
room <uuid> # the room anchor
|
||||
floor <uuid> <y> # floor height (normally 0)
|
||||
ceiling <uuid> <y> # ceiling height
|
||||
outline <n> x0 z0 x1 z1 ... x(n-1) z(n-1)
|
||||
# floor polygon, n >= 3, not closed, POSITIVE signed area:
|
||||
# sum(x[i]*z[i+1] - x[i+1]*z[i]) > 0 (turning from +X towards +Z; seen from
|
||||
# above with -Z forward that is clockwise). Readers reverse a negative outline.
|
||||
wall <uuid> x0 z0 x1 z1 <y0> <y1> [LABEL]
|
||||
# one wall face from (x0,z0) to (x1,z1), bottom y0, top y1.
|
||||
# Its normal points INTO the room: walls follow the outline's order, and the
|
||||
# inward normal is (-dz, dx)/|d| for d = (x1-x0, z1-z0).
|
||||
# LABEL (optional, default WALL_FACE): INVISIBLE_WALL_FACE for an edge the
|
||||
# scanner closed without seeing a wall (open side of a room, doorway gap).
|
||||
volume <uuid> <LABEL> cx cz top_y yaw_deg w d h
|
||||
# furniture box: top-face centre (cx, top_y, cz), yaw about +Y in degrees
|
||||
# (0 = local X along world X), width w along local X, depth d along the
|
||||
# horizontal local axis perpendicular to it, height h downwards from the top
|
||||
plane <uuid> <LABEL> px py pz qx qy qz qw w h
|
||||
# extra 2D plane (DOOR_FRAME, WINDOW_FRAME, WALL_ART, ...): pose of its centre,
|
||||
# Meta convention (bounds in local XY, local +Z = the plane's normal)
|
||||
mesh <uuid> <file name> <vertex count> <index count>
|
||||
# global mesh sidecar in the same folder (GLOBAL_MESH entity)
|
||||
```
|
||||
- `<uuid>` = 32 lowercase hex digits (no dashes). New ones per scan; FrameBridge uses them as the XrUuidEXT bytes.
|
||||
- Labels are Meta's semantic label strings: FLOOR, CEILING, WALL_FACE, INVISIBLE_WALL_FACE, TABLE, COUCH, BED,
|
||||
STORAGE, SCREEN, LAMP, PLANT, DOOR_FRAME, WINDOW_FRAME, WALL_ART, OTHER, GLOBAL_MESH.
|
||||
- Readers ignore unknown record types (forward compatibility) and reject a file whose first line isn't
|
||||
`frameport-room <version>` with a version they know. Missing ceiling -> 2.5 m above the floor. Missing walls ->
|
||||
walls from the outline edges. Missing outline -> the bounding rectangle of the walls.
|
||||
|
||||
### How FrameBridge maps records to Meta entities (scene_emu.c)
|
||||
- ROOM: ROOM_LAYOUT (floor, ceiling, walls in file order) + SPACE_CONTAINER (every other entity) + LOCATABLE/STORABLE.
|
||||
- floor/ceiling: pose at the outline centroid at height y; local +Z = +Y (floor) / -Y (ceiling);
|
||||
BOUNDED_2D = the outline's bounding rect in local XY; BOUNDARY_2D = the outline polygon in local XY.
|
||||
- wall: pose at the segment midpoint, height (y0+y1)/2; local +X along the wall (x0,z0)->(x1,z1), local +Y up,
|
||||
local +Z = inward normal; BOUNDED_2D = length x (y1-y0) centred.
|
||||
- volume: pose at the top-face centre, rotation = yaw about +Y then local +Z pointing up (+Y); BOUNDED_3D offset
|
||||
(-w/2, -d/2, -h), extent (w, d, h) (Meta: volumes hang below their anchor); BOUNDED_2D = the top face (w x d).
|
||||
- plane: pose as given; BOUNDED_2D = w x h centred.
|
||||
- mesh: entity at the identity pose labelled GLOBAL_MESH with TRIANGLE_MESH (XR_META_spatial_entity_mesh).
|
||||
|
||||
## room-mesh.bin (little-endian)
|
||||
```
|
||||
char[4] "FPRM", u32 version = 1, u32 vertex_count, u32 index_count,
|
||||
float32 xyz[vertex_count], u32 indices[index_count] (triangles; counter-clockwise seen from the room's inside)
|
||||
```
|
||||
Coordinates in STAGE space. Kept below 200k triangles (decimated).
|
||||
|
||||
## Raw files and scan.json (scanner sidecars)
|
||||
`fp_roomscan` refines its output by default (docs/mr/scan.md "Refinement"). `room.txt` + `room-mesh.bin` are always
|
||||
the refined room (what games and the GUI read; the contract above is unchanged). Next to them:
|
||||
- `room-raw.txt` + `room-mesh-raw.bin`: the scanner's unrefined output in the same formats (its `mesh` record names
|
||||
`room-mesh-raw.bin`). Same UUIDs as the refined room.txt, so anchors stay the same when the room is refined again.
|
||||
The refined room differs in: the ceiling height (re-estimated from the scan, wall tops follow it), the volumes
|
||||
(after the label rules) and the mesh. Records the scanner doesn't model (e.g. `plane`) are copied verbatim.
|
||||
- `scan.json` (UTF-8 JSON, `version` 1):
|
||||
```
|
||||
{"version": 1,
|
||||
"heads": [[x, y, z], ...], # head path of the recording (STAGE), <= 2000 points, decimated evenly
|
||||
"refine": {"refine": 1, "walls": "panels", "band": 0.35, ..., "erase": [[x, y, z, r], ...]},
|
||||
# every refinement setting used (keys = the CLI options, docs/mr/scan.md)
|
||||
"report": {"triangles", "kept", "structural", "near_walls", "outside", "small_pieces", "thin_pieces",
|
||||
"erased", "walked", "tops", "panel_triangles", "truncated", "walked_skipped", "floor_y",
|
||||
"ceiling_y", "boxes", "relabelled", "removed_boxes", "suggestions",
|
||||
"plane_shifts": [{"kind": "wall"|"floor"|"ceiling", "wall": i, "shift": m}, ...]},
|
||||
"labels": [{"from": "COUCH", "to": "TABLE"|null, "reason": "top 0.69 m", "suggest": "door"|"closet" (optional),
|
||||
"id": "<volume uuid>", "box": {"cx", "cz", "top_y", "yaw_deg", "w", "d", "h"}}, ...],
|
||||
# one per scanner volume, in room-raw.txt order; to = null: removed
|
||||
"suggestions": [{"kind": "door"|"closet", "box": {...}, "wall": i, "along": t, "reason": "..."}, ...]}
|
||||
# tall thin slabs; never applied by the scanner. wall = index of the nearest
|
||||
# wall record in room-raw.txt, along = 0..1 position on it (from x0,z0)
|
||||
```
|
||||
`report` counts triangles of the raw mesh per pass (`panel_triangles` are added ones), `wall` in `plane_shifts`
|
||||
indexes the wall records. Readers ignore unknown keys.
|
||||
- `fp_roomscan --refine-only <dir>` re-runs only the refinement from these files (an older folder without them:
|
||||
its room.txt + mesh are copied to the raw names first). Publishing order: [raw mesh, raw text,] `scan.json`,
|
||||
`room-mesh.bin`, `room.txt`.
|
||||
|
||||
## Where the files live
|
||||
- Frame: `~/.local/share/frameport/rooms/<universe>/room.txt` + `room-mesh.bin` (+ `scan.log`, the raw files and
|
||||
`scan.json` above).
|
||||
`~/.local/share/frameport/rooms/current` = symlink to the newest room's folder.
|
||||
- Per game: launch.sh copies the current universe's room (else `current`) to `<app_dir>/room.txt` (+ mesh) before
|
||||
Lepton starts, next to `settings.conf`; FrameBridge reads `dirname($FRAMEBRIDGE_CONFIG)/room.txt`. No room ->
|
||||
the old guardian box.
|
||||
- A game's own saved anchors: `/sdcard/Android/data/<pkg>/files/framebridge-anchors.txt`
|
||||
(`anchor <uuid> px py pz qx qy qz qw` per line, STAGE space), written by FrameBridge on save/erase.
|
||||
|
||||
## Scan dumps (*.fprs) — recorded RoomView 3D meshes, input of `fp_roomscan --offline`
|
||||
```
|
||||
char[4] "FPRS", u32 version = 1, then frames until EOF:
|
||||
f64 time_s, float32 pose[7] (px py pz qx qy qz qw: mesh -> STAGE), u32 vertex_count, u32 index_count,
|
||||
float32 xyz[vertex_count] (mesh space), u32 indices[index_count]
|
||||
```
|
||||
@@ -0,0 +1,216 @@
|
||||
# Scene emulation stream
|
||||
|
||||
Implemented against `ROOM_FORMAT.md` without changing the contract, patch revisions, agent version or analysis version.
|
||||
No Frame access or external downloads were used. Headset behavior still needs maintainer verification.
|
||||
|
||||
## Implementation
|
||||
|
||||
- `scene_emu.c` owns entities, component APIs, pose transforms, bounded query snapshots and the event queue.
|
||||
`scene_room.c` parses room files and meshes; `scene_meta.c` implements META discovery and batch persistence;
|
||||
`scene_anchors.c` implements per-game saved anchors. All are included by the adapter, with no new dependencies.
|
||||
- At session creation, read `dirname(FRAMEBRIDGE_CONFIG)/room.txt`. Invalid magic/version, unusable geometry or
|
||||
absent files retain the original guardian box, settings and seven deterministic UUIDs. Unknown records are
|
||||
ignored; malformed records are logged and skipped. Missing ceiling, outline and walls follow the contract.
|
||||
- Floor/ceiling use the polygon area centroid and real local-XY polygons. Walls retain file order, with local +X
|
||||
along their segment, +Y up and +Z inward. Furniture poses use yaw then +Z up; volume bounds extend downwards.
|
||||
The room container includes every other room-file entity, excluding independently created application anchors.
|
||||
- Geometry exposes BOUNDED_2D, BOUNDED_3D, semantic labels and TRIANGLE_MESH as appropriate. OpenXR has no separate
|
||||
`BOUNDARY_2D` component enum: `xrGetSpaceBoundary2DFB` belongs to BOUNDED_2D. Labels preserve comma-separated strings.
|
||||
All variable-sized outputs support count-only and insufficient-capacity calls, including independent mesh arrays.
|
||||
- Limits: 2,048 entities including application anchors, 1,024 outline points, 255 label bytes, 64 KiB record lines,
|
||||
one global mesh, 600,000 vertices/indices (200,000 triangles) and 16 MiB mesh payload. Sidecar filenames cannot
|
||||
escape the config directory. Magic, version, counts, exact byte size, finite coordinates and indices are checked.
|
||||
- FB queries and META discovery snapshot results under the mutation lock, before queuing available/complete events.
|
||||
META component/UUID filters intersect; unrecognized filters impose no restriction. FB max-result limits are honored.
|
||||
There are 64 retained query snapshots and 63 queued events; calls that cannot fit their events return
|
||||
`XR_ERROR_LIMIT_REACHED`. Geometry readers share the mutation lock with persistence.
|
||||
- Application anchors use kernel randomness (`getrandom`, then `/dev/urandom`), never index-derived UUIDs.
|
||||
Saved poses are in STAGE space. FB/META saves write a private temporary file, flush/fsync, then rename to
|
||||
`/sdcard/Android/data/<package>/files/framebridge-anchors.txt`. A process suffix such as `:game` is stripped.
|
||||
Erase removes application anchors from disk and the discoverable in-memory set. Failed writes report failure
|
||||
through completion events and restore memory. Room-file entities remain owned by the scan: save/erase are no-ops
|
||||
for those entities. Storage is local; there is no cloud synchronization.
|
||||
- A new session reloads the room and saved anchors. Fake handles fit both adapter ABIs and include a session
|
||||
generation; erased/stale fake spaces are never passed to SteamVR. Session generations wrap after 256 sessions.
|
||||
- Capture completes immediately and logs exactly `scene: capture requested -> scanned room` for a usable room file,
|
||||
or `scene: capture requested -> guardian box` for the fallback. The former covers all file sources, including manual
|
||||
and boundary files. The load summary preserves the actual `source` and `universe`.
|
||||
|
||||
The cached `openxr-f2448a8797` headers already contain discovery, persistence and mesh declarations and enum values.
|
||||
No local structure/enum substitutes were necessary. Both META extensions are added to the existing conditional
|
||||
extension advertisement/instance filtering. Their system capability structures report support when emulation is active.
|
||||
Passthrough and unrelated adapter settings are unchanged.
|
||||
|
||||
## Routing and integration
|
||||
|
||||
All of the following use `scene_emulation` through **both** the adapter's normal `xrGetInstanceProcAddr` and
|
||||
`framebridge_extension_proc`, reached through xrshim's existing name-agnostic lookup:
|
||||
|
||||
```
|
||||
xrQuerySpacesFB xrRetrieveSpaceQueryResultsFB
|
||||
xrGetSpaceUuidFB xrEnumerateSpaceSupportedComponentsFB
|
||||
xrSetSpaceComponentStatusFB xrGetSpaceComponentStatusFB
|
||||
xrGetSpaceContainerFB xrGetSpaceRoomLayoutFB
|
||||
xrGetSpaceBoundingBox2DFB xrGetSpaceBoundingBox3DFB
|
||||
xrGetSpaceBoundary2DFB xrGetSpaceSemanticLabelsFB
|
||||
xrRequestSceneCaptureFB xrCreateSpatialAnchorFB
|
||||
xrSaveSpaceFB xrEraseSpaceFB
|
||||
xrGetSpaceTriangleMeshMETA xrDiscoverSpacesMETA
|
||||
xrRetrieveSpaceDiscoveryResultsMETA xrSaveSpacesMETA
|
||||
xrEraseSpacesMETA
|
||||
```
|
||||
|
||||
The shim additionally routes `xrPollEvent`, `xrGetSystemProperties`, `xrLocateSpace`, `xrLocateSpaces`,
|
||||
`xrLocateSpacesKHR` and `xrDestroySpace` to the adapter while scene emulation is active. Other shim names continue to
|
||||
OVRPort, preserving existing haptics/render-model handling. The real shim is host-tested against a dispatcher that
|
||||
refuses every scene function; every emulated scene lookup succeeds and an actual discovery/retrieve round trip runs.
|
||||
|
||||
**Plumb-stream requirement:** ensure `scene_emul=1` also installs xrshim and rewrites OVRPlugin's loader string to it.
|
||||
The pre-existing adapter patch only did this for other shim settings. These files are outside this stream's ownership.
|
||||
Delivery of room files before Lepton starts and detection of universe misalignment also belong to plumb. FrameBridge
|
||||
serves the supplied coordinates; it does not silently transform a room belonging to another universe.
|
||||
|
||||
No contract changes are needed. Whether each MRUK version accepts a room without further permissions, how it chooses
|
||||
volume versus mesh geometry, and whether its visible placement matches the scan need headset tests. The existing
|
||||
single-active-session architecture is retained; simultaneous independent sessions are not implemented.
|
||||
|
||||
## Offline verification
|
||||
|
||||
- 25 host tests passed, compiling the actual emulation and shim. Coverage includes malformed records, unknown
|
||||
records, mesh corruption/path/cap checks, derived geometry, 300 walls, poses/normals, all buffer sizing idioms,
|
||||
discovery event order/filters/backpressure, UUID query/discovery of saved anchors, new sessions/processes,
|
||||
FB/META save/erase and atomic-write failure rollback.
|
||||
- ASan/UBSan: scanned room, generated walls and box cases passed. LeakSanitizer was disabled because this sandbox
|
||||
prohibits ptrace; its default exit failed with that environmental limitation.
|
||||
- `python native/build.py --only adapter,xrshim` succeeded from cached tools. Two final builds were byte-identical,
|
||||
including `SHA256SUMS`. The shim was rebuilt and is byte-identical to its previous artifact (the routing lives in
|
||||
the adapter). Artifact content changes are the two adapter binaries and two checksum lines.
|
||||
- Full pytest was attempted: **1,771 passed, 40 skipped, 11 failed, 16 errors**. All 27 failing/erroring tests create
|
||||
local HTTP sockets, which the sandbox prohibits (`PermissionError: [Errno 1] Operation not permitted`). They are
|
||||
in `test_deeplink`, `test_devkit`, `test_install_links_extra`, `test_livestream`, `test_setup_url` and `test_ui_state`.
|
||||
With exactly those 27 tests deselected, and the additional real-shim test included: **1,772 passed, 40 skipped,
|
||||
27 deselected**. An unrestricted maintainer run of the whole suite is still required.
|
||||
- The worktree had no usable venv. Verification reused the existing local dependencies through an ignored venv
|
||||
`.pth`, with `PYTHONPATH="$PWD/src"`, `UV_CACHE_DIR=/tmp/frameport-uv-cache`, `UV_OFFLINE=true` and
|
||||
`uv run --no-sync pytest`. No dependencies were downloaded. The build cache was made a local directory with
|
||||
symlinks to cached tools/headers so generated shader files stayed inside this writable worktree.
|
||||
|
||||
Adapter SHA-256:
|
||||
|
||||
```
|
||||
arm64-v8a: a90c8d58904857bc198a48fd7a411b7807f68f14fa58fd268c57e2c2ca87e389
|
||||
armeabi-v7a: 92daecaf5b86e1857ee8a61b76598e5645f8b427e5f373067c1f87bf8ff99d07
|
||||
```
|
||||
|
||||
## Maintainer device tests
|
||||
|
||||
These commands are a handoff, not commands run by this agent. Use an isolated populated `FRAMEPORT_HOME`, cached
|
||||
build tools, a paired Frame and legally obtained game sources. Stop games before changing their files. Back up the
|
||||
existing recipe, APK, room files and saved anchors first. Integrate plumb before testing normal room delivery.
|
||||
|
||||
On the PC, from the integrated checkout (replace `frame.local` if necessary):
|
||||
|
||||
```sh
|
||||
export FRAMEPORT_HOME="$PWD/.home"
|
||||
export FRAMEPORT_NO_UPDATE_CHECK=1 FRAMEPORT_NO_CATALOG_UPDATE=1
|
||||
FRAME=steamos@frame.local
|
||||
PKG=com.Novelab.Demeter
|
||||
uv run frameport recipe "$PKG" --set scene_emul=1
|
||||
uv run frameport install "$PKG" --frame "$FRAME" --force
|
||||
uv run frameport test "$PKG" --frame "$FRAME" --seconds 45
|
||||
ssh "$FRAME"
|
||||
```
|
||||
|
||||
In that Frame shell, find the deployment's actual base (also supports a game moved to another drive):
|
||||
|
||||
```sh
|
||||
PKG=com.Novelab.Demeter
|
||||
BASE=$(python3 -c 'import json,os,sys; p=os.path.expanduser("~/Applications/quest-frame/"+sys.argv[1]+"/deployment.json"); print(json.load(open(p)).get("base",os.path.dirname(p)))' "$PKG")
|
||||
grep -E 'scene:|extension shim:.*(Spaces|TriangleMesh|BoundingBox3D|PollEvent)' "$BASE/launch.log"
|
||||
```
|
||||
|
||||
With a current scan delivered beside `settings.conf`, expect:
|
||||
|
||||
```
|
||||
scene: room from scan file (N walls, M volumes, P planes, K mesh triangles, universe U)
|
||||
scene: capture requested -> scanned room
|
||||
```
|
||||
|
||||
The capture line appears only if the game requests capture. A source made from a boundary/manual file names that
|
||||
source in the summary. The shim logs each requested function, for example
|
||||
`extension shim: xrDiscoverSpacesMETA -> FrameBridge`. There must be no `Unknown proc addr` for a scene function
|
||||
that the game obtains through the shim. Repeat the rebuild/install/headset check for `com.gritGames.Nope` and
|
||||
`com.zenstudios.PFXVRQuest` with `scene_emul=1`; these exercise newer MRUK discovery/persistence paths.
|
||||
|
||||
Wear the headset and launch from Steam. Enter each game's MR mode and request room setup if offered. Verify the
|
||||
floor polygon, every wall, ceiling and furniture match the scan; tables sit below their top-face anchors; doors and
|
||||
windows have the correct orientation. Headless startup alone cannot prove tracking or visual placement.
|
||||
|
||||
Fallback/undo test, while stopped: preserve `$BASE/room.txt` and `$BASE/room-mesh.bin` under a separate backup
|
||||
folder, then disable plumb's room delivery for this run or move the source room out of `rooms/current` as well.
|
||||
Without that precaution launch.sh will restore the file. Launch again; expect the original box summary and
|
||||
`scene: capture requested -> guardian box`. Test `scene_width=3 scene_depth=4 scene_height=2.7` with the existing
|
||||
`frameport settings` command and expect `scene: room 3.00 x 4.00 x 2.70 m`. Restore the saved files, recipe/settings
|
||||
and normal room delivery afterward. Do not delete a real game's saved anchors to test the fallback.
|
||||
|
||||
### Android persistence probe (independent of a game's UI)
|
||||
|
||||
This probe uses the **real scene code with the fake runtime from the host harness**. It checks Android storage,
|
||||
process-name handling and both persistence APIs; the headset tests above check the real SteamVR integration.
|
||||
The following cross-compilation command was verified offline:
|
||||
|
||||
```sh
|
||||
native/.cache/ndk-27.2.12479018/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android29-clang \
|
||||
-std=gnu11 -O2 -Wall -Wextra -Werror -Wno-unused-function \
|
||||
-I native/.cache/openxr-f2448a8797 -I native/adapter tests/native/scene_emu_test.c \
|
||||
-lm -o /tmp/frameport-scene-test-android
|
||||
scp /tmp/frameport-scene-test-android "$FRAME":/tmp/
|
||||
```
|
||||
|
||||
Keep a freshly installed game running so its Lepton container exists, then run in the Frame shell:
|
||||
|
||||
```sh
|
||||
APPID=$(python3 -c 'import json,os,sys; print(json.load(open(os.path.expanduser("~/Applications/quest-frame/"+sys.argv[1]+"/deployment.json")))["appid"])' "$PKG")
|
||||
CONTAINER=lepton-steamlaunch-$APPID
|
||||
podman cp /tmp/frameport-scene-test-android "$CONTAINER":/data/local/tmp/frameport-scene-test
|
||||
podman exec "$CONTAINER" test ! -e /sdcard/Android/data/org.frameport.scenetest
|
||||
# Continue only if that test succeeds: use a fresh test-only package directory.
|
||||
podman exec "$CONTAINER" mkdir -p /sdcard/Android/data/org.frameport.scenetest/files /data/local/tmp/frameport-scene-probe
|
||||
for mode in save reload erase-failure reload erase empty; do
|
||||
podman exec "$CONTAINER" sh -c "FRAMEBRIDGE_CONFIG=/data/local/tmp/frameport-scene-probe/settings.conf exec -a org.frameport.scenetest /data/local/tmp/frameport-scene-test $mode" || break
|
||||
done
|
||||
podman exec "$CONTAINER" wc -l /sdcard/Android/data/org.frameport.scenetest/files/framebridge-anchors.txt
|
||||
```
|
||||
|
||||
Every invocation must exit 0 without `FAIL`; logs show `anchors saved`, `loaded 2 saved anchors`, a deliberate
|
||||
`anchor write failed` during `erase-failure` (then the following reload still finds two), and `anchors erased`.
|
||||
The final file has zero lines. `save` also recreates a session internally, while separate invocations validate
|
||||
new-process reload. Undo, before stopping this container:
|
||||
|
||||
```sh
|
||||
podman exec "$CONTAINER" rm -rf /sdcard/Android/data/org.frameport.scenetest /data/local/tmp/frameport-scene-probe
|
||||
podman exec "$CONTAINER" rm -f /data/local/tmp/frameport-scene-test
|
||||
rm -f /tmp/frameport-scene-test-android
|
||||
```
|
||||
|
||||
Remove the PC's temporary probe binary too. Restore backed-up game files/settings and install the previous APK if
|
||||
rolling the feature back. No probe command changes real application anchors.
|
||||
|
||||
## Commit handoff under the sandbox
|
||||
|
||||
This worktree's real Git metadata is on a read-only mount. Commits therefore live on `mr/emul` in
|
||||
`/tmp/frameport-mr-emul.git`, with a portable bundle at `/tmp/frameport-mr-emul.bundle`. The actual worktree contains
|
||||
all final source/artifact edits but its original branch metadata cannot be advanced here. No local instruction,
|
||||
app-data or credential files were staged. The base is `4bf48d3`.
|
||||
|
||||
After reviewing the worktree and verifying it contains exactly these changes, the maintainer can import the commits
|
||||
and align the existing worktree index without discarding its files:
|
||||
|
||||
```sh
|
||||
git bundle verify /tmp/frameport-mr-emul.bundle
|
||||
git fetch /tmp/frameport-mr-emul.bundle mr/emul
|
||||
git log --oneline 4bf48d3..FETCH_HEAD
|
||||
git reset --mixed FETCH_HEAD
|
||||
```
|
||||
|
||||
Run the unrestricted whole suite before merging. The bundle preserves the ordered implementation/test/build commits.
|
||||
@@ -0,0 +1,171 @@
|
||||
# Room plumbing stream
|
||||
|
||||
Implemented on `mr/plumb`; no Frame access or device verification. The shared `ROOM_FORMAT.md`, agent version,
|
||||
analysis version and patch revisions were left unchanged. No native binaries belong to this stream.
|
||||
|
||||
## What changed
|
||||
|
||||
- `frameport.mr.room`: room/entity dataclasses, v1 text and binary-mesh validation, canonical serialization,
|
||||
missing ceiling/wall/outline defaults, clockwise outline/wall repair, summaries and top-down SVG previews.
|
||||
Mesh counts, indices, finite coordinates, UUIDs and sidecar basenames are checked before import.
|
||||
- Standalone stdlib agent room commands live in one marked block. Rooms are staged under
|
||||
`~/.local/share/frameport/rooms/<universe>/`; a `current` symlink selects the latest saved room. Uploads validate
|
||||
before replacing a room. Deleting the current room selects the newest remaining room.
|
||||
- `room_status` returns `rooms`, current `universe`, selected `room`, `room_text`, `aligned`, `default_circle`
|
||||
(null when no usable boundary exists) and `scanner_installed`. Summaries include wall/plane counts, furniture
|
||||
counts by label, floor area and ceiling height. Status text lets the UI preview a room without another transfer.
|
||||
- Universe detection takes the last matching vrserver log line, then the newest chaperone record if no log match
|
||||
exists. Dummy universe 424242 means unknown. Boundary conversion chains collision quads' bottom edges and refuses
|
||||
the default 20-point, roughly 1 m circle. Boundary ceilings default to 2.5 m; SteamVR's 2.43 m wall extrusion
|
||||
does not measure the real ceiling.
|
||||
- Finalize records `scene_room` from adapter settings. A marked `_room_sync` launcher line follows the existing
|
||||
config-sync pattern, before Lepton starts. Launcher upgrades also handle old recipes listing
|
||||
`adapter.scene_emul`. Matching universe wins over `current`; absent rooms remove stale sidecars. Live setting
|
||||
changes update the launcher and clear copies when disabled. Non-scene launchers keep their existing layout.
|
||||
- PC room helpers implement status/import/export/delete/boundary/build/show/capture. Scanner upload reuses the
|
||||
SHA-256 helper extracted from live view; `fp_venc` retains its soft-failure behavior. Import uploads canonical
|
||||
text. Export checks mesh counts; build uploads FPRS dumps, runs the scanner and cleans its staging directory.
|
||||
- CLI: `frameport frame room status|from-boundary|import|export|delete|build|show`, with `--frame` on each command.
|
||||
`show --output <path.svg>` writes a preview (default below `FRAMEPORT_HOME/rooms`). Build accepts `--universe`.
|
||||
- Frame page Room card: checklist, asset-URL preview, boundary/delete/capture actions, refresh and recoverable errors.
|
||||
SSH and preview writes run in a background worker; state is cached per connection. Scene-game pages link to Room
|
||||
setup when the connected Frame has no room. `ui_smoke.py --fake-frame` supplies local room-command answers.
|
||||
- Every USE_SCENE/USE_ANCHOR_API game gets the scene-emulation suggestion. Migration `scene_emul_all` enables it once
|
||||
in automatic recipes, preserving user recipes. NOPE and Pinball FX keep their other recipe fields and now request
|
||||
scene emulation. Triage explains guardian fallback and missing discovery; patch docs and translation template
|
||||
were regenerated.
|
||||
|
||||
## Integration questions and limits
|
||||
|
||||
1. Headset capture is intentionally unfinished: `room_capture` returns `{"error":"not implemented yet"}`. The
|
||||
`capture_roomview(seconds, out_path)` hook documents the pending RoomView queue work. The GUI explains this and
|
||||
offers the boundary path. Do not present the capture button as a completed scan workflow yet.
|
||||
2. This branch has no `fp_roomscan` artifact; merge the scan stream before using `frame room build`. The exact scanner
|
||||
invocation is `fp_roomscan --offline <dump> --out <temporary directory> --universe U --json`. Stdout must contain
|
||||
one JSON value, stderr is saved as `scan.log`, and output must include a valid `room.txt` with the requested U.
|
||||
3. Merge the emulation/shim stream before expecting games to use saved rooms, meshes or new Meta entry points.
|
||||
Runtime log spelling is owned there; triage accepts both existing `using guardian room`/dimension logs and the
|
||||
brief's `guardian box`/`room from guardian` messages. Reconcile new success logs at integration.
|
||||
4. Confirm the chaperone JSON path, exact universe-switch logs and whether collision quads are already in STAGE on
|
||||
the device. This implementation follows the brief's path and coordinate contract; it does not apply a second
|
||||
standing-origin transform. A universe match alone cannot detect Valve changing an origin without changing its ID.
|
||||
5. Agent and desktop perform independent validation because the deployed agent cannot import the desktop package.
|
||||
The desktop canonicalizes outlines and fills defaults; the agent preserves scanner output and validates it.
|
||||
The format contract needed no changes.
|
||||
6. Rooms are shared across games; per-game saved anchors are not deleted or rewritten by these commands. Replacing
|
||||
a room creates new entity UUIDs, so a game may need its own room-setup reset. No automatic game reset was added.
|
||||
|
||||
## Offline validation
|
||||
|
||||
All commands used `FRAMEPORT_HOME` in this worktree (tests use temporary isolated homes), an existing dependency
|
||||
venv and offline/no-sync uv settings. No dependencies or native toolchains were downloaded.
|
||||
|
||||
- Room, PC/CLI, UI, heuristics, migration, MR triage and translation checks passed: 63 tests. The related
|
||||
agent/launcher/screenshots/codec/gamepad regression group passed 170 tests. The final deployment/configuration
|
||||
regression check passed 48 tests, including all four recipe/settings precedence cases.
|
||||
- Ruff over changed Python files and `scripts/patch_docs.py --check` passed. The UI detector reported no findings.
|
||||
`scripts/i18n_extract.py` regenerated `src/frameport/locales/template.json`.
|
||||
- `pytest -m games`: 3 skipped (no game dumps). Dev/holdout evaluation used only the checked-in demo analysis
|
||||
snapshot, 21 catalog matches: dev dated 9/17 exact, F1 81.1%; holdout dated 1/4 exact, F1 73.3%. These are limited
|
||||
fixture results, not a new score for the owner's complete collection; the evaluator exits 1 for recipe differences.
|
||||
- Final full suite: **1790 passed, 40 skipped, 11 failed, 16 errors** in 87.35 s. All 27 failures/errors are
|
||||
`PermissionError: [Errno 1] Operation not permitted` when existing tests create sockets. The whole-suite green
|
||||
requirement cannot be met in this sandbox; rerun `uv run pytest -q` on a host permitting loopback sockets.
|
||||
Browser UI smoke also requires a loopback server, so it
|
||||
was not run. New UI tests construct the real Flet controls and exercise deferred preview/error/cache states.
|
||||
|
||||
## Exact device checks (maintainer only)
|
||||
|
||||
Run these after integrating emul + scan. Substitute the paired Frame address below; keep the current worktree's
|
||||
`FRAMEPORT_HOME=.home` and use explicit `--frame` so the test does not open another app data directory. The source
|
||||
connection syncs the agent by digest even without a version bump, unless the Frame has a newer agent version.
|
||||
The maintainer must perform integration version bumps before testing against a newer installed agent.
|
||||
|
||||
```sh
|
||||
export FRAME='steamos@<frame>'
|
||||
export PKG='com.gritGames.Nope' # repeat with com.zenstudios.PFXVRQuest and a known scene game
|
||||
uv run frameport frame info --frame "$FRAME"
|
||||
uv run frameport frame room status --frame "$FRAME" > .home/mr-status.json
|
||||
# Back up ALL rooms on the Frame before destructive checks; use an unused backup name.
|
||||
ssh "$FRAME" 'test ! -e ~/.local/share/frameport/rooms.before-mr-test && { test ! -d ~/.local/share/frameport/rooms || cp -a ~/.local/share/frameport/rooms ~/.local/share/frameport/rooms.before-mr-test; }'
|
||||
```
|
||||
|
||||
1. Inspect status: no room gives `room: null`, `aligned: false`. With Valve's default circle, `default_circle: true`;
|
||||
`uv run frameport frame room from-boundary --frame "$FRAME"` must fail with the default-circle explanation.
|
||||
Set a real play area in SteamVR, repeat `from-boundary`: expect source `boundary`, one wall per boundary edge,
|
||||
correct floor area and `aligned: true`. Open Frame → Room: visible preview, warning for the unscanned room,
|
||||
aligned row ready; the capture button should show the friendly unavailable message without crashing.
|
||||
2. Exercise import/export and preview with a manual fixture in the current universe:
|
||||
|
||||
```sh
|
||||
uv run frameport frame room status --frame "$FRAME" > .home/mr-status.json
|
||||
uv run python - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
from frameport.mr.room import Mesh, Volume, box_room, dumps, write_mesh
|
||||
p = Path('.home/mr-check'); p.mkdir(parents=True, exist_ok=True)
|
||||
r = box_room(4, 3, 2.5)
|
||||
r.source = 'manual'
|
||||
r.universe = json.loads(Path('.home/mr-status.json').read_text())['universe']
|
||||
r.volumes.append(Volume('a'*32, 'TABLE', .5, 0, .8, 20, 1.2, .7, .8))
|
||||
r.mesh = Mesh(vertices=[(-2, 0, -1.5), (2, 0, -1.5), (2, 0, 1.5)], indices=[0, 1, 2])
|
||||
(p/'room.txt').write_text(dumps(r))
|
||||
(p/'room-mesh.bin').write_bytes(write_mesh(r.mesh))
|
||||
PY
|
||||
uv run frameport frame room import .home/mr-check/room.txt --frame "$FRAME"
|
||||
uv run frameport frame room export .home/mr-export --frame "$FRAME"
|
||||
uv run frameport frame room show --output .home/mr-check/preview.svg --frame "$FRAME"
|
||||
cmp .home/mr-check/room-mesh.bin .home/mr-export/room-mesh.bin
|
||||
```
|
||||
|
||||
Expect four walls, one TABLE, area 12 m² and height 2.5 m. The preview must label TABLE and the game must show it
|
||||
hanging below its top-face anchor. Check the corresponding universe folder and `rooms/current` on the Frame.
|
||||
3. Use a recorded dump from the same standing universe (do not invent one). Copy it to `.home/mr-check/scan.fprs`,
|
||||
then run the command below. Expect the scanner JSON under `report`, source `scan`, aligned room data, `scan.log`
|
||||
on the Frame and an installed executable. Run twice: an unchanged scanner must not upload again.
|
||||
|
||||
```sh
|
||||
uv run frameport frame room build .home/mr-check/scan.fprs --frame "$FRAME"
|
||||
uv run frameport frame room status --frame "$FRAME"
|
||||
```
|
||||
|
||||
If the recording is from another universe, pass its original `--universe <id>`; do not relabel its coordinates.
|
||||
4. Prepare the game's source and its existing backed-up signing key in the isolated test library, then force a
|
||||
rebuild/reinstall after integration: `uv run frameport install "$PKG" --frame "$FRAME" --force --apk-only`.
|
||||
Repeat for both catalog games. In each deployment, expect `scene_room: true`. Run `frame info` twice to upgrade
|
||||
old launchers and check one marker only:
|
||||
|
||||
```sh
|
||||
uv run frameport frame info --frame "$FRAME"
|
||||
uv run frameport frame info --frame "$FRAME"
|
||||
ssh "$FRAME" "grep -c '# frameport-room-sync' ~/Applications/quest-frame/$PKG/launch.sh"
|
||||
uv run frameport settings "$PKG" scene_emul=1 --frame "$FRAME"
|
||||
```
|
||||
|
||||
Expected marker count: 1. Start the game with Steam's Play button in the headset, enter its MR mode, then inspect
|
||||
`<deployment.base>/room-sync.log`: JSON has the selected universe and alignment. The two room files must be
|
||||
beside `<deployment.base>/settings.conf`. FrameBridge must load that room; `xrDiscoverSpacesMETA` must resolve
|
||||
through the extension shim, with no `Unknown proc addr`/MRUK `NoRoomsFound`. Exact new native success log strings
|
||||
should be taken from the emul stream. Verify walls/table placement physically; never infer alignment from FPS.
|
||||
5. Exit the game. Switch `scene_emul=0` using Game settings (or the settings command above with 0): marker and game
|
||||
sidecars disappear. Switch it back to 1: one marker returns. Delete the room and launch again: stale copies are
|
||||
removed and existing FrameBridge guardian fallback logs trigger informational `mr-no-room` triage.
|
||||
6. To check a universe mismatch, back up/export the room, change its `universe` record to another valid ID and import
|
||||
it, then delete the original universe's room with `frame room delete --universe <original-id>`. Status must select
|
||||
`current` but report `aligned: false`; the Room card warns. Restore the original file before playing physically.
|
||||
The dummy universe 424242 should report current universe 0 rather than claim alignment.
|
||||
|
||||
## Undo device checks
|
||||
|
||||
Close all games. Restore the backed-up store (or remove only the fixture universes if there was no previous store):
|
||||
|
||||
```sh
|
||||
ssh "$FRAME" 'if test -d ~/.local/share/frameport/rooms.before-mr-test; then rm -rf ~/.local/share/frameport/rooms; mv ~/.local/share/frameport/rooms.before-mr-test ~/.local/share/frameport/rooms; fi'
|
||||
```
|
||||
|
||||
Set each tested game's `scene_emul` back to its recorded original value. To remove a test room without a backup:
|
||||
`uv run frameport frame room delete --universe <fixture-universe> --frame "$FRAME"`. Start a scene game once to
|
||||
refresh/remove its copies. The scanner can be removed with
|
||||
`ssh "$FRAME" 'rm -f ~/.local/share/frameport/bin/fp_roomscan'`; the next build reinstalls it. Game saves and
|
||||
`framebridge-anchors.txt` are untouched. Restore the previous app/agent/native artifacts through the maintainer's
|
||||
normal integration rollback if testing the combined native implementation fails.
|
||||
@@ -0,0 +1,452 @@
|
||||
# Offline room scanner (scan stream)
|
||||
|
||||
`fp_roomscan` converts recorded FPRS v1 meshes to the [room contract](ROOM_FORMAT.md), including the optional
|
||||
wall label adopted from upstream commit `8cb2783`:
|
||||
`room.txt` and `room-mesh.bin`, refined by default (see "Refinement" below), plus the unrefined `room-raw.txt` +
|
||||
`room-mesh-raw.bin` and `scan.json` (head path, settings, per-pass report, label decisions, door/closet
|
||||
suggestions; ROOM_FORMAT.md "Raw files and scan.json"). It does not capture RoomView queues, contact a headset, install rooms, or modify
|
||||
the current-room symlink. Scanner work changes `native/roomscan/`, its build target and artifact/checksum,
|
||||
`scripts/mr/synth_scan.py`, `tests/test_roomscan.py`, and this note. The upstream contract commit also brings its
|
||||
existing prompt assets and renderer unchanged. No runtime, agent, patch, or analysis versions were bumped.
|
||||
|
||||
## Build and run
|
||||
|
||||
Use the cached NDK; no downloads are needed for this target:
|
||||
|
||||
```sh
|
||||
python native/build.py --only roomscan
|
||||
```
|
||||
|
||||
The result is `artifacts/linux-arm64-bin/fp_roomscan`, a static aarch64 Linux ELF with its own `_start`, raw Linux
|
||||
syscalls, and no interpreter, dynamic section, libc, libm, UAPI headers, generated tables, or new dependencies.
|
||||
`platform.h` is the reconstruction code's entire I/O/allocation boundary. The same eight C sources compile on the
|
||||
host:
|
||||
|
||||
```sh
|
||||
cc -std=c11 -O2 -Wall -Wextra -Werror -ffp-contract=off -fno-math-errno -DFP_HOSTED native/roomscan/*.c -o /tmp/fp_roomscan
|
||||
python scripts/mr/synth_scan.py /tmp/room.fprs
|
||||
/tmp/fp_roomscan --offline /tmp/room.fprs --out /tmp/scanned-room --universe 0 --json
|
||||
```
|
||||
|
||||
The output directory's parent must exist; the scanner creates the final directory if needed. It always prints
|
||||
exactly one JSON summary, whether `--json` is supplied or omitted. Success exits 0, errors exit 2 with
|
||||
`{"ok":false,"error":"..."}`. `--help` prints usage and exits 0. Successful summaries contain:
|
||||
|
||||
```json
|
||||
{"ok":true,"frames":30,"skipped_frames":0,"far_vertices":0,"discontinuity_triangles":0,
|
||||
"walls":4,"detected_walls":4,"volumes":2,"floor_y":-0.0023,
|
||||
"ceiling_y":2.5992,"coverage_percent":100.0,"hint_used":false,"triangles":70997,
|
||||
"accepted_triangles":118804,"rejected_triangles":704,"heads":28,
|
||||
"refine":{"triangles":70997,"kept":7159,"structural":46121,"near_walls":148,"outside":0,"small_pieces":38,
|
||||
"thin_pieces":0,"erased":0,"walked":203,"tops":1896,"panel_triangles":12,"truncated":0,"walked_skipped":false,
|
||||
"floor_y":-0.0051,"ceiling_y":2.6028,"boxes":2,"relabelled":1,"removed_boxes":0,"suggestions":0,
|
||||
"plane_shifts":[{"kind":"wall","wall":0,"shift":-0.0004},...,{"kind":"ceiling","shift":-0.0021}],
|
||||
"output_triangles":7171,"output_volumes":2},"warnings":[],
|
||||
"timings_ms":{"read":0,"fusion":0,"model":0,"mesh":0,"refine":0,"total":0}}
|
||||
```
|
||||
|
||||
The example above illustrates the schema; actual counts and timings depend on the input. Heights are absolute
|
||||
STAGE coordinates. `frames` includes skipped frames; `skipped_frames`, `far_vertices`, and
|
||||
`discontinuity_triangles` count input exclusions once, not once per pass. `walls` counts output polygon
|
||||
edges including synthesized edges; `detected_walls` counts fitted physical planes. Coverage means the percentage
|
||||
of the output perimeter supported by observed wall planes, **not camera coverage of all room surfaces**. Timings
|
||||
include parsing, fusion, structural/furniture modelling, mesh generation, refinement, and total time including
|
||||
output. The scanner fields describe the unrefined scan (`triangles`, `volumes`, `ceiling_y` are the raw files');
|
||||
`refine` is the refinement report (same keys as scan.json's `report`) plus the refined `output_triangles` and
|
||||
`output_volumes`. `heads` counts the head-path points kept for the walked-volume pass.
|
||||
|
||||
All frame headers, timestamps/vertices/poses, approximately unit quaternions, counts, indices and EOF boundaries
|
||||
are checked before publication. A non-finite timestamp/pose/vertex or invalid tracking quaternion skips the entire
|
||||
frame after consuming and structurally validating its record, with a count and warning. A bad frame never erases
|
||||
usable frames elsewhere in the dump. Vertices outside the camera-relative range (default 4 m) and every triangle
|
||||
touching them are discarded. Triangles with a camera-depth jump over `--depth-jump + --depth-ratio * nearest_depth`
|
||||
are rejected before fusion; this complements long-edge and grazing rejection. Frame counts are capped at
|
||||
1 million vertices / 3 million indices;
|
||||
oversized records fail rather than allocating from untrusted counts. A header-only dump or one without usable
|
||||
triangles fails. Empty frames may accompany useful frames. Structural corruption (truncation, unsupported header,
|
||||
bad counts or indices) remains fatal, preserves any existing output byte for byte and creates no output directory.
|
||||
UUIDs come from `getrandom`, fresh per scan; mesh geometry is repeatable.
|
||||
|
||||
All files are computed first, then each is written to an exclusive, randomly named temporary file and renamed, in
|
||||
the order `room-mesh-raw.bin`, `room-raw.txt`, `scan.json`, `room-mesh.bin`, `room.txt` (meshes before their room
|
||||
text, `room.txt` last). A write failure removes the temporary files. These are atomic **file** replacements, not a
|
||||
multi-file filesystem transaction: the installer should scan into a fresh staging directory
|
||||
and publish that directory/symlink after success. Do not run concurrent writers into the same output directory.
|
||||
There is no fsync-based power-loss durability promise. Neither scan nor build writes application data outside
|
||||
the worktree's `FRAMEPORT_HOME`; fixtures and test output use temporary directories.
|
||||
|
||||
## Reconstruction
|
||||
|
||||
1. The first streaming pass validates every frame, rotates its mesh by the supplied quaternion, and translates
|
||||
it into STAGE. It rejects degenerate triangles, excessively long edges, out-of-range points, and grazing
|
||||
surfaces. Winding is corrected toward the observing camera. A deterministic bounded reservoir of 32,768
|
||||
accepted triangle centroids determines median position and padded bounds. No frames are retained across reads.
|
||||
2. The second pass rasterizes accepted triangles at 0.75 voxel spacing. Per-cell surfels average positions and
|
||||
inward normals, weighted by area and squared incidence. A capped accumulated weight lets later views correct
|
||||
earlier ones. The grid is sized from the sample bounds and capped at 8 x 8 x 4 metres about the data median.
|
||||
Unobserved cells have zero weight. This implementation uses a bounded dense backing array for the sparse
|
||||
observations, rather than a hash table; the 4.2-million-cell hard cap bounds all later work. Complete regular
|
||||
fans are recognized from their index topology, without hardcoding a camera resolution. Their normals come from
|
||||
12 x 12 neighbourhood covariance plane fits, rejecting patches above `--patch-residual` RMS; irregular meshes
|
||||
retain triangle normals. Each cell counts contributing frames once and needs `--min-views` evidence, capped at
|
||||
ten percent of usable frames (rounded down, at least one) for brief captures. This suppresses transient stereo
|
||||
geometry without requiring a short scan to see each surface five times.
|
||||
3. Horizontal normal/height histograms select the dominant floor within the STAGE floor band and a ceiling above
|
||||
the minimum ceiling height. Missing floor uses STAGE y=0 with a warning; missing ceiling uses floor plus
|
||||
`--ceiling-default`, warning exactly `ceiling not seen`. A ceiling must also have at least 15% as many supported
|
||||
cells as all upward-facing horizontal surfaces, so a few isolated down-facing patches do not invent a ceiling.
|
||||
4. RANSAC fits near-vertical planes using distance and normal agreement, then refines them by position covariance.
|
||||
Minimum height, span, and area reject furniture faces. Wall directions within the snap tolerance of the
|
||||
dominant wall's orthogonal directions are snapped, with offsets refitted. Directed wall segments are connected
|
||||
by nearest endpoints and adjacent line intersections. This preserves the L-shaped fixture's re-entrant corner.
|
||||
Parallel same-facing planes within 25 cm / approximately 14° merge, suppressing duplicate softened faces.
|
||||
Planes below 20% of the strongest original plane's area are discarded. When at least 65% of candidate wall area
|
||||
supports an orthogonal family, directions outside 12° of that family are discarded (`--snap-degrees 0` disables
|
||||
this orthogonal consensus filter). Large gaps close along observed endpoints. Ambiguous endpoints with three
|
||||
or four planes can use inward half-plane intersections; concave rooms retain segment ordering. Otherwise a
|
||||
warned observed-bounds rectangle is used. Polygon checks reject crossing edges. The output follows the contract's positive
|
||||
shoelace ordering in (x,z), with left-facing inward normals.
|
||||
5. Furniture tops are clustered separately from vertical faces, within supported horizontal height bands. A
|
||||
softened edge connecting a tabletop to a wall can no longer absorb the whole room into one box. Top boxes use
|
||||
a minimum-area horizontal orientation search and the median surface height; vertical screen candidates use
|
||||
face-direction harmonics / PCA. Bounds trim the outer 2% to suppress stereo halos. Top boxes extend down to the
|
||||
floor (the footprint and lower extent remain geometric estimates). Weak/sparse top patches are discarded.
|
||||
Labels follow height, area, long-side (with two voxels of measurement tolerance),
|
||||
thinness and near-wall rules, in order BED, COUCH, TABLE, SCREEN, STORAGE, OTHER. Only volumes are emitted;
|
||||
optional table plane records are not required. Floor/ceiling/wall surfaces are excluded before clustering.
|
||||
6. Surfel splats form an incidence/area-weighted signed distance field within three voxels of each surface. The
|
||||
sign is positive in observed free room space. Marching cubes traces zero crossings on cube faces, resolving
|
||||
ambiguous faces from their centre sign. It fans closed contours and orients triangles toward the positive
|
||||
gradient. Unknown corners suppress extraction rather than bridging unobserved space. If necessary, extraction
|
||||
is repeated on a coarser lattice (strides 2–4) until at most 200,000 triangles remain, with a decimation warning;
|
||||
excess beyond that fails. Vertices are unshared triangle vertices, so at most 600,000 vertices. Structural
|
||||
planes are semantic anchors, not substituted for the observed global mesh.
|
||||
|
||||
## Refinement
|
||||
|
||||
After the mesh, the scanner cleans the room the way the owner tuned it in the room-scan cleanup page (2026-10-18);
|
||||
`native/roomscan/refine.c` and `rules.c` port that page's `refine()` and label rules pass for pass (local parity
|
||||
check on the owner's recording: identical counts and bit-identical refined meshes, see below). `--refine 0`
|
||||
switches it off: room.txt and room-mesh.bin are then byte-for-byte what the scanner wrote before refinement existed
|
||||
(the raw files and scan.json are still written, so refinement can be switched on later without a new scan).
|
||||
|
||||
The input is exactly what the raw files hold: the raw room is written as text and read back (doubles parsed from the
|
||||
4-decimal text, like the page reads it), the head path is rounded the same way. Therefore `--refine-only <dir>`
|
||||
on the raw files reproduces the scan's refined files byte for byte. The mesh is welded at 1 mm (quantized, as the
|
||||
page does) for the passes that need topology; the output is again unshared triangle vertices.
|
||||
|
||||
1. **Label rules** (`rules.c`, `--label-rules 0` off) on the scanner's volumes, using the raw room (walls, outline,
|
||||
floor): a box that holds >= 60% of two boxes whose centres are >= 1 m apart covers several things (removed); a
|
||||
box >= 75% inside a bigger box with a top no higher (+5 cm) is part of it (removed); a centre > 10 cm outside the
|
||||
outline is behind a wall (removed). A tall thin slab from the floor (thinner than 22 cm, >= 1.8 m high, bottom
|
||||
<= 0.3 m, at most 0.5 m behind the wall line) becomes a door (<= 1.1 m wide) or closet **suggestion** in
|
||||
scan.json (box removed, nothing added: the GUI decides). Thin (< 22 cm) upright boxes are SCREEN only when
|
||||
raised >= 0.3 m, top <= 2.2 m, width/height >= 1.2, within 15 degrees of the nearest wall (when closer than
|
||||
0.35 m) and flat: the raw mesh's depth spread inside the box <= 3 cm (with >= 30 triangles; fewer = unknown,
|
||||
accepted); otherwise removed. Then BED (top 0.25-0.65 m, area >= 2 m^2, long side 1.8-3 m), COUCH (top
|
||||
0.35-0.6 m, long side >= 1.4 m), TABLE (top 0.6-1.1 m), STORAGE (>= 1.1 m high, >= 25 cm deep, within 0.35 m of a
|
||||
wall); anything taller than 1.1 m otherwise is removed, the rest is OTHER. Every box gets a reason in scan.json.
|
||||
These thresholds are constants in `rules.c` (the page's `RULES0`).
|
||||
2. **Furniture**: triangles within 10 cm of a ruled box (after the rules) are furniture; the passes below never
|
||||
classify, clear or walk them away, and the tubes pass keeps pieces that are mostly furniture.
|
||||
3. **Planes**: seen walls (`WALL_FACE` records), floor and ceiling. Each plane's offset moves to the median signed
|
||||
distance of the non-furniture triangles facing it (normal within `--facing-deg`) within `--band` of it (walls:
|
||||
within their length + band), when there are more than 50. An unseen ceiling (floor + 2.5 m) moves to the real
|
||||
one; the re-estimated ceiling is written to the refined room.txt (ceiling record and wall tops).
|
||||
4. **Structural triangles**: facing a plane, not furniture, behind it by less than `--band`, in front of it by less
|
||||
than `--band-in` (walls), `--floor-in`, `--ceil-in`; in a wall's top `--top-strip` below the ceiling the full
|
||||
band applies in front too (the depth bends where wall meets ceiling). The nearest qualifying plane owns it.
|
||||
5. **Erase spheres** (`--erase "x,y,z,r;..."`, from the GUI's eraser) drop every triangle whose centroid is inside.
|
||||
6. **Walked volume** (`--walked` = radius R, 0 off): where the user stood, nothing solid can be. Non-furniture
|
||||
triangles from 0.2 m above the floor up to 0.25 m above a head position, within R horizontally of it, go.
|
||||
Needs the head path (scan.json); without it the pass is skipped (`walked_skipped`).
|
||||
7. **Walls** (`--walls`): `panels` (default) removes the structural triangles, then non-furniture triangles closer
|
||||
than `--near` to any plane (within the wall's extent), and adds flat panels: one quad per seen wall (moved by its
|
||||
offset shift) from floor to ceiling, and the outline ear-clipped at the floor and the ceiling heights, all wound
|
||||
counter-clockwise seen from inside. `snap` instead projects every vertex of a structural triangle onto its plane
|
||||
(the last owner wins). `off` keeps them.
|
||||
8. **Furniture tops** (`--tops`, 0 off): upward-facing (normal y > 0.8) triangles within `--tops` of a box top and
|
||||
inside its footprint are flattened onto it.
|
||||
9. **Clip**: centroids further than `--clip` outside the outline (`--clip` + `--clip-open` past an unseen wall), or
|
||||
further than `--clip` above the ceiling / below the floor plane go. `--clip 1 --clip-open 2` switches it off.
|
||||
10. **Islands**: connected pieces smaller than `--island` m^2 go (`--protect 1` keeps furniture triangles).
|
||||
11. **Thin floating pieces** (`--tubes`, 0 off): pieces of >= 10 triangles, < 1 m^2, at most half furniture, whose
|
||||
longest principal axis (vertex PCA, std) is >= `--tubes` times the second and the second <= `--tube-thick`.
|
||||
12. **Smoothing**: `--smooth` Taubin iterations (lambda 0.5, mu -0.53) over the kept triangles.
|
||||
|
||||
Output: the kept scan triangles, then the panels (`refine.panel_triangles`), at most 200,000 triangles (more kept
|
||||
triangles would be cut, `truncated`, with a warning). The refinement adds about 20 ms on the host for the owner's
|
||||
64k-triangle scan; its buffers (welding hash, double positions, union-find, adjacency) stay below ~170 MiB for a
|
||||
200k-triangle mesh and the fusion grid is freed before it runs, so peak memory is still the fusion phase's.
|
||||
|
||||
`--refine-only <room dir> [refinement options] [--json]` runs only steps 1-12 from `room-raw.txt` +
|
||||
`room-mesh-raw.bin` (+ the head path from `scan.json`) and rewrites scan.json, room-mesh.bin and room.txt. A folder
|
||||
from an older scanner without raw files: its room.txt + mesh become the raw files first (the room text is rewritten
|
||||
with the mesh record pointing at `room-mesh-raw.bin`), a warning says so, and there is no head path. Its JSON:
|
||||
`{"ok":true,"refine_only":true,"raw_triangles":..,"volumes":..,"floor_y":..,"ceiling_y":..,"heads":..,
|
||||
"refine":{...},"warnings":[...],"timings_ms":{"refine":..,"total":..}}`.
|
||||
|
||||
Local parity (2026-10-18, private recording `room1.fprs`, not in the repository): the reference page's functions run
|
||||
under node on this scanner's raw output give the same per-pass counts, the same label decisions and reasons, and
|
||||
bit-identical refined meshes for the defaults, snap, off + protect, erase spheres + tubes 2, and a wide-band setting
|
||||
(one triangle differs there: a facing test exactly at the threshold), and the same on an older 128k-triangle room
|
||||
folder. Distance ties at outline corners decide which wall's clip allowance applies, so the corner distance uses
|
||||
V8's Math.hypot algorithm.
|
||||
|
||||
Peak allocation stays below 260 MiB, including maximum frame buffers, the grid, model worklists and mesh buffers
|
||||
(the latter phases do not retain frame buffers). Default synthetic scenes use a much smaller grid. Thresholds
|
||||
are CLI options below or named constants next to their algorithms. No room-specific coordinates or dimensions are
|
||||
used; on-device quality and speed still need validation.
|
||||
|
||||
## Tuning
|
||||
|
||||
Distances are metres, areas m², volumes m³. `--universe` accepts the full unsigned 64-bit range; 0 means unknown.
|
||||
|
||||
| Option | Default | Accepted range / meaning |
|
||||
| --- | ---: | --- |
|
||||
| `--voxel` | 0.05 | 0.04–0.20, spatial resolution; lower values cost more memory/work |
|
||||
| `--ceiling-default` | 2.5 | 1.8–3.8, height above floor when ceiling is unseen |
|
||||
| `--truncation` | 3 | 1–5, TSDF band in voxel units |
|
||||
| `--max-edge` | 0.6 | 0.05–1.5, reject longer triangle edges |
|
||||
| `--grazing` | 0.12 | 0.01–0.9, minimum absolute camera/normal cosine |
|
||||
| `--range` | 4 | 1–10, maximum camera-relative vertex distance; >4 m opts back into inpainted depth |
|
||||
| `--depth-jump` | 0.25 | 0.02–1, absolute part of a triangle's maximum depth discontinuity |
|
||||
| `--depth-ratio` | 0.12 | 0–0.5, additional fraction of nearest vertex depth |
|
||||
| `--min-views` | 5 | 1–16, temporal support target; brief scans cap it as described above |
|
||||
| `--patch-residual` | 0.03 | 0.005–0.1, maximum RMS residual of a regular-fan normal patch |
|
||||
| `--plane-distance` | 0.045 | 0.01–0.15, point-to-plane inlier tolerance |
|
||||
| `--min-plane-area` | 0.45 | 0.1–4, minimum occupied plane area estimate |
|
||||
| `--wall-height` | 1.5 | 0.5–3, minimum observed vertical wall span |
|
||||
| `--snap-degrees` | 5 | 0–15, orthogonal snap tolerance |
|
||||
| `--floor-band` | 0.35 | 0.05–1, floor search distance from STAGE y=0 |
|
||||
| `--ceiling-min` | 1.8 | 1.5–3, minimum absolute ceiling y |
|
||||
| `--min-volume` | 0.05 | 0.005–1, reject smaller furniture boxes |
|
||||
| `--cluster-gap` | 0.10 | 0.04–0.20, voxel neighbourhood gap (rounded up to cells) |
|
||||
| `--gap` | 0.65 | 0.1–1.5, maximum adjacent-wall endpoint extrapolation |
|
||||
| `--horizontal` | 0.85 | 0.6–0.99, minimum floor/ceiling/top normal cosine |
|
||||
|
||||
Refinement (all distances metres; defaults = the owner's settings):
|
||||
|
||||
| Option | Default | Accepted range / meaning |
|
||||
| --- | ---: | --- |
|
||||
| `--refine` | 1 | 0/1; 0 = the unrefined output in room.txt/room-mesh.bin |
|
||||
| `--walls` | panels | `off`, `snap` (project onto the planes) or `panels` (flat wall/floor/ceiling panels) |
|
||||
| `--band` | 0.35 | 0.02–1, structural band behind a surface; plane re-estimation window |
|
||||
| `--facing-deg` | 60 | 5–89, maximum angle between a triangle's normal and the surface's |
|
||||
| `--band-in` | 0.12 | 0–1, band in front of a wall (room side) |
|
||||
| `--floor-in` | 0.08 | 0–1, band above the floor |
|
||||
| `--ceil-in` | 0.25 | 0–1, band below the ceiling |
|
||||
| `--top-strip` | 0.5 | 0–2, top part of a wall where the full band applies in front too |
|
||||
| `--near` | 0.03 | 0–0.3, panels: also clear non-furniture scan this close to a plane |
|
||||
| `--clip` | 0.05 | 0–1, clip distance outside the outline / beyond floor and ceiling |
|
||||
| `--clip-open` | 0.10 | 0–2, extra allowance past unseen walls (`--clip 1 --clip-open 2` = no clipping) |
|
||||
| `--island` | 0.20 | 0–5 m^2, minimum connected piece area (0 off) |
|
||||
| `--smooth` | 8 | 0–50 integer, Taubin iterations |
|
||||
| `--tops` | 0.10 | 0–0.5, flatten upward scan this close to a box top (0 off) |
|
||||
| `--protect` | 0 | 0/1, islands never remove furniture triangles |
|
||||
| `--tubes` | 3 | 0–20, elongation (PCA axis ratio) of a thin floating piece (0 off) |
|
||||
| `--tube-thick` | 0.15 | 0.01–1, maximum second PCA axis (std) of a thin floating piece |
|
||||
| `--walked` | 0.40 | 0 or 0.05–1.5, walked-volume radius around the head path (0 off) |
|
||||
| `--label-rules` | 1 | 0/1, label rules on the scanner's volumes |
|
||||
| `--erase` | none | `"x,y,z,r;..."`, up to 64 spheres (r 0–5 m) whose scan is removed |
|
||||
|
||||
Additional constants in `model.c` include RANSAC trials, normal agreement, wall span, furniture margins,
|
||||
label height/area thresholds, box trimming and circle recognition. Fusion sampling/weight limits are in
|
||||
`fusion.c`; mesh splat radii/decimation limits in `mesh.c`; hard resource limits in `scan.h`.
|
||||
|
||||
`--hint outline.txt` accepts one `x z` point per line, decimal/scientific notation, optional `#` comments, either
|
||||
winding, at most 96 points / 8 KiB. At least eight points with radial coefficient of variation below 5% count as
|
||||
the default circle and are ignored. A non-circular hint is used only when walls are incomplete **and every
|
||||
observed wall has a compatible hint edge** within `--gap`. Complete scans are never constrained to chaperone.
|
||||
Incompatible inset play boundaries are ignored with a warning. Hint-completed edges retain synthesized status.
|
||||
|
||||
## Limits for integration
|
||||
|
||||
- Synthesized wall records now append `INVISIBLE_WALL_FACE` as their optional eighth field. Observed walls omit
|
||||
the label and default to `WALL_FACE`; comments are no longer used to carry semantics. This matches upstream's
|
||||
authorized contract change exactly; adapters need that same contract revision.
|
||||
- A private RoomView recording was used for offline robustness work. It, its derived geometry, and its results
|
||||
stay outside committed files. Synthetic fixtures independently exercise the input characteristics. They are
|
||||
regressions, not proof
|
||||
of headset accuracy. Furniture labels are geometric heuristics; adjacent/touching objects can merge, occluded
|
||||
objects may split, top-only clusters have inferred depth, and thin fragments can be screen candidates. Nearby
|
||||
parallel walls and small wall jogs can merge under coarse depth uncertainty. Large disconnected coplanar wall segments are
|
||||
currently fitted together; complicated rooms can fall back to a warned rectangle.
|
||||
- Cropped or sparse captures can produce an incomplete mesh and synthesized outline. Surfaces are not filled
|
||||
through unseen regions. Furniture is not an accessibility/safety boundary. Rooms beyond the grid cap are
|
||||
cropped with a warning. Default-circle recognition may also ignore an intentionally circular boundary.
|
||||
- Output paths are fixed by the contract. The plumbing stream should stage a new directory and switch `current`
|
||||
after scanner success, preserving the previous room for rollback. Assign the recording's actual universe id;
|
||||
this standalone scanner cannot query SteamVR or validate universe alignment.
|
||||
|
||||
## Offline validation
|
||||
|
||||
`tests/test_roomscan.py` builds with the host compiler (skips only if none exists). Its tests cover the noisy
|
||||
4 x 3 x 2.6 m room with table/couch, six-wall L, moving and rotated/translated mesh poses, ceiling fallback, a single
|
||||
frame, missing walls, circular/matching hints, correct UUIDs/wire layout, interior mesh winding, triangle budget,
|
||||
repeatable mesh bytes with fresh UUIDs, truncated/corrupt dumps, invalid counts/indices/poses and invalid CLI values.
|
||||
Refinement tests: a floating pole and a column in the walked path (`synth_scan.py` `TUBE`/`GHOST`, `extra=`)
|
||||
are meshed by the scanner and removed by the tubes / walked passes (each kept with its pass off, the table top kept);
|
||||
panels wind into the room on the re-estimated planes; `--refine-only` reproduces a scan's files byte for byte;
|
||||
`--refine 0` equals the raw output (and refining it later equals the default); scan.json settings/report/heads; head
|
||||
path decimation to <= 2000 points; erase spheres; the label rules on a hand-written older room folder (merged,
|
||||
nested, behind a wall, SCREEN from a flat panel, a door suggestion with wall and position, unknown records kept,
|
||||
idempotent, `--label-rules 0`); invalid refinement options.
|
||||
The test module takes about fourteen seconds including synthetic rendering and compilation (under the 60 s budget).
|
||||
New cases cover non-finite frames before/after usable frames, per-vertex far rejection, depth jumps below the edge
|
||||
limit, weak ceiling evidence, wall labels and a complete camera-fan scan. The deterministic stdlib renderer now
|
||||
defaults to a 132 x 128 full index fan (99,822 indices), +Z forward / -Y image up, with poses mapping to STAGE.
|
||||
It adds separable depth smoothing, spatially correlated noise, far inpainting, two initial NaN poses, 3 Hz timestamps
|
||||
and head-height drift from 0.9 to 1.7 m. `--profile legacy` retains the smaller 110° x 76° test cone. New CLI controls
|
||||
are `--profile`, `--invalid-start`, `--smoothing` and optional resolution overrides. Use `--invalid-start 0` for a
|
||||
usable one-frame fixture; default RoomView warm-up frames otherwise have no valid tracking.
|
||||
|
||||
```sh
|
||||
UV_OFFLINE=1 UV_CACHE_DIR=/tmp/frameport-uv-cache uv run pytest -q tests/test_roomscan.py
|
||||
UV_OFFLINE=1 UV_CACHE_DIR=/tmp/frameport-uv-cache uv run ruff check scripts/mr/synth_scan.py tests/test_roomscan.py
|
||||
```
|
||||
|
||||
The synthetic camera fan passes AddressSanitizer + UndefinedBehaviorSanitizer. Two consecutive final native builds
|
||||
produce identical SHA-256 `2cc16b90abcd3d359f1658c581172bd650017bf2725e24268915f50904c000ec` (97,808 bytes, with refinement),
|
||||
matching `artifacts/SHA256SUMS`. ELF inspection confirms aarch64, little-endian, no PT_INTERP / PT_DYNAMIC, and no
|
||||
embedded local build paths.
|
||||
|
||||
The required full `uv run pytest -q` run completed with **1,788 passed, 40 skipped, 11 failed, 16 errors** in 79 s.
|
||||
All 27 failures/errors are the sandbox's `PermissionError: [Errno 1] Operation not permitted` on socket creation
|
||||
in existing HTTP/pairing/live-stream tests (`test_deeplink`, `test_devkit`, `test_install_links_extra`,
|
||||
`test_livestream`, `test_setup_url`, `test_ui_state`). This environment forbids sockets, including loopback.
|
||||
The unchanged baseline had the identical 27 failures/errors before the scanner tests were added (1,747 passed).
|
||||
Consequently the whole-suite-pass requirement cannot be satisfied here; rerun it outside that restriction at
|
||||
integration. No tests were changed/skipped to hide the restriction and no network/device access was attempted.
|
||||
No patch/settings/catalog/triage files were touched, so the conditional patch documentation regeneration is
|
||||
unnecessary.
|
||||
|
||||
## Ten-minute capture budget (synthetic benchmark)
|
||||
|
||||
The scanner is streaming and single-threaded. On an Intel Core i7-14700K host, a **synthetic-only** 1,800-frame
|
||||
132 x 128 dump (1,083,751,208 bytes, equivalent to ten minutes at 3 Hz) took **7.41 s elapsed / 7.08 s CPU**, with
|
||||
**27,208 KiB maximum RSS** at the default voxel size. The file repeats a 30-view synthetic sweep, including its
|
||||
warm-up frames; it tests parsing/fusion throughput rather than 1,800 independent viewpoints. Geometry-dependent
|
||||
model/mesh work and storage caches affect timing. No private recording or derived room data is in that benchmark.
|
||||
|
||||
Reproduce with bounded copies rather than loading a gigabyte into Python:
|
||||
|
||||
```sh
|
||||
python scripts/mr/synth_scan.py /tmp/roomview.fprs
|
||||
python - <<'PY'
|
||||
import shutil
|
||||
with open('/tmp/roomview.fprs', 'rb') as source, open('/tmp/roomscan-1800.fprs', 'wb') as out:
|
||||
out.write(source.read(8))
|
||||
for repeat in range(60):
|
||||
source.seek(8)
|
||||
shutil.copyfileobj(source, out, 1024 * 1024)
|
||||
PY
|
||||
/usr/bin/time -v /tmp/fp_roomscan --offline /tmp/roomscan-1800.fprs --out /tmp/roomscan-benchmark --json
|
||||
```
|
||||
|
||||
For the Frame's Cortex-A720/X4-class cores, budgeting **3–5 times the host CPU time plus storage** gives roughly
|
||||
**23–38 s** for this workload, leaving headroom under the requested ~60 s. This is an estimate, not an ARM
|
||||
measurement or an 8-core speedup claim. Worst-case allocations are bounded below 260 MiB independently of frame
|
||||
count (48-byte cells, 4.2-million-cell cap, bounded frame buffers/worklists), below both the original 300 MB and
|
||||
follow-up 600 MB limits. A maximum-sized complex room can add model/mesh work beyond this small synthetic sweep.
|
||||
The maintainer must time a ten-minute real capture on the Frame and check CPU time, wall time and RSS before
|
||||
claiming the target is achieved on-device. Hardware square root instructions replace iterative division; neither
|
||||
build links libm. The build and host command both require `-fno-math-errno` for that intrinsic.
|
||||
|
||||
## Exact device checks (maintainer only; none executed by this stream)
|
||||
|
||||
1. On the development PC, build, then render the canonical fixtures:
|
||||
|
||||
```sh
|
||||
python native/build.py --only roomscan
|
||||
python scripts/mr/synth_scan.py /tmp/box.fprs
|
||||
python scripts/mr/synth_scan.py /tmp/l.fprs --shape l
|
||||
python scripts/mr/synth_scan.py /tmp/down.fprs --no-ceiling
|
||||
python scripts/mr/synth_scan.py /tmp/one.fprs --frames 1 --invalid-start 0
|
||||
scp artifacts/linux-arm64-bin/fp_roomscan /tmp/box.fprs /tmp/l.fprs /tmp/down.fprs /tmp/one.fprs steamos@<FRAME>:~/Downloads/
|
||||
```
|
||||
|
||||
2. In a terminal **on the Frame**, use an isolated directory; no game or current room is changed:
|
||||
|
||||
```sh
|
||||
mkdir -p "$HOME/.local/share/frameport/roomscan-test"
|
||||
cd "$HOME/.local/share/frameport/roomscan-test"
|
||||
cp "$HOME/Downloads/fp_roomscan" ./fp_roomscan
|
||||
chmod 700 ./fp_roomscan
|
||||
./fp_roomscan --help
|
||||
./fp_roomscan --offline "$HOME/Downloads/box.fprs" --out box --universe 0 --json > box.json
|
||||
cat box.json
|
||||
./fp_roomscan --offline "$HOME/Downloads/l.fprs" --out l --json > l.json
|
||||
./fp_roomscan --offline "$HOME/Downloads/down.fprs" --out down --ceiling-default 2.7 --json > down.json
|
||||
./fp_roomscan --offline "$HOME/Downloads/one.fprs" --out one --json > one.json
|
||||
```
|
||||
|
||||
Expected JSON: box has `ok:true`, `walls:4`, `detected_walls:4`, `volumes:2`, floor within 5 cm of 0,
|
||||
ceiling within 5 cm of 2.6, coverage 100; the only expected box warnings concern skipped warm-up frames and
|
||||
out-of-range vertices. `box/room.txt` includes TABLE and COUCH volume records.
|
||||
L has six walls and the concave corner near (0,0), rather than a rectangle. Down includes exactly the warning
|
||||
string `ceiling not seen` (other warnings are expected), with ceiling minus floor 2.7. One includes
|
||||
`few frames; scan more viewpoints`. Every mesh should have at most 200,000 triangles. JSON is the scanner log;
|
||||
there are no expected stderr status lines.
|
||||
|
||||
3. Verify sidecars, wall labels and the corrupt-input exit status on the Frame:
|
||||
|
||||
```sh
|
||||
python3 - <<'PY'
|
||||
import json, pathlib, struct
|
||||
for name in ('box', 'l', 'down', 'one'):
|
||||
report = json.loads(pathlib.Path(name + '.json').read_text())
|
||||
data = pathlib.Path(name, 'room-mesh.bin').read_bytes()
|
||||
magic, version, nv, ni = struct.unpack_from('<4sIII', data)
|
||||
assert report['ok'] and magic == b'FPRM' and version == 1
|
||||
assert ni % 3 == 0 and 0 < ni // 3 <= 200000
|
||||
assert len(data) == 16 + nv * 12 + ni * 4
|
||||
for line in pathlib.Path(name, 'room.txt').read_text().splitlines():
|
||||
if line.startswith('wall '):
|
||||
fields = line.split()
|
||||
assert len(fields) in (8, 9)
|
||||
if len(fields) == 9:
|
||||
assert fields[-1] == 'INVISIBLE_WALL_FACE'
|
||||
print(name, nv, ni // 3, report['warnings'])
|
||||
pathlib.Path('broken.fprs').write_bytes(b'FPRS\x01\0\0\0\0')
|
||||
PY
|
||||
./fp_roomscan --offline broken.fprs --out broken --json
|
||||
echo "$?"
|
||||
test ! -e broken/room.txt
|
||||
test ! -e broken/room-mesh.bin
|
||||
```
|
||||
|
||||
Expected: `{"ok":false,"error":"truncated frame header"}`, exit status 2, no `broken` outputs.
|
||||
|
||||
4. Record actual RoomView dumps with the maintainer's recorder (FPRS v1, mesh-to-STAGE poses, stable universe).
|
||||
Copy the recording to `~/Downloads/capture.fprs`. Look around slowly for about a minute, including floor,
|
||||
ceiling, every wall, and furniture from several directions. With the **recorded universe id** assigned below:
|
||||
|
||||
```sh
|
||||
scan_universe=0 # replace 0 with the recorder's actual decimal universe id
|
||||
/usr/bin/time -v ./fp_roomscan --offline "$HOME/Downloads/capture.fprs" --out real --universe "$scan_universe" --json > real.json 2> real-timing.log
|
||||
cat real.json real-timing.log
|
||||
cat real/room.txt
|
||||
```
|
||||
|
||||
Measure wall offsets/yaws, ceiling and table top physically and compare to the records (target 5 cm / 2°).
|
||||
Check resident memory below 300 MB, sensible coverage/warnings, tabletop labels and no false wall/furniture
|
||||
blobs from smeared edges. Repeat with `--max-edge 0.35 --grazing 0.2` into `real-stricter` if edges smear;
|
||||
try `--voxel 0.08 --plane-distance 0.07` into `real-coarser` if depth is noisy. Keep both JSON logs and raw
|
||||
dumps **outside git** for threshold calibration. These are proposed tuning probes, not established device fixes.
|
||||
|
||||
5. Repeat the synthetic 1,800-frame benchmark from the preceding section on the Frame, using the same command
|
||||
and `/usr/bin/time -v`. Then test a ten-minute independent real capture. Target: about 60 s elapsed or less,
|
||||
less than 600 MB RSS, successful JSON and bounded mesh size. Record CPU time as well as wall time so storage
|
||||
overhead is visible. With difficult stereo edges compare `--min-views 8 --patch-residual 0.02` in a fresh output
|
||||
directory; short captures cap view support automatically. Include the JSON's exclusion counters in triage;
|
||||
skipped frames may occur anywhere in the capture. Keep all personal results outside git.
|
||||
|
||||
6. After merging plumbing/emulation, publish this staged room through that stream's room-install path and try
|
||||
Demeter, then an MRUK discovery/mesh game. Check room loading, furniture alignment, and warning/fallback
|
||||
behaviour in-headset; the scanner alone cannot verify the Meta API path. Do not replace `rooms/current` by hand
|
||||
during these standalone checks.
|
||||
|
||||
Undo standalone checks: remove only the isolated `~/.local/share/frameport/roomscan-test` directory and the five
|
||||
test files copied to `~/Downloads` (plus `capture.fprs` if no longer needed). No Steam settings, games, agent,
|
||||
room universe folder, or current-room symlink were changed by these steps.
|
||||
@@ -87,6 +87,7 @@ shots:
|
||||
steps:
|
||||
- go: monitor
|
||||
- wait_for: "Processes"
|
||||
- hook: monitor_settled # the history replay finished (a slow runner shot it halfway: numbers differed)
|
||||
|
||||
# newer screens (docs/SHOWCASE.md's gallery; the README picks what it needs)
|
||||
- name: live-view # idle: a running stream shows its relay's random port and the data rate (no stable picture)
|
||||
@@ -107,3 +108,21 @@ shots:
|
||||
steps:
|
||||
- go: settings
|
||||
- wait_for: "Portal (OLED)"
|
||||
|
||||
- name: room # experimental: the pretend Frame's made-up bedroom scan (fakes.fake_room), no real scan
|
||||
docs: [README.md, docs/INSTALL.md]
|
||||
viewport: [1280, 1690]
|
||||
steps:
|
||||
- hook: open_room
|
||||
- wait_for: "Furniture & screens"
|
||||
- park: true
|
||||
|
||||
- name: room-editing # a box picked in the list (its type, place and size) next to the clean-up's labels
|
||||
docs: [docs/INSTALL.md]
|
||||
viewport: [1280, 2000] # (tall enough that nothing scrolls)
|
||||
steps:
|
||||
- hook: open_room
|
||||
- wait_for: "Furniture & screens"
|
||||
- hook: {name: room_editing, args: [box]}
|
||||
- park: true
|
||||
crop: {anchor: "Furniture & screens", left: -26, top: -28, width: 670, height: 1262}
|
||||
@@ -0,0 +1,151 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <errno.h>
|
||||
#ifndef MAP_FIXED_NOREPLACE
|
||||
#define MAP_FIXED_NOREPLACE 0x100000
|
||||
#endif
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
|
||||
static size_t mx_audio_page;
|
||||
static int mx_audio_branch(uintptr_t from,uintptr_t to,uint32_t *word) {
|
||||
if((from|to)&3)return 0;
|
||||
if(to>=from) {if(to-from>=((uintptr_t)1<<27))return 0;}
|
||||
else if(from-to>((uintptr_t)1<<27))return 0;
|
||||
*word=0x14000000u|((uint32_t)((to-from)>>2)&0x03ffffffu);
|
||||
return 1;
|
||||
}
|
||||
static void mx_audio_jump(unsigned char *out,uintptr_t destination) {
|
||||
const uint32_t jump[2]={0x58000050u,0xd61f0200u};
|
||||
memcpy(out,jump,8);memcpy(out+8,&destination,8);
|
||||
}
|
||||
struct mx_audio_candidate { uintptr_t address,distance; };
|
||||
static int mx_audio_candidate_order(const void *a,const void *b) {
|
||||
const struct mx_audio_candidate *x=a,*y=b;
|
||||
if(x->distance!=y->distance)return x->distance<y->distance?-1:1;
|
||||
return x->address<y->address?-1:x->address>y->address;
|
||||
}
|
||||
// Maps are sorted by address. Clip every free gap to the intersection of both
|
||||
// branch ranges; enumerate pages, then sort outward (lower address breaks ties).
|
||||
static int mx_audio_candidates(FILE *maps,uintptr_t target,
|
||||
struct mx_audio_candidate **out,size_t *count,const char **failure) {
|
||||
uintptr_t radius=(uintptr_t)1<<27;
|
||||
uintptr_t low=target>radius?target-radius:0;
|
||||
uintptr_t high=target>UINTPTR_MAX-radius?UINTPTR_MAX:target+radius;
|
||||
uintptr_t cursor=low;
|
||||
size_t capacity=0,line_capacity=0;char *line=NULL;
|
||||
*out=NULL;*count=0;
|
||||
for(;;) {
|
||||
unsigned long long start,end;
|
||||
*failure="read /proc/self/maps";
|
||||
int eof=getline(&line,&line_capacity,maps)<0;
|
||||
if(eof && (ferror(maps) || !feof(maps)))goto fail;
|
||||
if(!eof && (sscanf(line,"%llx-%llx",&start,&end)!=2 || end<=start)) {*failure="parse /proc/self/maps";errno=EINVAL;goto fail;}
|
||||
uintptr_t stop=eof?high:(uintptr_t)start;
|
||||
if(stop>high)stop=high;
|
||||
uintptr_t page=cursor;
|
||||
if(page%mx_audio_page)page+=mx_audio_page-page%mx_audio_page;
|
||||
for(;page<stop && stop-page>=mx_audio_page;page+=mx_audio_page) {
|
||||
uint32_t word;
|
||||
if(!page || !mx_audio_branch(target,page,&word) ||
|
||||
!mx_audio_branch(page+20,target+4,&word))continue;
|
||||
if(*count==capacity) {
|
||||
size_t next=capacity?capacity*2:64;
|
||||
void *p=realloc(*out,next*sizeof(**out));
|
||||
if(!p){*failure="allocate stub candidates";goto fail;}
|
||||
*out=p;capacity=next;
|
||||
}
|
||||
(*out)[(*count)++]=(struct mx_audio_candidate){page,page>target?page-target:target-page};
|
||||
}
|
||||
if(eof || stop==high)break;
|
||||
if((uintptr_t)end>cursor)cursor=(uintptr_t)end;
|
||||
if(cursor>=high)break;
|
||||
}
|
||||
free(line);
|
||||
if(*count)qsort(*out,*count,sizeof(**out),mx_audio_candidate_order);
|
||||
return 1;
|
||||
fail:
|
||||
{int saved=errno;free(line);free(*out);*out=NULL;*count=0;errno=saved;return 0;}
|
||||
}
|
||||
// Callers serialize installation. Only the verified SUB SP / STP prologues
|
||||
// below may be replayed: no PC-relative instruction or branch relocation.
|
||||
static void *mx_audio_detour(unsigned char *address,void *replacement,const uint32_t expected[4],void *original_slot) {
|
||||
uint32_t branch;
|
||||
uintptr_t target=(uintptr_t)address;
|
||||
const char *failure=NULL;int failure_errno=0,rollback_errno=0;
|
||||
if(target&3)failure="unaligned entry";
|
||||
else if(memcmp(address,expected,16))failure="16-byte guard mismatch";
|
||||
else if(expected[0]!=0xd10743ffu && expected[0]!=0xa9bd7bfdu &&
|
||||
expected[0]!=0xd10243ffu && expected[0]!=0xd10183ffu)
|
||||
failure="unsupported replay instruction";
|
||||
if(failure) {
|
||||
LOG("FrameBridge audio detour: target=%p failed: %s; entry unchanged",address,failure);
|
||||
return NULL;
|
||||
}
|
||||
if(!mx_audio_page || (mx_audio_page&(mx_audio_page-1))) {
|
||||
LOG("FrameBridge audio detour: target=%p failed: invalid page size=%zu; entry unchanged",address,mx_audio_page);
|
||||
return NULL;
|
||||
}
|
||||
struct mx_audio_candidate *candidates=NULL;size_t count=0;
|
||||
FILE *maps=fopen("/proc/self/maps","r");
|
||||
if(!maps) {
|
||||
LOG("FrameBridge audio detour: target=%p failed: open /proc/self/maps errno=%d; entry unchanged",address,errno);
|
||||
return NULL;
|
||||
}
|
||||
int parsed=mx_audio_candidates(maps,target,&candidates,&count,&failure);
|
||||
int parse_errno=errno;fclose(maps);
|
||||
if(!parsed) {
|
||||
LOG("FrameBridge audio detour: target=%p failed: %s errno=%d; entry unchanged",address,failure,parse_errno);
|
||||
return NULL;
|
||||
}
|
||||
unsigned char *code=MAP_FAILED;void *result=MAP_FAILED,*attempt=NULL;
|
||||
void *closest=count?(void*)candidates[0].address:NULL;
|
||||
int map_errno=0;size_t attempts=0;
|
||||
for(size_t i=0;i<count;i++) {
|
||||
attempt=(void*)candidates[i].address;attempts++;
|
||||
result=mmap(attempt,mx_audio_page,PROT_READ|PROT_WRITE,
|
||||
MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED_NOREPLACE,-1,0);
|
||||
if(result==MAP_FAILED) {map_errno=errno;continue;}
|
||||
// Old kernels may ignore NOREPLACE. Never accept a relocated mapping.
|
||||
if(result!=attempt) {munmap(result,mx_audio_page);map_errno=0;continue;}
|
||||
code=result;break;
|
||||
}
|
||||
free(candidates);
|
||||
if(code==MAP_FAILED) {
|
||||
LOG("FrameBridge audio detour: target=%p failed: stub allocation closest=%p attempted=%p mmap=%p errno=%d attempts=%zu; no safe ELF slack fallback; entry unchanged",
|
||||
address,closest,attempt,result,map_errno,attempts);
|
||||
return NULL;
|
||||
}
|
||||
uint32_t back;
|
||||
if(!mx_audio_branch(target,(uintptr_t)code,&branch) ||
|
||||
!mx_audio_branch((uintptr_t)code+20,target+4,&back)) {
|
||||
failure="branch range validation";goto fail;
|
||||
}
|
||||
mx_audio_jump(code,(uintptr_t)replacement);
|
||||
memcpy(code+16,expected,4);memcpy(code+20,&back,4);
|
||||
__builtin___clear_cache((char*)code,(char*)code+24);
|
||||
if(mprotect(code,mx_audio_page,PROT_READ|PROT_EXEC)) {failure="mprotect stub RX";failure_errno=errno;goto fail;}
|
||||
unsigned char *page=(void*)(target&~(mx_audio_page-1));
|
||||
if(mprotect(page,mx_audio_page,PROT_READ|PROT_WRITE|PROT_EXEC)) {failure="mprotect entry RWX";failure_errno=errno;goto fail;}
|
||||
// Publish the callable original before any worker can enter the hook.
|
||||
if(original_slot)__atomic_store_n((void**)original_slot,code+16,__ATOMIC_RELEASE);
|
||||
__atomic_store_n((uint32_t*)address,branch,__ATOMIC_RELEASE);
|
||||
__builtin___clear_cache((char*)address,(char*)address+4);
|
||||
if(mprotect(page,mx_audio_page,PROT_READ|PROT_EXEC)) {
|
||||
failure_errno=errno;
|
||||
__atomic_store_n((uint32_t*)address,expected[0],__ATOMIC_RELEASE);
|
||||
__builtin___clear_cache((char*)address,(char*)address+4);
|
||||
if(mprotect(page,mx_audio_page,PROT_READ|PROT_EXEC))
|
||||
rollback_errno=errno; // Cannot force the kernel to restore RX.
|
||||
// A worker may already be in the stub/hook/trampoline. No grace period
|
||||
// exists here, so neither the mapping nor original_slot can be retired.
|
||||
LOG("FrameBridge audio detour: target=%p failed: mprotect entry RX errno=%d; rollback mprotect entry RX errno=%d; entry restored; published code retained",address,failure_errno,rollback_errno);return NULL;
|
||||
}
|
||||
return code+16;
|
||||
fail:
|
||||
munmap(code,mx_audio_page);
|
||||
LOG("FrameBridge audio detour: target=%p failed: %s errno=%d; entry unchanged",address,failure,failure_errno);return NULL;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
#pragma once
|
||||
#include <pthread.h>
|
||||
#include <sched.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
// The source NativeArray belongs to a UnityWebRequest. Copy while its caller
|
||||
// still owns that request, before async-void queues the background parser.
|
||||
typedef struct MxGeometryOwned {
|
||||
struct MxGeometryOwned *next;
|
||||
size_t size;
|
||||
unsigned char bytes[];
|
||||
} MxGeometryOwned;
|
||||
static pthread_mutex_t mx_geometry_owned_mutex=PTHREAD_MUTEX_INITIALIZER;
|
||||
static MxGeometryOwned *mx_geometry_owned_head;
|
||||
static unsigned mx_geometry_queued,mx_geometry_finished;
|
||||
static void (*mx_geometry_queue_original)(void*,const void*,uint64_t,void*);
|
||||
static void (*mx_geometry_worker_original)(void*,void*);
|
||||
static void mx_geometry_queue(void *object,const void *data,uint64_t packed_length,void *method) {
|
||||
void (*original)(void*,const void*,uint64_t,void*);
|
||||
while(!(original=__atomic_load_n(&mx_geometry_queue_original,__ATOMIC_ACQUIRE)))sched_yield();
|
||||
int32_t length=(int32_t)(uint32_t)packed_length;
|
||||
size_t size=length>0?(size_t)length:0;
|
||||
if(!data || !size || size>SIZE_MAX-sizeof(MxGeometryOwned)){original(object,data,packed_length,method);return;}
|
||||
MxGeometryOwned *owned=malloc(sizeof(*owned)+size);
|
||||
if(!owned){original(object,data,packed_length,method);return;}
|
||||
owned->size=size;
|
||||
memcpy(owned->bytes,data,size);
|
||||
pthread_mutex_lock(&mx_geometry_owned_mutex);
|
||||
owned->next=mx_geometry_owned_head;mx_geometry_owned_head=owned;
|
||||
pthread_mutex_unlock(&mx_geometry_owned_mutex);
|
||||
unsigned queued=__atomic_fetch_add(&mx_geometry_queued,1,__ATOMIC_RELAXED);
|
||||
if(queued<4)LOG("FrameBridge audio geometry: owned %zu bytes before background task queued",size);
|
||||
original(object,owned->bytes,packed_length,method);
|
||||
}
|
||||
static void mx_geometry_worker(void *closure,void *method) {
|
||||
const void *data=NULL;
|
||||
if(closure)memcpy(&data,(const unsigned char*)closure+16,8);
|
||||
void (*original)(void*,void*);
|
||||
while(!(original=__atomic_load_n(&mx_geometry_worker_original,__ATOMIC_ACQUIRE)))sched_yield();
|
||||
original(closure,method);
|
||||
MxGeometryOwned *finished=NULL;
|
||||
pthread_mutex_lock(&mx_geometry_owned_mutex);
|
||||
for(MxGeometryOwned **p=&mx_geometry_owned_head;*p;p=&(*p)->next)if((*p)->bytes==data) {
|
||||
finished=*p;*p=finished->next;break;
|
||||
}
|
||||
pthread_mutex_unlock(&mx_geometry_owned_mutex);
|
||||
if(finished) {
|
||||
unsigned count=__atomic_fetch_add(&mx_geometry_finished,1,__ATOMIC_RELAXED);
|
||||
if(count<4)LOG("FrameBridge audio geometry: background parse finished; releasing %zu owned bytes",finished->size);
|
||||
free(finished);
|
||||
}
|
||||
}
|
||||