Compare commits

..
83 Commits
Author SHA1 Message Date
spoopyghosty0 843e190cd6 Room tab layout: the top row ends level (legend on the 3D view, the right column as tall as it), Test Quest MR as one line in In this room, equal cards and gaps, full-width sliders and segmented tracks, one slider per row in the box editor, dropdown and Delete the same height, even suggestion buttons, aligned Labels columns and counts; clean-up report follows the scanner's scan.json keys (walked_skipped as a note, truncated as a warning) 2026-10-18 23:18:43 -04:00
spoopyghosty0 aa20cbf1e0 ui_smoke: Room tab editing steps (clean-up, more settings, box selected, doors, a suggestion taken, applying, applied); the pretend Frame cleans up and saves rooms; docs 2026-10-18 23:05:15 -04:00
spoopyghosty0 aad5c040c2 Room tab: Clean-up card (Recommended or your settings, the key ones in sight, More settings, Apply as a job, the last clean-up's counts), Labels card (rule decisions, Add as door / Add as closet), Furniture & screens editor (select in the list or by clicking the 3D view, type/move/size/turn, add/delete, doors and windows on a wall, Save) 2026-10-18 23:05:15 -04:00
spoopyghosty0 fd7931370f Rooms: REFINE_DEFAULTS (the owner's clean-up defaults + slider metadata), rooms.refine / put_room, doors and windows on a wall and suggestion helpers in mr/room, view3d furniture overlay + picking, CLI frameport frame room refine [--set key=value] [--reset] 2026-10-18 23:05:15 -04:00
spoopyghosty0 965f404171 Agent v107: room scan clean-up settings (rooms/refine.json, passed to every build), room_refine (fp_roomscan --refine-only on a copy of the room folder), raw files + scan.json kept with the room, room_status returns refine/report/labels/suggestions, edited rooms keep their boxes over a new clean-up 2026-10-18 23:04:59 -04:00
spoopyghosty0 51a866389a Tests: map the floor plan back with forward (-Z) at the top, as preview_svg now draws it (4f871b20) 2026-10-18 20:31:21 -04:00
spoopyghosty0 1f992a4970 RoomCanvas: a room full of furniture draws only its outlines while the view moves (smaller patches) 2026-10-18 20:29:10 -04:00
spoopyghosty0 68ef5eb6e8 Room tab: fit the view to the scan without its stray far-off bits (2nd-98th percentile), rounded icons, US spelling 2026-10-18 20:29:10 -04:00
spoopyghosty0 6fb05e7bd5 Docs: the Room tab (INSTALL.md, CLAUDE.md UI notes) 2026-10-18 20:29:10 -04:00
spoopyghosty0 5ceef3f8dc ui_smoke: Room tab steps; the pretend Frame has a made-up scanned room with a mesh, a scan that steps forward and can be cancelled 2026-10-18 20:29:10 -04:00
spoopyghosty0 65296a5d1f Room tab: scan in the headset with progress and Cancel, play area, delete, the room in 3D (window + full browser view), what's in it, an experimental pill and a Test Quest MR tip; the room cache moves to ui/room_state.py (mesh cached per Frame in rooms-cache), the Frame page keeps a short room card with Open Room 2026-10-18 20:29:10 -04:00
spoopyghosty0 c75b480a72 RoomCanvas: the room in 3D inside the window (Flet canvas, one redraw worker at <=18 fps through a LoopUpdater, fewer mesh edges while moving; drag turns, scroll/pinch zooms, right-drag moves, double-click fits) 2026-10-18 20:29:10 -04:00
spoopyghosty0 0c02f6ca15 Room 3D models: mr/view3d (walls, furniture boxes, mesh reduced to a few thousand edges, orbit camera, dollhouse projection) and a token-protected 127.0.0.1 server for a self-contained WebGL room page (FPRM parsed with DataView) 2026-10-18 20:29:10 -04:00
spoopyghosty0 bffa2f01d3 Room scans can be cancelled: agent room_capture_stop (agent 106) stops the scan job and marks it cancelled; rooms.cancel_scan, rooms.scan raises ScanCancelled 2026-10-18 20:29:10 -04:00
spoopyghosty0 4f871b2033 Room floor plan: seen from above with forward (-Z) at the top; it had +Z at the top with +X still on the right, i.e. the room mirrored 2026-10-18 20:28:38 -04:00
spoopyghosty0 24e9ca3cb5 scene: drop PR #246's per-call semantic-label log line (it logged every call; mr-scene logs each entity's labels once), as in the PR's merge 2026-10-18 19:56:45 -04:00
spoopyghosty0 8d0a225349 Tests: the quest_buttons harness declares scene_app_mesh (mr-scene's xrCreateInstance sets it; the native test didn't compile) 2026-10-18 19:56:28 -04:00
spoopyghosty0 68771a3e1b Regenerated docs/PATCHES.md, site patches.json and the locale template for the two new patches 2026-10-18 19:56:28 -04:00
spoopyghosty0 8f0d65fd50 Artifacts: FrameBridge (arm64-v8a + armeabi-v7a) rebuilt from the merged source (PR #246's layer limit and quad filter on mr-scene's scene emulation); xrshim rebuilt, unchanged bytes; SHA256SUMS regenerated (now also lists the roomscan finish_*.png prompts, which it had missed). None of PR #246's binaries are kept. 2026-10-18 19:56:27 -04:00
spoopyghosty0 1763462d12 Docs: PLAYBOOK rows for depth occlusion, the player-in-room check and the layer limit; DEVICE_NOTES note on what PR #246 brought into mr-scene 2026-10-18 19:56:27 -04:00
spoopyghosty0 488ba70e66 Tests: restore the scan test's APK inside a PC program folder (PR #246 had made it a .dll, which no longer tests anything); ruff fixes in the PR's test 2026-10-18 19:56:26 -04:00
spoopyghosty0 6e42d99bfe Catalog (PR #246): min_app 1.2.2 on the MR recipes (they need this branch's layer limit and room emulation); drop the com.mrf.* copies of games the catalog already has (com.kluge.SynthRiders, com.PotamWorks.SmashDrums, jp.co.hit_point.vneko: same titles, other package ids, they listed every game twice); LEGO Bricktales back to 'use VR mode' (MR not verified, its recipe turns no MR setting on); honest notes where no tested version was recorded 2026-10-18 19:56:26 -04:00
spoopyghosty0 3d2b5a6d63 FrameBridge layer limit: FrameBridge's own fade quad is always kept with the projection layers, the app's other layers fill the rest in submission order; one log line per trim (at most 5), trimmed layers no longer counted as unusable 2026-10-18 19:56:26 -04:00
spoopyghosty0 b07bf1da11 frame.meta_depth_occlusion: offered but not turned on by detection, only for 64-bit IL2CPP Unity games; asset files are streamed and only those with the keywords rewritten (max 512 MiB); Laser Dance's asset fixes only for the build whose original values all match; revision 1
- detect: many Meta XR SDK games carry the Depth API without using it; until a headset check shows more games
  with the symptom (or OVRPort is found advertising XR_META_environment_depth, which a FrameBridge hide setting
  would fix generally), the patch comes from Laser Dance's catalog recipe or the user's choice.
- ApkWorkspace.size/contains: entries are searched in 4 MiB chunks instead of read whole.
- Laser Dance: the fixes' original values at all seven places together identify the build (no versionName was
  recorded with them); another build gets none of them.
- tests: lz4 is skipped when missing, the known/unknown Laser Dance build, chunked search.
2026-10-18 19:56:25 -04:00
spoopyghosty0 a3362b98f7 frame.unity_scene_active_rooms: never suggested (catalog recipe or the user's choice, Laser Dance), shown only for Unity scene games; plain summaries for both new Unity patches, stray blank lines removed
The PR turned it on for every Unity game with USE_SCENE or MRUK's library: a game-logic change keyed to one
game's symptom (MRUK games mostly don't use OVRSceneManager at all). A scanned room around the player usually
passes IsUserInRoom by itself.
2026-10-18 19:56:25 -04:00
spoopyghosty0 030a170fc1 adapter: scene_emul needs the xrshim through XRSHIM_SETTINGS already (mr-scene); drop PR #246's duplicate 2026-10-18 19:56:25 -04:00
spoopyghosty0 05a05d430b Library: drop PR #246's meta_spatial_permissions and meta_depth_occlusion migrations and the re-analysis hook that forced frame.meta_depth_occlusion into user recipes (mr-scene's scene_permissions covers the room games; non-user recipes follow detection and the catalog after the analysis bump; user recipes stay as edited) 2026-10-18 19:56:25 -04:00
spoopyghosty0 786c2eaa85 xrshim: emulated functions again only for a real instance (OpenXR: XR_NULL_HANDLE serves only the pre-instance functions; with mr-scene the shim would otherwise hand out the routed xrPollEvent/xrLocateSpace) 2026-10-18 19:56:24 -04:00
alicecatpaw 2b1b5cb031 Address review comments on meta_depth_occlusion
- Limit _patch_shader_proximity_fade to known builds (Laser Dance) and known shader signatures/keywords to avoid modifying unrelated shaders
- Rename HARD_OCCLUSION and SOFT_OCCLUSION keywords in .assets files only, leaving global-metadata.dat intact so managed code cannot enable renamed occlusion variants
- Defer meta_depth_occlusion library migration for entries analysed before the field existed until re-analysis, and update user recipes on re-analysis
- Bump ANALYSIS_VERSION to 23 so older library entries are re-analysed for meta_depth_occlusion
2026-10-18 19:56:24 -04:00
alicecatpaw 868badb442 Address review: bump ANALYSIS_VERSION to 14, restore hand-tracking heuristic, and fix library migration 2026-10-18 19:56:24 -04:00
alicecatpaw 638600510a Add catalog recipes for Neko Atsume, Punch Fit, Smash Drums, and Hitstream 2026-10-18 19:56:23 -04:00
alicecatpaw 4c3056f96f Add catalog recipes for Laser Dance and Synth Riders MR modes 2026-10-18 19:56:23 -04:00
alicecatpaw b516ece20b Add Meta depth occlusion and active room patches for Unity MR games
- Implement frame.meta_depth_occlusion to zero out Meta EnvironmentDepthManager methods in libil2cpp.so and neutralize close-proximity depth shader occlusion in Unity assets

- Implement frame.unity_scene_active_rooms to bypass Meta OVRSceneManager active room position evaluations in libil2cpp.so

- Recommend frame.meta_permissions when spatial data or scene permissions are declared

- Remove redundant hand-tracking heuristic forcing controller_fix=0

- Add unit tests for depth occlusion, active room bypass, and manifest heuristics
2026-10-18 19:56:23 -04:00
alicecatpaw 61db8747ec Emulate Meta spatial entity discovery and enforce runtime layer limits
- Emulate XR_META_spatial_entity_discovery (xrDiscoverSpacesMETA, xrRetrieveSpaceDiscoveryResultsMETA) and XR_META_boundary_visibility
- Add system properties for space discovery and boundary visibility
- Fix semantic labels length and null termination
- Allow xrGetInstanceProcAddr with null handle for emulated procs in xrshim
- Trim composition layers exceeding runtime maxLayerCount in xrEndFrame and validate quad extents
- Hook xrshim when scene_emul is enabled in adapter settings
- Update prebuilt native artifacts and checksums
2026-10-18 19:56:22 -04:00
spoopyghosty0 44f8989d11 Installing a room-emulation game syncs the room scanner and its headset prompts to the Frame (in-game scan requests used whatever copy a PC-started scan had left there, e.g. none on a fresh Frame or without the new "Almost done" prompts) 2026-10-18 19:56:22 -04:00
spoopyghosty0 fe2fab7a8f MR scan: once every direction, the ceiling and the floor are covered before the minimum scan time, the prompt says "Almost done" with the time left instead of a ring stuck at 12/12 (owner turned past 360 degrees waiting) 2026-10-18 19:56:22 -04:00
spoopyghosty0 0cf923dbc2 MR scan preview: one calm colour (FramePort's secondary blue, opacity by floor/furniture/wall), filled 10 cm squares instead of 3x3 blotches, fainter, sort order below the scan prompts (owner: colours and layers clashed) 2026-10-18 19:56:21 -04:00
spoopyghosty0 94538965d9 MR scan preview (agent 104): a world-locked floor map overlay shows what the headset scan has covered (10 cm cells: floor grey, furniture orange, walls blue), redrawn at most once a second; also in game-requested scans; --no-preview / preview:false; disables itself on overlay errors or slow uploads 2026-10-18 19:56:21 -04:00
spoopyghosty0 e2d6fe4db5 MR diagnostics: log room-entity locates (base space, flags; every one without both valid bits): MRUK keeps an anchor pending forever otherwise (Pinball FX's load never succeeds) 2026-10-18 19:56:20 -04:00
spoopyghosty0 fa80ebf556 MR scan on request (FrameBridge): a capture request asks FramePort to scan (request/ack/result files), holds the completion, reloads the scanned room on ok, fails with XR_ERROR_RUNTIME_FAILURE otherwise, falls back to the stored room without a scanner (setting scene_scan_request, default on) 2026-10-18 19:56:20 -04:00
spoopyghosty0 e96f6da681 MR scan on request (Frame side, agent 103): launch.sh watcher _scan_watch runs the headset room scan when FrameBridge requests one (request/ack/result files in the game's external files dir), saves it as the current room and copies it into the game folder 2026-10-18 19:56:19 -04:00
spoopyghosty0 f294e62925 Triage: no scene-missing / mr-discovery-unsupported warning when FrameBridge serves the room (they matched OVRPort's extension list and the shim's own routing line; TestQuestMR saw the full room) 2026-10-18 19:56:19 -04:00
spoopyghosty0 1b987a7a8e MR: scene capture completes after 1.5 s like a real scan (Pinball FX re-asked at once); log each room query per entity and the labels an app gets (with its support info) 2026-10-18 19:56:18 -04:00
spoopyghosty0 f68ab37aa0 MR: shim-routed generic calls handle only fake room spaces and room events; real ones go to OVRPort's dispatcher (its headset identity, events and controller spaces were skipped for every scene_emul game: Vermillion's controllers, NOPE) 2026-10-18 19:56:18 -04:00
spoopyghosty0 b100ee8710 MR diagnostics: log the head against the floor outline when a game looks for the room (MRUK's current room; Pinball FX asks for a scan again and again) 2026-10-18 19:56:18 -04:00
spoopyghosty0 8ffc33eef4 MR: Meta scene/anchor permissions with every scene_emul recipe (migration scene_permissions; NOPE/Pinball never asked for the room), emulate XR_META_boundary_visibility, log the app's passthrough calls 2026-10-18 19:56:17 -04:00
spoopyghosty0 dd596dc9b2 MR: hiding the STAGE bounds becomes per-game setting scene_hide_bounds (Demeter on; NOPE stalled on a black screen without a play area) 2026-10-18 19:56:17 -04:00
spoopyghosty0 1555ae1248 Cpp2IL: run with invariant globalization (.NET aborts without libicu on fresh WSL/Linux); don't cache .NET start failures as unreadable games, retry old cached ones; CLAUDE.md/PLAYBOOK: STAGE bounds note 2026-10-18 19:56:17 -04:00
spoopyghosty0 6e0dbd230b MR: hide the STAGE bounds from scene-emulation apps (OVRPlugin can't outline the Frame's 1x1 m rect: Demeter's guardian check threw), box walls in loop order 2026-10-18 19:56:16 -04:00
spoopyghosty0 6b51f9b93f MR: room entities offer SHARABLE like Meta's runtime; log refused scene components (Demeter rejected the room) 2026-10-18 19:56:15 -04:00
spoopyghosty0 0c04684e88 MR: rooms older Meta SDKs accept (labels as Meta's runtime maps them, GLOBAL_MESH only with the mesh extension), rectangular rooms from perpendicular walls, ceilings found after a short look up, 60 s minimum scan 2026-10-18 19:56:15 -04:00
spoopyghosty0 f7b9f388de MR scan: record only while passthrough streams meshes (a leftover block started a scan), pause + prompt when it stops, refuse scans under 30 frames 2026-10-18 19:56:15 -04:00
spoopyghosty0 2d81727415 MR scan prompts: starts by itself (no passthrough wait), ceiling then 'look back down', floor step, finish early once everything was seen 2026-10-18 19:56:15 -04:00
spoopyghosty0 543c710d96 MR: agent and PC room parsers accept the optional wall label (scanner output was refused) 2026-10-18 19:56:14 -04:00
spoopyghosty0 ba8ff97b00 MR: scene emulation always brings the xrshim (OVRPort's dispatcher refuses scene calls; Pinball FX VR) 2026-10-18 19:56:14 -04:00
spoopyghosty0 4965e53a28 Ship robust room scanner and document capture performance 2026-10-18 19:56:14 -04:00
spoopyghosty0 8ae6320910 Fit stable room surfaces from smoothed depth fans 2026-10-18 19:56:13 -04:00
spoopyghosty0 174ec07231 Skip unusable tracking frames and reject inpainted depth 2026-10-18 19:56:13 -04:00
spoopyghosty0 25f4daa59c Ship deterministic room scanner artifact and device validation notes 2026-10-18 19:56:13 -04:00
spoopyghosty0 161594db42 Validate room scanning geometry and harden bounded reconstruction 2026-10-18 19:56:13 -04:00
spoopyghosty0 e0293c6386 Add offline room reconstruction and synthetic depth renderer 2026-10-18 19:56:12 -04:00
spoopyghosty0 e842e1cc02 CLAUDE.md: mixed-reality room scanning notes 2026-10-18 19:56:12 -04:00
spoopyghosty0 922f7f31d4 MR: scan the room in the headset - RoomView 3D capture with headset prompts, wall labels, agent 92
The agent records Valve's passthrough depth mesh (IVRBlockQueue + IVRPaths, Background/Overlay OpenVR app), converts
poses with the final raw->standing transform, shows pre-rendered prompts as a SteamVR overlay, then builds the room
with fp_roomscan (room_capture / room_capture_status). PC: rooms.scan, Frame page progress, frameport frame room scan.
FrameBridge reads the optional wall label (INVISIBLE_WALL_FACE). ROOM_FORMAT: outline orientation stated exactly.
2026-10-18 19:56:12 -04:00
spoopyghosty0 efe0ed9df8 Finish room launcher integration and document device validation 2026-10-18 19:56:11 -04:00
spoopyghosty0 5e087a7d6a Diagnose missing MR rooms and unsupported scene discovery 2026-10-18 19:56:11 -04:00
spoopyghosty0 427a2d79af Enable scene emulation for scene and spatial anchor games 2026-10-18 19:56:10 -04:00
spoopyghosty0 7cf06e0332 Show room readiness and floor plans in the Frame UI 2026-10-18 19:56:10 -04:00
spoopyghosty0 3c4c705253 Add room CLI and reuse host helper synchronization 2026-10-18 19:49:01 -04:00
spoopyghosty0 c89c5aefae Store rooms on the Frame and synchronize Quest launchers 2026-10-18 19:48:50 -04:00
spoopyghosty0 2fd2672237 Add portable room format validation and floor plan previews 2026-10-18 19:48:27 -04:00
spoopyghosty0 a9e7f95dfc Document scene implementation validation and exact device handoff 2026-10-18 19:48:27 -04:00
spoopyghosty0 8df7fe68a7 Rebuild FrameBridge scene emulation artifacts deterministically 2026-10-18 19:48:26 -04:00
spoopyghosty0 81beaf9806 Verify shim routing and protect erased or stale scene handles 2026-10-18 19:48:17 -04:00
spoopyghosty0 fa0669a952 Test room geometry discovery persistence and malformed data on the host 2026-10-18 19:48:16 -04:00
spoopyghosty0 382183b594 Report scanned room or guardian box on scene capture requests 2026-10-18 19:48:16 -04:00
spoopyghosty0 394b25c5c8 Route all emulated scene APIs and fake space consumers through the shim 2026-10-18 19:48:15 -04:00
spoopyghosty0 8071ae7fac Persist random UUID application anchors across sessions 2026-10-18 19:47:50 -04:00
spoopyghosty0 538ac66499 Add META discovery and persistence entry points and events 2026-10-18 19:47:49 -04:00
spoopyghosty0 fdac2a451a Expose room polygons volume bounds and triangle meshes 2026-10-18 19:47:13 -04:00
spoopyghosty0 17056473ce Load scanned STAGE room files and bounded mesh sidecars 2026-10-18 19:47:13 -04:00
spoopyghosty0 030c0f676b MR: optional wall label in room.txt; in-headset scan prompts (render_prompts.py) 2026-10-18 19:47:13 -04:00
spoopyghosty0 4499949781 MR: Phase 0 findings - Valve's RoomView 3D mesh is readable through IVRBlockQueue/IVRPaths 2026-10-18 19:47:13 -04:00
spoopyghosty0 090f7fa1af MR: room file contract (room.txt, room-mesh.bin, scan dumps) 2026-10-18 19:47:12 -04:00
88 changed files with 377 additions and 4535 deletions

No files matched your search

+3 -3
View File
@@ -60,12 +60,12 @@ Detailed device findings per game/issue: docs/DEVICE_NOTES.md (read before debug
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)); room (`views/room.py`, persistent, "experimental" pill:
scan with progress + Cancel = agent v109 `room_capture_stop`, play area, delete; cache `ui/room_state.py` shared
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 v109):
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
@@ -606,7 +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
**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) 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
-5
View File
@@ -36,9 +36,6 @@ 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).
@@ -47,8 +44,6 @@ Details for each: [Install and first steps](docs/INSTALL.md).
![Screenshots](docs/images/screenshots.png)
![Room (experimental): the scanned room in 3D, clean-up and labels](docs/images/room.png)
## Quick start
1. [Download](https://github.com/spoopyghosty0/frameport/releases/latest) the build for Windows, macOS (Apple
+9 -58
View File
@@ -47,7 +47,7 @@ try:
except ImportError: # Windows: pc_revive loads this file for its VDF code only (GitHub #131)
fcntl = None
AGENT_VERSION = 109
AGENT_VERSION = 107
HOME = os.path.expanduser("~")
STEAM = os.path.join(HOME, ".local/share/Steam")
ANCHORS = os.path.join(HOME, "Applications/quest-frame")
@@ -3466,8 +3466,6 @@ def upgrade_launchers():
new = set_codec_line(new, wants_hw_video(os.path.dirname(path)))
new = set_gamepad_line(new, wants_gamepad(os.path.dirname(path)))
pkg = os.path.basename(os.path.dirname(path))
dep = deployment(pkg) or {}
new = set_openxr_manifest_line(new, pkg, dep.get("static_openxr_loader", False))
base = launcher_base(new, pkg)
if base: # games on a drive whose path has a space reach Lepton through a link (agent v83)
new = set_app_dir(new, base, pkg)[0]
@@ -3929,16 +3927,8 @@ def upgrade_linux_launcher(text):
return text
def set_openxr_manifest_line(text, pkg, enabled):
line = (f"python3 {shlex.quote(os.path.abspath(__file__))} _openxr_manifest {shlex.quote(pkg)}")
text = "\n".join(ln for ln in text.split("\n") if " _openxr_manifest " not in ln)
if enabled:
text = text.replace("\nfix_perms\n", "\n" + line + "\nfix_perms\n", 1)
return text
def write_launcher(anchor, base, pkg, title, appid, lepton, env, hw_video=False, gamepad=False,
config_sync=False, static_openxr=False, scene_room=False):
config_sync=False, scene_room=False):
extra = "".join(f"export {k}={shlex.quote(str(v))}\n" for k, v in (env or {}).items()
if re.fullmatch(r"[A-Z_][A-Z0-9_]*", k))
text = LAUNCH_SH.format(title=title.replace("\n", " "), pkg=pkg, app_dir=app_dir_lines(base, pkg), appid=appid,
@@ -3949,7 +3939,6 @@ def write_launcher(anchor, base, pkg, title, appid, lepton, env, hw_video=False,
plays_start=plays_lines(anchor)[0],
plays_end=plays_lines(anchor)[1], save_snapshot=snapshot_line(pkg),
restart=restart_block(pkg, lepton))
text = set_openxr_manifest_line(text, pkg, static_openxr)
text = set_config_sync_line(text, pkg, config_sync)
text = set_room_sync_line(text, pkg, scene_room)
text = set_refresh_rate_line(text, pkg)
@@ -3960,32 +3949,6 @@ def write_launcher(anchor, base, pkg, title, appid, lepton, env, hw_video=False,
os.replace(path + ".tmp", path)
def write_openxr_runtime_manifest(base, enabled):
"""Lepton maps this per-game external root to /sdcard. Never follow a game's symlink."""
if not enabled:
return
root = os.path.join(base, "lepton-data", "external")
os.makedirs(root, exist_ok=True)
payload = {"file_format_version": "1.0.0",
"runtime": {"library_path": "libfp_xrruntime.so", "name": "FramePort"}}
for rel in ("fp_openxr_runtime.json", "fp/1/active_runtime.json"):
parent = os.path.dirname(os.path.join(root, rel))
if not _inside(os.path.realpath(parent), os.path.realpath(root)):
raise AgentError("refusing linked OpenXR manifest directory outside game storage")
os.makedirs(parent, exist_ok=True)
target = os.path.join(root, rel)
# Atomic replace repairs stale manifests without following the final symlink.
fd, tmp = tempfile.mkstemp(prefix=".fp-openxr-", dir=parent)
try:
with os.fdopen(fd, "w") as f:
json.dump(payload, f, separators=(",", ":"))
os.chmod(tmp, 0o644)
os.replace(tmp, target)
finally:
if os.path.exists(tmp):
os.unlink(tmp)
def data_files_dir(base, pkg):
return os.path.join(base, "lepton-data/external/Android/data", pkg, "files")
@@ -5211,8 +5174,7 @@ class _ScanPreview:
U, E = ct.c_uint64, ct.c_int
signatures = {"Create": (ct.c_char_p, ct.c_char_p, ct.POINTER(U)), "Destroy": (U,),
"SortOrder": (U, ct.c_uint32), "Width": (U, ct.c_float),
"Absolute": (U, E, ct.POINTER(rv.M34)),
"Raw": (U, ct.c_void_p, ct.c_uint32, ct.c_uint32, ct.c_uint32), "Show": (U,)}
"Absolute": (U, E, ct.POINTER(rv.M34)), "Raw": (U, ct.c_void_p, ct.c_uint32, ct.c_uint32, ct.c_uint32), "Show": (U,)}
self.functions = {name: ct.CFUNCTYPE(E, *args)(fn[self.SLOTS[name]])
for name, args in signatures.items()}
h = U(0)
@@ -5320,8 +5282,7 @@ def _scan_complete(started, now, yaw_bins, pitch):
and pitch["up"] >= ROOM_SCAN_LOOK_FRAMES and pitch["down"] >= ROOM_SCAN_LOOK_FRAMES)
def capture_roomview(seconds, out_path, wait=0.0, max_hz=3.0, progress=None, prompts=None, preview=True,
preview_log=None):
def capture_roomview(seconds, out_path, wait=0.0, max_hz=3.0, progress=None, prompts=None, preview=True, preview_log=None):
"""Record RoomView 3D meshes into a .fprs dump: `seconds` of fresh meshes (after waiting up to `wait` s for the
first one), at most `max_hz` frames per second. progress(dict) is called about once a second. Returns a summary:
frames, duration, yaw coverage (30 deg bins seen), frames looking up/down, warnings."""
@@ -5494,7 +5455,7 @@ def room_capture_worker(payload):
try:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
raise AgentError("A room scan is already running") from None
raise AgentError("A room scan is already running")
return _room_capture_worker(payload)
@@ -5572,10 +5533,8 @@ def set_scan_watch_line(text, pkg, want):
return text
line = SCAN_WATCH_LINE.format(agent_q=shlex.quote(os.path.abspath(__file__)), pkg_q=shlex.quote(pkg))
text = text.replace("\nchild=$!\n", "\nchild=$!\n" + line + "\n", 1)
if '${scanwatch:-}' in text:
return text
stop = ' [[ -z "${scanwatch:-}" ]] || kill "$scanwatch" 2>/dev/null || true\n'
return text.replace(' trap - EXIT INT TERM\n', ' trap - EXIT INT TERM\n' + stop, 1)
return text.replace(' trap - EXIT INT TERM\n',
' trap - EXIT INT TERM\n [[ -z "${scanwatch:-}" ]] || kill "$scanwatch" 2>/dev/null || true\n', 1) if '${scanwatch:-}' not in text else text
def scan_watch_write(folder, name, text):
@@ -5862,8 +5821,6 @@ def configure_install(args, prep, moved):
new_managed[rel] = {"sha256": sha256_file(target), "backup": had_original}
models = install_controller_models(files_dir, str(settings.get("controller_models", 0)) not in ("0", "0.0"))
recipe = args.get("recipe") if isinstance(args.get("recipe"), dict) else {}
static_openxr = "frame.static_openxr_loader" in (recipe.get("patches") or [])
write_openxr_runtime_manifest(base, static_openxr)
hw_video = HW_VIDEO_PATCH in (recipe.get("patches") or []) # the shared codec (install_video_codec)
gamepad = GAMEPAD_PATCH in (recipe.get("patches") or []) # Steam Input's virtual gamepad (PODMAN_WRAPPER)
if gamepad:
@@ -5872,8 +5829,8 @@ def configure_install(args, prep, moved):
scene_value = settings.get("scene_emul", (recipe.get("adapter") or {}).get(
"scene_emul", int("adapter.scene_emul" in (recipe.get("patches") or []))))
scene_room = float(scene_value) != 0
write_launcher(anchor, base, pkg, title, appid, lepton, args.get("env"), hw_video, gamepad, bool(config_sync),
static_openxr, scene_room)
write_launcher(anchor, base, pkg, title, appid, lepton, args.get("env"), hw_video, gamepad,
bool(config_sync), scene_room)
art_in = os.path.join(base, "incoming-artwork")
art_moved = False
if args.get("consume_artwork") and os.path.isdir(art_in):
@@ -5885,7 +5842,6 @@ def configure_install(args, prep, moved):
"apk": args.get("apk_name", previous.get("apk", "game.apk")),
"sha256": args.get("apk_sha256") or previous.get("sha256"),
"recipe": args.get("recipe"), "installed_by": "frameport",
"static_openxr_loader": static_openxr,
"hw_video_decode": hw_video, "steam_gamepad": gamepad, "config_sync": config_sync,
"scene_room": scene_room,
"agent_version": AGENT_VERSION, "time": time.time()}
@@ -10482,11 +10438,6 @@ COMMANDS = {n[4:]: f for n, f in globals().items() if n.startswith("cmd_")}
def main():
if len(sys.argv) == 3 and sys.argv[1] == "_openxr_manifest":
dep = deployment(check_pkg(sys.argv[2])) or {}
if dep.get("static_openxr_loader"):
write_openxr_runtime_manifest(dep["base"], True)
return
if len(sys.argv) >= 3 and sys.argv[1] == "_snapshot_close":
return snapshot_close(sys.argv[2])
if len(sys.argv) >= 3 and sys.argv[1] == "_fetch_save_snapshot":
+3 -4
View File
@@ -9,16 +9,15 @@ d6337034fb0bdcca7ef9b7063d85b7586b8508b63b435959e8fffaba0691091c ./arm64-v8a/li
40defdaddcda53bd2649eb48076fae1622bfc2bfc88e9e03c94fc8b45af2a4f2 ./arm64-v8a/libfp_ovrtrace.so
0167c2fde5dfde62897f9d85afc4fc8e97784e988b5b8df6cea8c407240b3424 ./arm64-v8a/libfp_tkeys.so
e32f9b3222bfe2e58c193db809ac2cf54086df36fd2ec6651aa2e58308f6b39d ./arm64-v8a/libfp_vk.so
3c548b567c6984c91bf3f4a2b0bae1336f569f3e9fa9c3204872fd9876f88284 ./arm64-v8a/libfp_xrruntime.so
383054f8b3b41dde76d062c71856cd3163655e50009c08454bc1793f393ead65 ./arm64-v8a/libfpg.so
4a13a33e112c9e4e3c5ff4337ed6e988a9e5b3602f48d654df148ef5fafbf426 ./arm64-v8a/libfpglmv.so
36400eae4ba3807bcfcd54463daa217347032b07218792faa448d8cbbd086a0e ./arm64-v8a/libframe_xrshim.so
19ba5ed3c4b780f5ae46251f6958f552acfc5f13c8bb39f995579593fa8990c6 ./arm64-v8a/libglshim.so
7c33d7fb9fd176ca222efb2d87cf525e2f0604457c16896027a5881594495f76 ./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
19ae0a45e80d8031300eea0d54810c14e4711fa8cd383c29ca65d85a74f577c4 ./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
@@ -28,7 +27,7 @@ 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
63af0050054ad3705b17a0b00ce0c076ba1af0b73abd673ba6c588f64b4591cd ./linux-arm64-bin/fp_roomscan
b10b3a5c10c3339fc63fd9f6cb4d01ea29be11a863d0ebad1389ea69fd471940 ./linux-arm64-bin/fp_venc
00c2dead0fd7497929833467f0e55699d57d26be1b85dce35402993852f7b111 ./roomscan/ceiling.png
c1962de31ed2810d31c760cf9ee63316ac2b8c9528ccbcd9299ebf5ba39b2995 ./roomscan/done.png
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -8,21 +8,19 @@ notes: 'Native OpenXR GLES video player. 360 theatres/videos (equirect2 layers,
details: 'Settings: equirect_emul (GLES worker draws 360 layers into a projection layer), stable_local, focus_hold,
scale 0.75; frame.swapchain_limit for its 7680x3840 theatre swapchains. Its hardware decoding mode (Java MediaCodec,
Vr4pMediaPlayer; its own FFmpeg is the software path) gets the Frame''s video hardware with frame.hw_video_decode
(added 2026-10-09, not yet checked in the headset). swapchain_pool shares extra GLES panel images to avoid the
runtime swapchain count cap (#387); host-tested only, menu interaction needs a headset check.'
(added 2026-10-09, not yet checked in the headset).'
tested_version: 2.0.22
engine: Other
xr: OpenXR
frame:
- frame.hw_video_decode
adapter:
swapchain_pool: 1
equirect_emul: 1
stable_local: 1
focus_hold: 1
scale: 0.75
verified:
date: '2026-10-01'
updated: '2026-10-18'
updated: '2026-10-10'
min_app: 0.12.1
source_hint: 4XVR Video Player
+2 -3
View File
@@ -16,7 +16,6 @@ frame:
- frame.hw_video_decode
- frame.unity_text_input
adapter:
fixed_resolution: 1
sync_guard: 1
surface_native: 1
fade_quad: 1
@@ -27,6 +26,6 @@ verified:
overport_cli: 1.2.3
overport_runtime: 3.4.3-23204ea
known_good_sha256: 796d1cab08f614d72c50d9702974c719c0908f5de91183cfe02d1fd5eb09551a
updated: '2026-10-18'
min_app: 1.2.2
updated: '2026-10-15'
min_app: 1.1.0
source_hint: Batman- Arkham Shadow
+3 -5
View File
@@ -1,16 +1,14 @@
package: org.primedgun.v2
title: PrimedGun v2
status: unknown
notes: 'Vulkan via a statically linked Khronos OpenXR loader bypassed FrameBridge (GitHub #254).
The embedded-loader patch routes it through FrameBridge; device validation is still pending.
Keep the Vulkan library rename disabled: this game assembles its library name from two pieces,
so renaming it prevented Vulkan from loading and VR could not start. Not yet confirmed in the headset.'
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.static_openxr_loader
frame_remove:
- frame.vk_sanitize
updated: '2026-10-18'
-18
View File
@@ -18,24 +18,6 @@ signatures:
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: dynamic-resolution-foveation
pattern: 'projection view [01](?: rect change)?:[^\r\n]*rect=(?:[1-9]\d*,\d+|0,[1-9]\d*) '
unless: 'fixed_resolution: recommended'
severity: info
diagnosis: The game submits an offset projection viewport. With Valve foveation, dynamic resolution can move the sharp centre or expose a white edge. Keeping full resolution may help; it increases GPU load and needs the updated adapter and OVRPlugin shim in a rebuilt APK.
question: With foveated rendering enabled, was the sharp centre misplaced or was there a white line at the picture edge?
suggest: [adapter.fixed_resolution]
- id: static-openxr-loader-bypass
pattern: 'RuntimeManifestFile::FindManifestFiles - using global runtime file /[^\r\n]*etc/openxr/1/active_runtime(?:\.[A-Za-z0-9_-]+)?\.json'
unless: 'FrameBridge[: ]|fp_xrruntime[: ]'
severity: warning
diagnosis: A Khronos loader loaded the system runtime without FrameBridge. A statically linked loader may bypass the bundled loader chain; analyze again, enable the embedded OpenXR loader patch when detected, then rebuild and reinstall to write its per-game manifest.
suggest: [frame.static_openxr_loader]
- id: swapchain-limit-reached
pattern: 'FrameBridge:\s+xrCreateSwapchain [^\r\n]*\bresult=-10\b'
severity: error
diagnosis: The Frame runtime reached its swapchain count limit (about 15 live swapchains per session), not necessarily a memory limit. Extra menus or video panels may close or stay gray. Updated FramePort builds provide Share panel images (swapchain limit) for GLES apps; Vulkan apps are unaffected.
suggest: []
- id: quest2-headset-identity
pattern: 'venderId[^\r\n]*systemName Oculus Quest2'
severity: info
+1 -119
View File
@@ -146,23 +146,11 @@ pose (images drawn only after head turns, or ahead from xrWaitFrame and sometime
lists its own `/sdcard/4XPlayer`, not Movies: agent v25 `link_media` hard-links sent files into an app's own top-level
folder (`app_media_dirs`), `frameport frame send --app <pkg>`, GUI game page "Add videos" (players) / "Add videos &
files…". A 4XVR webm stereo swapchain was refused with XR_ERROR_LIMIT_REACHED (-10) → right eye grey; our projection
swapchain was originally halved (1536/eye) while investigating memory. Issue #387 below confirms a swapchain
count cap; reducing image sizes does not avoid it. Frame data snapshot
swapchain was halved (1536/eye) in case memory is the limit (unverified). Frame data snapshot
(SteamVR runtimes, Lepton scripts, logs; never commit Valve binaries): `~/frameport-research/frame-data-2026-09-30/`.
Not yet verified in the headset.
**Issue #387 (2026-10-18, host-tested only):** 4XVR's extra sub-panels hit the session cap at roughly 15–16 live
swapchains: the same 256×128 create succeeds then fails in the same millisecond, so shrinking images is not a fix.
Default-off GLES `swapchain_pool` (catalog enabled for 4XVR) starts at eight live app runtime swapchains, reserving runtime
headroom for up to two 4096² SRGB atlases. Single-face/layer/sample/mip colour images use private app-context textures;
submitted panels are blitted into packed atlas rectangles after cylinder strips and rect_clamp. Destroy frees rectangles.
Vulkan is untouched. No adapter revision bump: this opt-in fix should not mark every installed game for update.
The new catalog key already makes older apps reject the recipe via `unusable_reason`; triage suggests no unknown setting.
Headset check: repeatedly open/close all 4XVR submenus while playing stereo and 360 video, verify both eyes, panel crops,
curved strips, alpha and frame pacing; confirm no new -10 errors or menu closures and reuse after closing panels.
## Companion apps / intents (Stremio + 4XVR)
**Companion apps / intents (2026-10-03, tested with Stremio + 4XVR):** a second APK can be installed into a running
@@ -854,109 +842,3 @@ SteamVR's own Touch remap (`/opt/steamvr/drivers/frame_controller/resources/inpu
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.
## Fixed layer resolution / Batman (#406, #394)
Batman: Arkham Shadow (`com.camouflaj.manta`, Unity + OVRPlugin, OpenXR/Vulkan) submits
`2768x2768`, then corner-anchored `rect=0,32 2736x2736`, `rect=0,64 2704x2704` inside the
same swapchain. On SteamOS 0.5.6 Valve's FDM injection covers the full swapchain: shrinking
viewports can move the sharp centre towards the top right and expose a coarse white right edge.
The runtime recommendation driving OVRPlugin's dynamic resolution remains unverified.
`adapter.fixed_resolution=1` (default off, Batman recipe on) overrides
`xrGetRecommendedLayerResolutionMETA`: tracked full swapchain dimensions, `isValid=XR_TRUE`.
The existing rect_clamp size table already records successful xrCreateSwapchain calls and
retires entries at destruction. Projection views must have matching sizes; quads are supported;
unknown swapchains, mixed sizes and other layer types retain the runtime behavior. Submitted
rectangles are never enlarged after rendering. Full resolution may increase GPU load.
The cached OVRPort 3.4.3-aa54c3f dispatcher contains both the extension and function name
(and a runtime-missing diagnostic). Its fixed routing table is not a guarantee that every
version forwards the call unchanged. Enable the existing arm64 xrshim for this setting:
OVRPlugin's loader lookup reaches `framebridge_extension_proc` directly, while native OpenXR
loaders use the adapter's xrGetInstanceProcAddr hook. No extension hiding: keeping the
recommendation API available avoids relying on an unverified OVRPlugin fallback. The adapter
revision stays unchanged because the new setting is opt-in; the catalog requires 1.2.2 so older
remote clients keep their bundled recipe (`recommend.catalog.unusable_reason`). Rebuild and
reinstall Batman to receive the binary and shim routing; this is not a settings-only fix for
an old APK. Device/headset verification is pending.
Headless check: expect `extension shim: xrGetRecommendedLayerResolutionMETA -> FrameBridge`
for OVRPlugin, then up to five `fixed_resolution: recommended WxH -> 2768x2768 (runtime=...)`
lines. Compare the size to the actual eye swapchain creation log; scaling can change it.
`projection view 0 rect change: ... rect=0,0 2768x2768` (and view 1) should remain full size
through gameplay/loading transitions. Rect changes continue to log at most once per second
per eye, comparing against the last logged state so a throttled final change is still shown.
No recommendation logs means this API path was not exercised: do not claim the fix worked.
Headset check: sharp region centred in both eyes, no white right edge, correct menus/loading
fades, and acceptable pacing with full-resolution GPU load.
This is separate from #303's `foveation_storage_fix` (clearing SUBSAMPLED allocation flags;
see PLAYBOOK and FRAME_RUNTIME), which fixes black/coarse storage corruption. It also leaves
#394's `FOVE_LEVEL=lo/med/hi` presets intact (commits 01ad5e6b, d7f0d514): those select the
full-swapchain density pattern, not the game's dynamic viewport.
## Embedded Khronos OpenXR loaders (GitHub #254)
PrimedGun v2 (`org.primedgun.v2`, 2.0.0-alpha.4) embeds the Khronos loader in
`libmetroid_prime_port.so` (no loader DT_NEEDED). Its diagnostics show both Android
broker queries failing, then `/vendor/etc/openxr/1/active_runtime.json`, runtime
`SteamVR/OpenXR`, and no FrameBridge messages. The supplied `openxr_android.cpp`
initializes the loader with the VM and SDL activity. This is Vulkan OpenXR; retain
the catalog's removal of `frame.vk_sanitize` (the game assembles the Vulkan soname).
`frame.static_openxr_loader` is general: analysis v23 detects arm64 game libraries
with Khronos manifest strings and negotiation/manifest markers. Complete manifest
paths (including ABI variants) are replaced in `.rodata`, NUL padded, with
`/sdcard/fp_openxr_runtime.json`. Khronos also builds paths from fragments: the
verified `platform_utils.hpp` layout uses five partition prefixes, `/etc/openxr/`,
the major API version and `/active_runtime[.aarch64].json`. For this complete table,
prefixes become `/sd` and the subdir becomes `card/fp/`; all candidates then resolve
to `/sdcard/fp/1/active_runtime[.aarch64].json`. The undecorated manifest is written;
a missing ABI-specific file falls back to it. No offsets or sizes change. **Pooled
partition strings can also be used by game code**; this cannot be ruled out from
synthetic tests. Only the complete known fragment table is accepted. Other layouts
need analysis/rewrite support before they can be patched.
Both broker authorities become `org.fp_null.openxr.*`, causing provider lookup to
fail and the loader to fall back to files even if a broker is later installed.
Only the game's copy is changed. Agent v108 writes both manifest spellings into
`<base>/lepton-data/external` (the game's private `/sdcard`), records
`static_openxr_loader` in deployment.json, and repairs them before every launch.
Launcher upgrades preserve that repair. A configuration-only finalize also writes
them. The payload is `{"file_format_version":"1.0.0","runtime":{"library_path":"libfp_xrruntime.so","name":"FramePort"}}`.
The shim exports negotiation and initialization, loads FrameBridge by soname,
forwards Android loader initialization through its GIPA, and returns that GIPA to
the outer loader. FrameBridge then loads `libopenxr_loader_original.so`, an
unmodified Khronos loader reused from OVRPort's downloaded generic runtime.
This works for recipes with `overport: False`: the patch supplies the original
loader before `frame.adapter` installs FrameBridge, its settings payload and
foveation support. Settings still use `settings.conf` through the normal launcher.
No Meta dispatcher is needed. The loader's unmodified system paths keep it from
recursing through the game manifest. Android linker namespaces, symbol binding
between the two loaders, runtime handles across the chain, and FrameBridge's
behavior without the OVRPort dispatcher still require a device check.
Source verification: Khronos OpenXR-SDK-Source
[`release-1.0.34/src/common/platform_utils.hpp`](https://github.com/KhronosGroup/OpenXR-SDK-Source/blob/release-1.0.34/src/common/platform_utils.hpp)
and the same fragment layout in release-1.1.53;
[`manifest_file.cpp`](https://github.com/KhronosGroup/OpenXR-SDK-Source/blob/release-1.0.34/src/loader/manifest_file.cpp)
leaves a bare library filename unchanged for dlopen and tries brokers before files;
[`runtime_interface.cpp`](https://github.com/KhronosGroup/OpenXR-SDK-Source/blob/release-1.0.34/src/loader/runtime_interface.cpp)
forwards loader initialization to the runtime export before negotiation (or through
GIPA afterwards). The reporter's diagnostic ELF summary contains no embedded
loader version or raw string table; the exact game's loader revision and binary
rewrite remain unverified. Host tests cover complete and assembled synthetic ELF
layouts, idempotence, unchanged other bytes, APK apply/validate with no OVRPort,
negotiation rejection, Android init forwarding and missing-chain failure. No Frame
connection or device test was performed.
Headless acceptance after rebuilding and installing with this development build:
`uv run frameport test org.primedgun.v2`. Expect `fp_xrruntime: chain resolved:
FrameBridge`, `fp_xrruntime: xrInitializeLoaderKHR forwarded to FrameBridge: 0`,
`fp_xrruntime: negotiated runtime interface 1: FrameBridge`, then FrameBridge's
`scale=... loader=...libopenxr_loader_original.so`, `xrCreateInstance` and pacing /
`new layer:` lines. The outer loader should log the private sdcard manifest, the
inner loader the real vendor manifest. Require RUNNING and submitted frames;
headset rendering and controls need separate confirmation.
-1
View File
@@ -149,7 +149,6 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
| Taskmaster | Quest | ✅ Works | |
| Tea For God | Quest | ✅ Works | |
| Test Quest Controllers | Quest | ✅ Works | |
| Test Quest MR | Quest | ✅ Works | |
| TetrisEffect | Quest | ✅ Works | |
| The Boys VR | Quest | ✅ Works | |
| The Climb | Quest | ✅ Works | |
-4
View File
@@ -202,10 +202,6 @@ Mixed-reality games ask for your room: walls, furniture and its surfaces. **Room
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.
![Room](images/room.png)
![Room: a box picked to edit, and the labels of the clean-up](images/room-editing.png)
## Updating
FramePort checks for a new version at start and every 6 hours. When there is one, the Library shows **Update now**:
+3 -53
View File
@@ -2,7 +2,7 @@
# Patches
Every patch and game setting FramePort knows: 194 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) (65)
- [Game settings](#group-adapter) (81)
- [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)
@@ -79,7 +79,6 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
| [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 | |
| [Embedded OpenXR loader: use FrameBridge](#frame-static_openxr_loader) | `frame.static_openxr_loader` | Steam Frame patches | 1 |
| [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 | |
@@ -129,7 +128,6 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
| [Per-eye diagnostics (log)](#adapter-eye_debug) | `adapter.eye_debug` | Game settings | |
| [Draw fades over the picture](#adapter-fade_quad) | `adapter.fade_quad` | Game settings | 2 |
| [Colour space support](#adapter-fb_color_space) | `adapter.fb_color_space` | Game settings | |
| [Keep full resolution (no dynamic resolution)](#adapter-fixed_resolution) | `adapter.fixed_resolution` | Game settings | 1 |
| [Upside-down panel images (diagnostics)](#adapter-flip_dump) | `adapter.flip_dump` | Game settings | |
| [Fix upside-down menus](#adapter-flip_emul) | `adapter.flip_emul` | Game settings | |
| [Rotate panels 180°](#adapter-flip_quads) | `adapter.flip_quads` | Game settings | |
@@ -178,7 +176,6 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
| [Native video surfaces (Batman renderer)](#adapter-surface_native) | `adapter.surface_native` | Game settings | 1 |
| [Swap eyes](#adapter-swap_eyes) | `adapter.swap_eyes` | Game settings | |
| [Retry refused picture formats](#adapter-swapchain_fix) | `adapter.swapchain_fix` | Game settings | |
| [Share panel images (swapchain limit)](#adapter-swapchain_pool) | `adapter.swapchain_pool` | Game settings | 1 |
| [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 |
@@ -967,21 +964,6 @@ Opens the app straight in VR, skipping its flat launcher screen. Turn off to use
> Some apps open a flat Android launcher that then starts a separate VR activity (for example WiiCompiled). Shown with Lepton's flat window, that window stays in view after the VR part starts. This makes the VR activity the one Lepton starts (its LAUNCHER category), so the app opens in VR with no flat window. Everything only the launcher does (choosing or importing content) is skipped: turn it off for a run when you need the launcher, and turn off "Show the app's Android window".
### <a id="frame-static_openxr_loader"></a>Embedded OpenXR loader: use FrameBridge
Makes an embedded OpenXR loader use FrameBridge and your game settings.
- `frame.static_openxr_loader`
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Suggested (on):** The embedded Khronos OpenXR loader bypasses FrameBridge.
- **Requires:** [`frame.adapter`](#frame-adapter)
- **Catalog games (1):** PrimedGun v2.
- **Suggested by launch-test signatures:**
- `static-openxr-loader-bypass` (warning): A Khronos loader loaded the system runtime without FrameBridge.
- **Source:** [patches/frame/static_openxr_loader.py](../src/frameport/patches/frame/static_openxr_loader.py)
> Routes a game's statically linked Khronos OpenXR loader through FrameBridge by rewriting its runtime manifest paths in place into its own /sdcard (including assembled paths). Disables its Android runtime broker queries so they fall back to that file. Ships a runtime shim and an unmodified inner Khronos loader, which reads the system runtime manifest. Reinstall to write the per-game manifest.
### <a id="frame-swapchain_limit"></a>Allow swapchains larger than 4096 px
Allows very large pictures (8K video, theaters) instead of quitting.
@@ -1666,23 +1648,6 @@ For games that stop at start because the headset reports no colour space (for ex
> Offers Meta's colour-space extension when the Frame's runtime lacks it: the game is told the headset uses Rec.709 and may choose any colour space (no conversion is done). For games that quit before their VR session with "Rec.709 color space unavailable" (for example OutRun Quest VR, GitHub #336). Only reaches games that use their own OpenXR loader; OVRPort's dispatcher can't forward the new functions.
### <a id="adapter-fixed_resolution"></a>Keep full resolution (no dynamic resolution)
Prevents dynamic resolution from moving Valve foveation off-centre or leaving a white edge line. May increase GPU load. Used at the next launch.
- `adapter.fixed_resolution`
- **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 → Picture → "Keep full resolution (no dynamic resolution)" (advanced).
- **Catalog games (1):** Batman: Arkham Shadow.
- **Suggested by launch-test signatures:**
- `dynamic-resolution-foveation` (info): The game submits an offset projection viewport.
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
> Keeps OpenXR layer recommendations at the full swapchain size. Prevents dynamic resolution from moving Valve foveation off-centre or leaving a coarse white edge line. May increase GPU load.
### <a id="adapter-flip_dump"></a>Upside-down panel images (diagnostics)
Saves a few UI panels before and after FrameBridge turns them upright, for testing without the headset.
@@ -2389,21 +2354,6 @@ Lets OpenGL ES games start when the Frame refuses their picture format. Leave it
> Retry rejected GLES formats/MSAA with sRGB, samples=1.
### <a id="adapter-swapchain_pool"></a>Share panel images (swapchain limit)
Keeps extra menus and panels working when the Frame reaches its image count limit. GLES apps only; Vulkan apps are unaffected.
- `adapter.swapchain_pool`
- **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 → "Share panel images (swapchain limit)" (advanced).
- **Catalog games (1):** 4XVR Video Player.
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
> Share GLES panel images in runtime atlases to avoid the Frame's swapchain count limit (about 15 per session). Useful when opening extra menus closes them or leaves panels gray. Vulkan apps are unaffected.
### <a id="adapter-sync_guard"></a>Steady controller input after focus
For games that crash right after you return to them (for example after the Steam menu).
+1 -3
View File
@@ -12,9 +12,7 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
## Startup failures (visible in launch.log)
| Symptom | Cause | Fix |
| `RuntimeManifestFile::FindManifestFiles - using global runtime file /vendor/etc/openxr/1/active_runtime.json`, no FrameBridge lines (#254) | Embedded Khronos loader bypasses the APK loader chain | `frame.static_openxr_loader`: redirect its manifest paths and broker authorities, bundle runtime shim + inner loader + FrameBridge; reinstall to write `/sdcard/fp_openxr_runtime.json`. Device check pending |
|---|---|---|
| Valve foveation sharp centre moves to the top right or a white line appears at the right edge as Batman shrinks its projection rect (#406 / #394) | Dynamic resolution draws a corner-anchored subrect inside the full swapchain used by Valve FDM | Enable `adapter.fixed_resolution` (Game settings → Picture), rebuild/reinstall for the new adapter and OVRPlugin shim; verify `fixed_resolution: recommended ... -> ...` and full-size `projection view ... rect change` logs. Headset verification pending; more GPU load |
| Quest 3-only options missing; OVRPlugin logs `venderId 10291, systemName Oculus Quest2` (#373, Asgard's Wrath 2) | OVRPort's dispatcher overwrites FrameBridge's name and supplies a Quest 2 `XR_META_headset_id`; OVRPlugin prioritizes that UUID | `device.headset_model`: Quest 3 by default, per-game Quest 3S / Quest Pro / Quest 2; rebuild the APK. Byte-checked dispatcher name + UUID edit; Quest 2 leaves the dispatcher unchanged. See DEVICE_NOTES 'Per-game headset identity' |
| Startup exits through `libxlua.so (lj_err_throw)`, then Unity aborts in mutex cleanup; captured Lua error is `bad light userdata pointer` (GitHub #257, BattleTalent 4.7) | This older LuaJIT encodes lightuserdata in 47 bits; Frame supplies valid wider native pointers. Controlled tests with the unchanged library pass a real low-address allocation and fail its own high-address static pointer | `frame.luajit_pointers` automatically selects a full-pointer Lua engine only for the verified library SHA-256, retaining xLua bindings and custom helpers. The scoped patch reaches OpenXR startup for 60 s headless; rendering and gameplay remain unverified. A `lj_err_throw` cleanup stack without the original error message is insufficient to infer this cause |
| A GL4ES Team Beef game draws stretched triangles or spikes across characters and the scene (GitHub #191, Lambda1VR) | The newer GL shim exports GLES draw functions that intercept GL4ES's desktop OpenGL calls, bypassing its client-array and immediate-mode translation | `frame.tbxr_vendor` keeps the shim's five draw symbols private when the APK bundles `libgl4es.so`. GL queries and single-sampled framebuffer fixes remain active; other games retain the draw hooks. A/B eye captures on SteamOS 0.4.5: old shim normal, current shim spikes, current shim with private draw symbols normal at ~72 fps. Worn-headset gameplay still needs confirmation |
@@ -119,7 +117,7 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
| Vermillion: the hand and brush disappear over the canvas while painting, but still paint (GitHub #200) | the game shows a canvas being painted as its own compositor (quad) layer and turns the canvas's scene renderer off; the Frame draws quad layers above every projection layer whatever the order, so the canvas covers the hand (Vulkan game: no GL layer emulation) | `frame.vermillion_scene_canvas` (catalog): `PaintableMesh.HasCompositorLayerAvailable` / `ShouldUseCompositorOverlay` -> false in libil2cpp.so (Cpp2IL), so the game's own fallback draws the canvas in the scene (Klownicle's finding) |
| Unity game hangs right after its first frames, no `pacing:` lines; SIGQUIT dump shows UnityMain in a game library's `JNI_OnLoad` → `usleep` (for example SKYBOX: `libskybox.so`, `SignatureChecker`) | the game checks its APK signing certificate; every port is re-signed | can't be fixed without defeating anti-tamper (FramePort doesn't): mark "Can't run". Diagnose hangs: `podman exec <container> kill -3 <pid>` writes all thread stacks to launch.log |
| Video player doesn't list sent videos (for example 4XVR "Internal Storage" only shows some) | the player lists its own /sdcard folder (4XVR: /sdcard/4XPlayer), not /sdcard/Movies; Android's media index doesn't work in Lepton | game page "Add videos" / game menu "Add videos and files" (Videos + the app's own folder, hard links) or `frameport frame send <files> --app <pkg>` |
| One eye gray in a stereo video, log `xrCreateSwapchain … result=-10` | the runtime refused a swapchain (XR_ERROR_LIMIT_REACHED) | GLES: `adapter.swapchain_pool=1` shares extra panel images in atlases (#387; headset validation pending). This is a count cap, so lowering resolution does not fix it; Vulkan unaffected |
| One eye gray in a stereo video, log `xrCreateSwapchain … result=-10` | the runtime refused a swapchain (XR_ERROR_LIMIT_REACHED) | fewer/smaller swapchains: lower `equirect_res`; unresolved |
| VrApi game (32-bit bridge or `frame.vrapi_bridge`): holding the menu button to recenter turns the view ~90-120 degrees to the side | the bridge's app space is LOCAL + the head's yaw at the start; the Frame's recenter moves LOCAL to the head, so the old yaw was added again | fixed in the VrApi bridge (2026-10-13): on XR_TYPE_EVENT_DATA_REFERENCE_SPACE_CHANGE_PENDING (LOCAL) and on `vrapi_RecenterPose` it takes the origin from the head again (log `recentered: head yaw …`); rebuild the game |
| Menu/screen jumps to where you look (for example 4XVR) | the app re-creates LOCAL spaces (4XVR: every 2–4 s) and the Frame places them at the current head yaw, or the app recenters after the Frame's brief focus dips | turn on `adapter.layer_debug`, check `that LOCAL space sits at … deg` vs `focus_hold: hid a … ms focus dip`; patches: `adapter.stable_local` (new LOCAL spaces line up with the first) / `adapter.focus_hold` (on by default: hides dips up to `focus_hold_ms` once the game has been focused for a second; never at start-up: a global debounce broke AC Nexus) |
| Pointer misses menu items | the Frame's aim pose differs from Touch's | `adapter.layer_debug` logs `aim in grip … pitch/yaw`; correct with `aim_pitch`/`aim_yaw`/`aim_forward` (`frameport settings`, no rebuild) |
-4
View File
@@ -117,10 +117,6 @@ 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.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 425 KiB

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 506 KiB

After

Width:  |  Height:  |  Size: 505 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 665 KiB

After

Width:  |  Height:  |  Size: 663 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 217 KiB

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 599 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 596 KiB

After

Width:  |  Height:  |  Size: 594 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 89 KiB

+1 -32
View File
@@ -65,39 +65,8 @@ float32 xyz[vertex_count], u32 indices[index_count] (triangles; counter-clockw
```
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).
- Frame: `~/.local/share/frameport/rooms/<universe>/room.txt` + `room-mesh.bin` (+ `scan.log`).
`~/.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 ->
+10 -124
View File
@@ -2,9 +2,7 @@
`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
`room.txt` and `room-mesh.bin`. 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.
@@ -19,7 +17,7 @@ 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
`platform.h` is the reconstruction code's entire I/O/allocation boundary. The same five C sources compile on the
host:
```sh
@@ -36,13 +34,8 @@ exactly one JSON summary, whether `--json` is supplied or omitted. Success exits
{"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}}
"accepted_triangles":118804,"rejected_triangles":704,"warnings":[],
"timings_ms":{"read":0,"fusion":0,"model":0,"mesh":0,"total":0}}
```
The example above illustrates the schema; actual counts and timings depend on the input. Heights are absolute
@@ -50,10 +43,7 @@ STAGE coordinates. `frames` includes skipped frames; `skipped_frames`, `far_vert
`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.
include parsing, fusion, structural/furniture modelling, mesh generation, and total time including output.
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
@@ -67,10 +57,9 @@ triangles fails. Empty frames may accompany useful frames. Structural corruption
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
Both complete files are written to exclusive, randomly named temporary files, then individually renamed, with
the mesh first and `room.txt` last. A write failure removes the temporary files. These are atomic **file**
replacements, not a two-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.
@@ -122,77 +111,6 @@ the worktree's `FRAMEPORT_HOME`; fixtures and test output use temporary director
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
@@ -225,31 +143,6 @@ Distances are metres, areas m², volumes m³. `--universe` accepts the full unsi
| `--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`.
@@ -281,17 +174,10 @@ Incompatible inset play boundaries are ignored with a warning. Hint-completed ed
## Offline validation
`tests/test_roomscan.py` builds with the host compiler (skips only if none exists). Its tests cover the noisy
`tests/test_roomscan.py` builds with the host compiler (skips only if none exists). Its 41 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
@@ -307,7 +193,7 @@ UV_OFFLINE=1 UV_CACHE_DIR=/tmp/frameport-uv-cache uv run ruff check scripts/mr/s
```
The synthetic camera fan passes AddressSanitizer + UndefinedBehaviorSanitizer. Two consecutive final native builds
produce identical SHA-256 `2cc16b90abcd3d359f1658c581172bd650017bf2725e24268915f50904c000ec` (97,808 bytes, with refinement),
produce identical SHA-256 `0d88e5adfa1a21da7e0cfec2698a835d7767961e15c0b41c84235fa24ae1e751` (54,248 bytes),
matching `artifacts/SHA256SUMS`. ELF inspection confirms aarch64, little-endian, no PT_INTERP / PT_DYNAMIC, and no
embedded local build paths.
-18
View File
@@ -108,21 +108,3 @@ 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}
-15
View File
@@ -4,14 +4,6 @@ FrameBridge includes automatic metadata retirement for the verified Meta XR Audi
Wwise build; see [AUDIO_METADATA.md](../docs/AUDIO_METADATA.md) for its lifetime
invariant and regression coverage. It uses the existing adapter installation.
`adapter/swapchain_pool.c` implements default-off GLES `swapchain_pool` (#387): at eight live app runtime
swapchains, eligible single-layer colour images use private GL textures packed into up to two lazy 4096² sRGB
runtime atlases. EndFrame copies each referenced source once and rewrites the final quads (including cylinder
strips), cylinders and stereo projection views after existing layer fixes. OpenXR image ownership, GL bindings,
scissor state, source fences, rectangle reuse and session cleanup are covered by `tests/test_swapchain_pool.py`
(`FRAMEPORT_NATIVE_TESTS=1`, host compiler + cached OpenXR and GLES/EGL headers). Vulkan is unaffected. Host-tested;
4XVR submenu interaction, visual correctness and copy cost still need a headset check.
Sources of the prebuilt binaries in `../artifacts/` (committed, with `SHA256SUMS`). End users never compile anything;
developers rebuild with `python native/build.py` (downloads NDK r27c, OpenXR headers at pinned commits, d8 into
`native/.cache/`, git-ignored). The adapter, GL shim and platform compat rebuild byte-identically; the VrApi bridge
@@ -30,7 +22,6 @@ embeds debug paths (`-g`) and the dex depends on the d8 version, so those differ
| `zinkfix/` | `arm64-v8a/libVkLayer_fp_shaderfix.so` | Shader-fix Vulkan layer `VK_LAYER_FP_shader_fix` for OpenGL ES games (patch `frame.zink_shader_fix`): sits between Zink (Mesa's GL on Vulkan) and the driver and inserts the words of `zink_shader_fix` (same format as the Vulkan shim's `vk_shader_fix`: size + SHA-256 match) into SPIR-V modules; `zink_shader_dump=1` writes every distinct module to `files/fp_spirv/`. The engine library loads it first (DT_NEEDED); its constructor puts the layer in front of Android's GraphicsEnv debug layer list (private Android 11 `getDebugLayers`/`setDebugLayers`, found through libvulkan.so's dependencies) before EGL starts, so the loader picks it up from the app's lib dir. Exports only the enumeration functions and `VK_LAYER_FP_shader_fixGet*ProcAddr` (never plain `vk*` entry points). Vader Immortal's lightspeed shaders (Klownicle, GitHub #49). |
| `xrlayer/` | `linux-arm64/libxr_frameport_timefix.so` + `XR_APILAYER_FRAMEPORT_timefix.json` | OpenXR API layer for Windows PC VR games under Proton on the Frame (glibc aarch64, built freestanding with the NDK's clang), on by default (patch `pcvr.xr_timefix`). Retries `xrCreateInstance` as an OpenXR 1.0 app when the runtime rejects 1.1 (the Frame's SteamVR runtime does, and Proton 11's VR helper asks for 1.1). Also emulates `xrConvertTimespecTimeToTimeKHR`/`xrConvertTimeToTimespecTimeKHR` if the runtime refuses them (the Frame's Android runtime does; its Linux runtime, used by Proton, supports them as of 2026-09-29 — so this part is only a fallback; Proton's wineopenxr needs them for `XR_KHR_win32_convert_performance_counter_time`, which Revive requires) with the adapter's xrWaitFrame-calibrated offset, and drops the extension from xrCreateInstance if the runtime rejects it. Enabled per game by the Proton launch.sh (`XR_API_LAYER_PATH`, `XR_ENABLE_API_LAYERS`). `tests/test_xrlayer.py` builds it for x86_64 and drives it via ctypes. |
| `oculushmd/` | `win-x64/fp_oculushmd.exe` | PC VR counterpart of overport's `patch_oculus_unreal` (patch `pcvr.oculus_unreal`, default for Unreal Rift games on the Frame). Unreal's OculusHMD and LibOVR's `ovr_Detect` only start when the Windows named event `OculusHMDConnected` exists and is signalled (the Oculus service creates it). `fp_oculushmd.exe <command line>` creates it (manual reset, signalled), runs the command (Revive's injector) in a job object and keeps the event until every process in the job — the game and its children — has exited (fallback without job support: 3 minutes); returns the command's exit code and logs `FramePort oculushmd: …` to stderr (launch.log). Freestanding Windows x64 console exe (no CRT, no Windows SDK): the NDK's clang + `lld-link /Brepro` with a kernel32 import library generated by `llvm-dlltool`, so it's byte-reproducible. Tested on Windows from WSL with a build whose event name is `-DEVENT_NAME=L"…"` (the Oculus service owns the real event on a PC with the Oculus app). |
| `xrruntime/` | `arm64-v8a/libfp_xrruntime.so` | Runtime negotiation endpoint for embedded Khronos loaders (`frame.static_openxr_loader`). Loads FrameBridge by soname, forwards Android loader initialization through its GIPA, and returns FrameBridge GIPA. The unmodified inner Khronos loader is reused from OVRPort’s generic loader (also when `overport: False`); its paths remain system paths. Built with `--only xrruntime`. |
| `xrshim/` | `arm64-v8a/libframe_xrshim.so` | FrameBridge extension shim, only in builds with `controller_models=1`. overport's dispatcher (`libopenxr_loader.so`) answers `xrGetInstanceProcAddr` from a fixed table and refuses everything else (`overportOXR: Unknown proc addr: xrLoadRenderModelFB`), so the adapter never sees XR_FB_render_model lookups. Meta's OVRPlugin gets the function with `dlopen("libopenxr_loader.so")` + `dlsym`, so FramePort rewrites that `.rodata` string in `libOVRPlugin.so` (same length, in place) to `libframe_xrshim.so`. The shim returns the adapter's emulated functions (`framebridge_extension_proc`) and forwards every other name to overport. A DT_NEEDED interposer does not work here (dlsym on a handle searches that library first); tried and dropped. |
| `venc/` | `linux-arm64-bin/fp_venc` | Hardware H.264 encoder for the live view, run on the Frame (spec: `venc/SPEC.md`). Reads SteamVR's "headset view" webcam (v4l2loopback `/dev/video99`, RGB24), box-scales and converts it to NV12 (BT.709 limited range; `convert.c`, NEON with a bit-identical scalar reference, also built for x86_64 by the host test), and feeds the SoC's V4L2 stateful encoder (`dev-encoder.rst`, Qualcomm iris `/dev/video-enc0`): CBR, no B-frames, SPS/PPS on every IDR, one frame per slot of a fixed grid (fps = a divisor of the panel refresh read through DRM KMS, default cap 45), Annex B on stdout. stdin `k` = keyframe, `q`/EOF = drain and stop; `--probe` prints the setup as JSON, `--selftest` compares NEON and scalar output. Fully static and freestanding (raw syscalls, own `_start`; only the NDK's kernel UAPI headers, staged in `.cache/uapi-arm64`), byte-reproducible. Lives in `linux-arm64-bin/` because `linux-arm64/` is uploaded whole as the OpenXR layer. |
| `java-stubs/` | `dex/oculusos-stubs.dex` | No-op `com.oculus.os.AnalyticsEvent` / `UnifiedTelemetryLogger` for native code that looks them up. |
@@ -40,9 +31,3 @@ missing symbols (no compiler needed).
After changing anything here: `python native/build.py`, commit artifacts + SHA256SUMS, then run `frameport parity`
(the report marks updated binaries as "expected") and a device launch test.
`adapter.fixed_resolution` (off by default, #406/#394) keeps META recommended layer
resolution at tracked full swapchain size. Both adapter GIPA and xrshim's existing
extension entry point serve it; the setting adds OVRPlugin shim routing. The extension
stays visible, avoiding dependence on a plugin fallback. No adapter revision bump for
this opt-in fix. See DEVICE_NOTES "Fixed layer resolution / Batman" for verification.
+1 -2
View File
@@ -42,8 +42,7 @@ static XRAPI_ATTR XrResult XRAPI_CALL eye_hook_xrReleaseSwapchainImage(XrSwapcha
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
if (is_standin(swapchain)) return XR_SUCCESS;
if (release_wait_active()) eye_wait_for_gpu();
int pooled = pool_find(swapchain);
XrResult result = pooled >= 0 ? pool_cycle(pooled, 2, NULL) : fn(swapchain, info);
XrResult result = fn(swapchain, info);
if (eye_debug) {
int64_t now = monotonic_ns();
pthread_mutex_lock(&eye_lock);
-22
View File
@@ -1,22 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
// swapchain_size is the adapter's mutex-protected table, shared with rect_clamp.
static int fixed_layer_size(const XrCompositionLayerBaseHeader *layer, XrExtent2Di *full) {
if (!layer) return 0;
uint32_t w = 0, h = 0;
if (layer->type == XR_TYPE_COMPOSITION_LAYER_PROJECTION) {
const XrCompositionLayerProjection *p = (const XrCompositionLayerProjection *)layer;
if (!p->viewCount || !p->views) return 0;
for (uint32_t v = 0; v < p->viewCount; ++v) {
uint32_t vw, vh;
if (!swapchain_size(p->views[v].subImage.swapchain, &vw, &vh)) return 0;
if (v && (vw != w || vh != h)) return 0; // no single full size for this layer
w = vw; h = vh;
}
} else if (layer->type == XR_TYPE_COMPOSITION_LAYER_QUAD) {
const XrCompositionLayerQuad *q = (const XrCompositionLayerQuad *)layer;
if (!swapchain_size(q->subImage.swapchain, &w, &h)) return 0;
} else return 0; // preserve runtime behavior for other layer types
if (!w || !h || w > INT32_MAX || h > INT32_MAX) return 0;
full->width = (int32_t)w; full->height = (int32_t)h;
return 1;
}
+6 -117
View File
@@ -131,8 +131,6 @@ static int swapchain_fix = 1;
static int cube_quads = 1;
static int cube_standin = 1; // cube swapchains the runtime refuses: served by the adapter (cube_standin.c)
static int rect_clamp = 1;
static int fixed_resolution;
static int swapchain_pool = 0;
static int fb_color_space; // fb_color_space: offer XR_FB_color_space when the runtime lacks it (#336)
static int action_dedup = 1; // rename duplicate action names in a set (session_fixes.c; GitHub #323)
static int pose_consistency = 0; // repeat xrLocateViews queries for one display time get the same poses (GitHub #8) // clamp submitted image rects to their swapchain (SteamVR rejects a 1 px overrun)
@@ -212,9 +210,7 @@ static void read_settings(const char *path) {
if (sscanf(line, "swapchain_fix=%f", &value) == 1) swapchain_fix = value != 0;
if (sscanf(line, "cube_quads=%f", &value) == 1) cube_quads = value != 0;
if (sscanf(line, "cube_standin=%f", &value) == 1) cube_standin = value != 0;
if (sscanf(line, "fixed_resolution=%f", &value) == 1) fixed_resolution = value != 0;
if (sscanf(line, "rect_clamp=%f", &value) == 1) rect_clamp = value != 0;
if (sscanf(line, "swapchain_pool=%f", &value) == 1) swapchain_pool = value != 0;
if (sscanf(line, "fb_color_space=%f", &value) == 1) fb_color_space = value != 0;
if (sscanf(line, "action_dedup=%f", &value) == 1) action_dedup = value != 0;
if (sscanf(line, "pose_consistency=%f", &value) == 1) pose_consistency = value != 0;
@@ -307,7 +303,6 @@ static void initialize(void) {
if (input_diag) LOG("per-game: input_diag=1 (controller-input diagnostics)");
if (proximity_emul) LOG("per-game: proximity_emul=%d (finger proximity from capacitive touch: %s)", proximity_emul,
proximity_emul > 1 ? "thumb + index" : "thumb");
if (fixed_resolution) LOG("per-game: fixed_resolution=1 (full swapchain recommendations)");
if (hide_fb_foveation) LOG("per-game: hide_fb_foveation=1 (Quest Vulkan foveation suppressed)");
if (hide_space_warp) LOG("per-game: hide_space_warp=1 (XR_FB_space_warp hidden, space warp info removed)");
if (strip_color_bias) LOG("per-game: strip_color_bias=%d (layer color scale/bias removed)", strip_color_bias);
@@ -565,7 +560,7 @@ static int64_t pick_format(XrSession session, int64_t wanted) {
return chosen;
}
// Known app swapchains, including pooled images rewritten into runtime atlases before submission.
// Swapchains the runtime actually created; layers referencing anything else are dropped.
static pthread_mutex_t swapchains_lock = PTHREAD_MUTEX_INITIALIZER;
static XrSwapchain *swapchains;
static size_t swapchain_count, swapchain_capacity;
@@ -606,29 +601,6 @@ static int swapchain_size(XrSwapchain handle, uint32_t *w, uint32_t *h) {
return found;
}
#include "fixed_resolution.h"
static XRAPI_ATTR XrResult XRAPI_CALL fixed_recommended_resolution(XrSession session,
const XrRecommendedLayerResolutionGetInfoMETA *info, XrRecommendedLayerResolutionMETA *out) {
PFN_xrGetRecommendedLayerResolutionMETA fn = (PFN_xrGetRecommendedLayerResolutionMETA)
lookup(active_instance, "xrGetRecommendedLayerResolutionMETA");
if (!info || !out) return XR_ERROR_VALIDATION_FAILURE;
XrExtent2Di full;
if (!fixed_layer_size(info->layer, &full))
return fn ? fn(session, info, out) : XR_ERROR_FUNCTION_UNSUPPORTED;
// Read the runtime recommendation only for diagnostics. The override also works
// when the runtime lacks this function (OVRPort advertises it itself).
XrRecommendedLayerResolutionMETA old = {XR_TYPE_RECOMMENDED_LAYER_RESOLUTION_META};
XrResult result = fn ? fn(session, info, &old) : XR_ERROR_FUNCTION_UNSUPPORTED;
static unsigned logged;
if (logged++ < 5) LOG("fixed_resolution: recommended %dx%d -> %dx%d (runtime=%d)",
old.recommendedImageDimensions.width, old.recommendedImageDimensions.height,
full.width, full.height, result);
out->recommendedImageDimensions = full;
out->isValid = XR_TRUE;
return XR_SUCCESS;
}
// Clamp `r` to a w x h image; returns whether it changed (2 = it was empty and now covers the whole image).
static int clamp_rect(XrRect2Di *r, uint32_t w, uint32_t h) {
XrRect2Di c = *r;
@@ -653,10 +625,6 @@ static int clamp_rect(XrRect2Di *r, uint32_t w, uint32_t h) {
}
static void forget_swapchain(XrSwapchain handle) {
pthread_mutex_lock(&sizes_lock);
for (int i = 0; i < MAX_SIZES; ++i)
if (sizes[i].handle == handle) memset(&sizes[i], 0, sizeof sizes[i]);
pthread_mutex_unlock(&sizes_lock);
pthread_mutex_lock(&swapchains_lock);
for (size_t i = 0; i < swapchain_count; ++i)
if (swapchains[i] == handle) { swapchains[i] = swapchains[--swapchain_count]; break; }
@@ -682,16 +650,6 @@ static void cube_outputs_destroy(void);
static void cube_session_destroy(XrSession session);
static XrResult standin_enumerate(XrSwapchain handle, uint32_t capacity, uint32_t *count, XrSwapchainImageBaseHeader *images);
static int pool_find(XrSwapchain h);
static int pool_should_create(XrSession session);
static void pool_note_runtime(XrSession session, XrSwapchain handle);
static void pool_forget_runtime(XrSwapchain handle);
static int pool_create(XrSession session, const XrSwapchainCreateInfo *ci, XrSwapchain *out, int limited);
static void pool_destroy(int i);
static void pool_cleanup(XrSession session);
static XrResult pool_cycle(int i, int op, uint32_t *index);
static XrResult pool_enumerate(int i, uint32_t cap, uint32_t *count, XrSwapchainImageBaseHeader *images);
static int known_swapchain(XrSwapchain handle) {
int found = 0;
pthread_mutex_lock(&swapchains_lock);
@@ -779,8 +737,6 @@ XRAPI_ATTR XrResult XRAPI_CALL xrCreateSwapchain(XrSession session, const XrSwap
snap_on_create(*swapchain, &fixed);
return XR_SUCCESS;
}
// Start before the session's ~15-slot cap; leave headroom for 360/video outputs and a second atlas.
if (pool_should_create(session) && pool_create(session, &fixed, swapchain, 0)) return XR_SUCCESS;
clear_gl_errors("xrCreateSwapchain");
XrResult result = fn(session, &fixed, swapchain);
LOG("xrCreateSwapchain %ux%u format=%lld samples=%u array=%u faces=%u usage=0x%llx flags=0x%llx result=%d handle=%p",
@@ -788,7 +744,6 @@ XRAPI_ATTR XrResult XRAPI_CALL xrCreateSwapchain(XrSession session, const XrSwap
(unsigned long long)fixed.usageFlags, (unsigned long long)fixed.createFlags, result,
(void *)(uintptr_t)(XR_SUCCEEDED(result) && swapchain ? *swapchain : 0));
if (XR_SUCCEEDED(result)) {
pool_note_runtime(session, *swapchain);
remember_swapchain(*swapchain);
note_swapchain_size(*swapchain, fixed.width, fixed.height);
flip_on_create_swapchain(*swapchain, &fixed);
@@ -796,7 +751,6 @@ XRAPI_ATTR XrResult XRAPI_CALL xrCreateSwapchain(XrSession session, const XrSwap
snap_on_create(*swapchain, &fixed);
return result;
}
if (result == XR_ERROR_LIMIT_REACHED && pool_create(session, &fixed, swapchain, 1)) return XR_SUCCESS;
if (!swapchain_fix) return standin_after_failure(session, &fixed, swapchain, result);
// Frame's runtime rejects some GLES formats (GL_RGBA8) and MSAA swapchains (overport #71).
@@ -814,14 +768,12 @@ XRAPI_ATTR XrResult XRAPI_CALL xrCreateSwapchain(XrSession session, const XrSwap
}
}
if (XR_SUCCEEDED(result)) {
pool_note_runtime(session, *swapchain);
remember_swapchain(*swapchain);
note_swapchain_size(*swapchain, fixed.width, fixed.height);
flip_on_create_swapchain(*swapchain, &fixed);
emul_on_create_swapchain(*swapchain, &fixed);
snap_on_create(*swapchain, &fixed);
}
if (result == XR_ERROR_LIMIT_REACHED && pool_create(session, &fixed, swapchain, 1)) return XR_SUCCESS;
return standin_after_failure(session, &fixed, swapchain, result);
}
@@ -832,9 +784,6 @@ XRAPI_ATTR XrResult XRAPI_CALL xrDestroySwapchain(XrSwapchain swapchain) {
static int logged;
if (logged++ < 200) LOG("layer_debug: xrDestroySwapchain %p", (void *)swapchain);
}
pool_forget_runtime(swapchain);
int pooled = pool_find(swapchain);
if (pooled >= 0) { pool_destroy(pooled); return XR_SUCCESS; }
if (is_standin(swapchain)) { standin_destroy(swapchain); return XR_SUCCESS; }
surf_scene_destroy(swapchain);
forget_swapchain(swapchain);
@@ -891,7 +840,7 @@ static int cylinder_strips(const XrCompositionLayerCylinderKHR *c, XrComposition
}
// A layer is usable if every swapchain it references was created successfully.
// Runtime images and pooled images (rewritten after layer fixes). Adapter-served 360 pictures only feed its drawing.
// Swapchains the runtime can show: created by it (adapter-served 360 pictures only feed the adapter's own drawing).
static int runtime_swapchain(XrSwapchain handle) {
return known_swapchain(handle) && !(equirect_emul && emul_is_virtual(handle));
}
@@ -1175,7 +1124,6 @@ XRAPI_ATTR XrResult XRAPI_CALL xrDestroySpace(XrSpace space) {
#include "input_diag.c"
#include "session_fixes.c"
#include "layer_emul_gl.c"
#include "swapchain_pool.c"
#include "cube_standin.c"
#include "snapshot_gl.c"
#include "surface_swapchain.c"
@@ -1362,10 +1310,6 @@ XRAPI_ATTR XrResult XRAPI_CALL xrCreateSession(XrInstance instance, const XrSess
scene_on_create_session(*session);
session_fixes_on_create_session(*session);
emul_on_create_session(*session, info);
if (swapchain_pool) {
for (const XrBaseInStructure *p = info->next; p; p = p->next)
if (p->type == XR_TYPE_GRAPHICS_BINDING_OPENGL_ES_ANDROID_KHR) pool_session = *session;
}
fade_on_create_session(*session, info);
}
return result;
@@ -1434,12 +1378,6 @@ XRAPI_ATTR XrResult XRAPI_CALL xrEnumerateSwapchainImages(XrSwapchain swapchain,
XrSwapchainImageBaseHeader *images) {
PFN_xrEnumerateSwapchainImages fn = (PFN_xrEnumerateSwapchainImages)lookup(active_instance, "xrEnumerateSwapchainImages");
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
int pooled = pool_find(swapchain);
if (pooled >= 0) {
XrResult r = pool_enumerate(pooled, capacity, count, images);
if (XR_SUCCEEDED(r) && capacity && images) emul_on_enumerate(swapchain, 1, images);
return r;
}
if (is_standin(swapchain)) return standin_enumerate(swapchain, capacity, count, images);
XrResult result = equirect_emul && emul_is_virtual(swapchain) ? emul_virtual_enumerate(swapchain, capacity, count, images)
: hide_fb_foveation_vulkan ? fb_hide_foveation_images(fn, swapchain, capacity, count, images)
@@ -1456,8 +1394,6 @@ XRAPI_ATTR XrResult XRAPI_CALL xrEnumerateSwapchainImages(XrSwapchain swapchain,
XRAPI_ATTR XrResult XRAPI_CALL xrWaitSwapchainImage(XrSwapchain swapchain, const XrSwapchainImageWaitInfo *info) {
PFN_xrWaitSwapchainImage fn = (PFN_xrWaitSwapchainImage)lookup(active_instance, "xrWaitSwapchainImage");
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
int pooled = pool_find(swapchain);
if (pooled >= 0) return pool_cycle(pooled, 1, NULL);
if (is_standin(swapchain)) return XR_SUCCESS;
XrResult result = fn(swapchain, info);
__atomic_add_fetch(&sc_waits, 1, __ATOMIC_RELAXED);
@@ -1468,8 +1404,6 @@ XRAPI_ATTR XrResult XRAPI_CALL xrWaitSwapchainImage(XrSwapchain swapchain, const
XRAPI_ATTR XrResult XRAPI_CALL xrReleaseSwapchainImage(XrSwapchain swapchain, const XrSwapchainImageReleaseInfo *info) {
PFN_xrReleaseSwapchainImage fn = (PFN_xrReleaseSwapchainImage)lookup(active_instance, "xrReleaseSwapchainImage");
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
int pooled = pool_find(swapchain);
if (pooled >= 0) return pool_cycle(pooled, 2, NULL);
if (is_standin(swapchain)) return XR_SUCCESS;
__atomic_add_fetch(&sc_releases, 1, __ATOMIC_RELAXED);
return fn(swapchain, info);
@@ -1479,12 +1413,6 @@ XRAPI_ATTR XrResult XRAPI_CALL xrAcquireSwapchainImage(XrSwapchain swapchain, co
uint32_t *index) {
PFN_xrAcquireSwapchainImage fn = (PFN_xrAcquireSwapchainImage)lookup(active_instance, "xrAcquireSwapchainImage");
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
int pooled = pool_find(swapchain);
if (pooled >= 0) {
XrResult r = pool_cycle(pooled, 0, index);
if (XR_SUCCEEDED(r)) emul_on_acquire(swapchain, *index);
return r;
}
if (is_standin(swapchain)) { // adapter-served cube stand-in: one image
if (!index) return XR_ERROR_VALIDATION_FAILURE;
*index = 0;
@@ -1513,8 +1441,7 @@ static XRAPI_ATTR XrResult XRAPI_CALL surf_composite_release(XrSwapchain handle,
tracked_swapchain *t=find_tracked(handle,0);
if(t && t->acquired_count && t->waited_count)surf_scene_capture(t,t->acquired[0]);
pthread_mutex_unlock(&flip_lock);
XrResult result=pool_find(handle)>=0 && !equirect_emul ? xrReleaseSwapchainImage(handle,info) :
equirect_emul?hook_xrReleaseSwapchainImage(handle,info):
XrResult result=equirect_emul?hook_xrReleaseSwapchainImage(handle,info):
((PFN_xrReleaseSwapchainImage)lookup(active_instance,"xrReleaseSwapchainImage"))(handle,info);
if(XR_SUCCEEDED(result)) {
surf_scene_release_commit(handle);
@@ -1528,8 +1455,7 @@ static XRAPI_ATTR XrResult XRAPI_CALL surf_composite_release(XrSwapchain handle,
return result;
}
static XRAPI_ATTR XrResult XRAPI_CALL surf_composite_wait(XrSwapchain handle,const XrSwapchainImageWaitInfo *info) {
XrResult result=pool_find(handle)>=0 && !equirect_emul ? xrWaitSwapchainImage(handle,info) :
equirect_emul?hook_xrWaitSwapchainImage(handle,info):
XrResult result=equirect_emul?hook_xrWaitSwapchainImage(handle,info):
((PFN_xrWaitSwapchainImage)lookup(active_instance,"xrWaitSwapchainImage"))(handle,info);
if(result==XR_SUCCESS) {
pthread_mutex_lock(&flip_lock);tracked_swapchain *t=find_tracked(handle,0);
@@ -1843,8 +1769,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrEndFrame(XrSession session, const XrFrameEndInf
frames = 0; drift_sum = drift_max = 0;
}
}
if (swapchain_pool && info && info->layerCount > 64) return XR_ERROR_LAYER_LIMIT_EXCEEDED;
if ((!swapchain_pool && !cube_standin && !layer_fix && !swap_eyes && !emulate_passthrough && !flip_quads && !flip_emul && !strip_depth && !fade_quad) || !info || !info->layerCount ||
if ((!cube_standin && !layer_fix && !swap_eyes && !emulate_passthrough && !flip_quads && !flip_emul && !strip_depth && !fade_quad) || !info || !info->layerCount ||
info->layerCount > 64)
return fn(session, info);
if (layer_debug) { // the submitted layer list (order, flags, eyes, chained structs) whenever its shape changes
@@ -2224,11 +2149,6 @@ XRAPI_ATTR XrResult XRAPI_CALL xrEndFrame(XrSession session, const XrFrameEndInf
for (uint32_t i = 0; i < tcount; ++i) kept[i] = trimmed[i];
count = tcount;
}
PoolFrame pool_frame;
if (swapchain_pool && session == pool_session) {
count = pool_rewrite_layers(kept, count, &pool_frame);
++swapped;
}
XrFrameEndInfo fixed = *info;
fixed.layers = kept;
fixed.layerCount = count;
@@ -2236,30 +2156,6 @@ XRAPI_ATTR XrResult XRAPI_CALL xrEndFrame(XrSession session, const XrFrameEndInf
static int warned, failures, passthrough_logged;
if (dropped && warned++ < 5) LOG("xrEndFrame: dropped %u unusable layer(s)", dropped);
if (passthrough && !passthrough_logged++) LOG("xrEndFrame: passthrough layer -> ALPHA_BLEND environment");
static struct { XrSwapchain swapchain; XrRect2Di rect; uint32_t array; } last_rect[2];
static double last_rect_log[2];
struct timespec rect_now;
clock_gettime(CLOCK_MONOTONIC, &rect_now);
double rect_seconds = rect_now.tv_sec + rect_now.tv_nsec / 1e9;
for (uint32_t i = 0; i < info->layerCount; ++i) {
const XrCompositionLayerBaseHeader *l = info->layers[i];
if (!l || l->type != XR_TYPE_COMPOSITION_LAYER_PROJECTION) continue;
const XrCompositionLayerProjection *p = (const XrCompositionLayerProjection *)l;
for (uint32_t v = 0; v < p->viewCount && v < 2; ++v) {
const XrSwapchainSubImage *sub = &p->views[v].subImage;
if ((last_rect[v].swapchain != sub->swapchain || last_rect[v].array != sub->imageArrayIndex ||
memcmp(&last_rect[v].rect, &sub->imageRect, sizeof(sub->imageRect))) &&
rect_seconds - last_rect_log[v] >= 1.0) {
LOG("projection view %u rect change: swapchain=%p rect=%d,%d %dx%d array=%u", v,
(void *)(uintptr_t)sub->swapchain, sub->imageRect.offset.x, sub->imageRect.offset.y,
sub->imageRect.extent.width, sub->imageRect.extent.height, sub->imageArrayIndex);
last_rect[v].swapchain = sub->swapchain;
last_rect[v].rect = sub->imageRect;
last_rect[v].array = sub->imageArrayIndex;
last_rect_log[v] = rect_seconds;
}
}
}
static int views_logged, quads_logged;
if (views_logged < 3 || quads_logged < 10) { // describe the stereo layer layout and quads
for (uint32_t i = 0; i < fixed.layerCount; ++i) {
@@ -2499,8 +2395,6 @@ static XRAPI_ATTR XrResult XRAPI_CALL shim_poll_event(XrInstance instance, XrEve
__attribute__((visibility("default"))) PFN_xrVoidFunction framebridge_extension_proc(const char *name) {
pthread_once(&init_once, initialize);
if (!name) return NULL;
if (fixed_resolution && !strcmp(name, "xrGetRecommendedLayerResolutionMETA"))
return (PFN_xrVoidFunction)fixed_recommended_resolution;
if (!strcmp(name, "framebridge_hide_fb_foveation"))
return (PFN_xrVoidFunction)framebridge_hide_fb_foveation;
if (hide_fb_foveation_vulkan) {
@@ -2566,10 +2460,6 @@ XRAPI_ATTR XrResult XRAPI_CALL xrGetInstanceProcAddr(XrInstance instance, const
*function = (PFN_xrVoidFunction)fb_hide_update_swapchain;
return XR_SUCCESS;
}
if (fixed_resolution && !strcmp(name, "xrGetRecommendedLayerResolutionMETA")) {
*function = (PFN_xrVoidFunction)fixed_recommended_resolution;
return XR_SUCCESS;
}
HOOK(xrGetSystemProperties)
HOOK(xrApplyHapticFeedback)
HOOK(xrCreateReferenceSpace)
@@ -2603,7 +2493,6 @@ XRAPI_ATTR XrResult XRAPI_CALL xrGetInstanceProcAddr(XrInstance instance, const
HOOK_AS(xrReleaseSwapchainImage, surf_composite_release)
HOOK_AS(xrWaitSwapchainImage, surf_composite_wait)
}
if (swapchain_pool) { HOOK_AS(xrDestroySession, hook_xrDestroySession) }
if (cube_standin) {
HOOK_AS(xrEndSession, cube_end_session)
HOOK_AS(xrDestroySession, hook_xrDestroySession)
@@ -2622,7 +2511,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrGetInstanceProcAddr(XrInstance instance, const
if (instance != XR_NULL_HANDLE) HOOK_AS(xrGetDisplayRefreshRateFB, hook_get_refresh_rate)
if (surface_emul) HOOK_AS(xrCreateSwapchainAndroidSurfaceKHR, hook_xrCreateSwapchainAndroidSurfaceKHR)
if (eye_debug || release_wait) HOOK_AS(xrReleaseSwapchainImage, eye_hook_xrReleaseSwapchainImage)
if (swapchain_pool || layer_debug || cube_standin) { // pooled images, counters + cube stand-ins; every other wait/release hook above takes precedence
if (layer_debug || cube_standin) { // counters + cube stand-ins; every other wait/release hook above takes precedence
HOOK(xrWaitSwapchainImage)
HOOK(xrReleaseSwapchainImage)
}
+2 -7
View File
@@ -304,7 +304,6 @@ static void emul_on_destroy_swapchain(XrSwapchain handle) {
static XRAPI_ATTR XrResult XRAPI_CALL hook_xrReleaseSwapchainImage(XrSwapchain swapchain, const XrSwapchainImageReleaseInfo *info) {
PFN_xrReleaseSwapchainImage fn = (PFN_xrReleaseSwapchainImage)lookup(active_instance, "xrReleaseSwapchainImage");
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
int pooled = pool_find(swapchain);
if (is_standin(swapchain)) return XR_SUCCESS;
pthread_mutex_lock(&emul_lock);
emul_source *s = emul_active() ? emul_find_source(swapchain, 0) : NULL;
@@ -315,8 +314,7 @@ static XRAPI_ATTR XrResult XRAPI_CALL hook_xrReleaseSwapchainImage(XrSwapchain s
if (fence) p_glFlush();
}
pthread_mutex_unlock(&emul_lock);
XrResult result = pooled >= 0 ? pool_cycle(pooled, 2, NULL) :
emul_is_virtual(swapchain) ? XR_SUCCESS : fn(swapchain, info);
XrResult result = emul_is_virtual(swapchain) ? XR_SUCCESS : fn(swapchain, info);
pthread_mutex_lock(&emul_lock);
s = emul_active() ? emul_find_source(swapchain, 0) : NULL;
if (s && XR_SUCCEEDED(result) && s->acquired_count) {
@@ -338,10 +336,8 @@ static XRAPI_ATTR XrResult XRAPI_CALL hook_xrReleaseSwapchainImage(XrSwapchain s
static XRAPI_ATTR XrResult XRAPI_CALL hook_xrWaitSwapchainImage(XrSwapchain swapchain, const XrSwapchainImageWaitInfo *info) {
PFN_xrWaitSwapchainImage fn = (PFN_xrWaitSwapchainImage)lookup(active_instance, "xrWaitSwapchainImage");
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
int pooled = pool_find(swapchain);
if (is_standin(swapchain)) return XR_SUCCESS;
XrResult result = pooled >= 0 ? pool_cycle(pooled, 1, NULL) :
emul_is_virtual(swapchain) ? XR_SUCCESS : fn(swapchain, info);
XrResult result = emul_is_virtual(swapchain) ? XR_SUCCESS : fn(swapchain, info);
pthread_mutex_lock(&emul_lock);
emul_source *s = emul_active() ? emul_find_source(swapchain, 0) : NULL;
if (s && s->reading >= 0 && s->reading == s->last_acquired) {
@@ -1009,7 +1005,6 @@ static void emul_on_destroy_session(XrSession session) {
static XRAPI_ATTR XrResult XRAPI_CALL hook_xrDestroySession(XrSession session) {
cube_session_destroy(session);
emul_on_destroy_session(session);
pool_cleanup(session);
PFN_xrDestroySession fn = (PFN_xrDestroySession)lookup(active_instance, "xrDestroySession");
return fn ? fn(session) : XR_ERROR_FUNCTION_UNSUPPORTED;
}
-26
View File
@@ -1,26 +0,0 @@
// First-fit edge packer. Removing a live rectangle immediately frees its space.
#pragma once
#include <stdint.h>
typedef struct {
int x, y, w, h;
} PoolRect;
static int pool_place(const PoolRect *used, int n, int side, int w, int h, PoolRect *out) {
if (w <= 0 || h <= 0 || w > side || h > side) return 0;
for (int yi = -1; yi < n; ++yi)
for (int xi = -1; xi < n; ++xi) {
int x = xi < 0 ? 0 : used[xi].x + used[xi].w;
int y = yi < 0 ? 0 : used[yi].y + used[yi].h;
if (x > side - w || y > side - h) continue;
int fits = 1;
for (int i = 0; i < n; ++i)
if (x < used[i].x + used[i].w && x + w > used[i].x && y < used[i].y + used[i].h && y + h > used[i].y) {
fits = 0;
break;
}
if (fits) {
*out = (PoolRect){x, y, w, h};
return 1;
}
}
return 0;
}
-389
View File
@@ -1,389 +0,0 @@
// Included after layer_emul_gl.c: reuses its dynamically loaded GLES entry points.
// Proactive at eight app swapchains: an atlas cannot be allocated after the runtime cap.
#include "pool_rect.h"
#define POOL_MAX 128
#define POOL_SIDE 4096
static XrSession pool_session;
static struct {
XrSwapchain handle;
GLuint images[16];
uint32_t count, index;
int acquired, waited;
} pool_atlas[2];
static struct {
XrSwapchain handle;
GLuint tex;
PoolRect rect;
int atlas, state, released, copied;
GLsync fence, read_fence;
} pool_images[POOL_MAX];
static unsigned pool_errors;
static XrSwapchain pool_runtime[256];
static unsigned pool_runtime_count;
static void pool_note_runtime_locked(XrSession session, XrSwapchain handle) {
if (!swapchain_pool || session != pool_session) return;
for (int i = 0; i < 256; ++i)
if (!pool_runtime[i]) {
pool_runtime[i] = handle;
++pool_runtime_count;
return;
}
}
static void pool_forget_runtime_locked(XrSwapchain handle) {
for (int i = 0; i < 256; ++i)
if (pool_runtime[i] == handle && handle) {
pool_runtime[i] = XR_NULL_HANDLE;
--pool_runtime_count;
return;
}
}
static int pool_should_create_locked(XrSession session) {
return swapchain_pool && session == pool_session && pool_runtime_count >= 8;
}
static int pool_find_locked(XrSwapchain h) {
for (int i = 0; h && i < POOL_MAX; ++i)
if (pool_images[i].handle == h) return i;
return -1;
}
static void pool_error(void) {
if (pool_errors++ % 240 == 0) LOG("swapchain_pool: atlas update failed; omitting affected layers");
}
static int pool_create_locked(XrSession session, const XrSwapchainCreateInfo *ci, XrSwapchain *out, int limited) {
if (!swapchain_pool || session != pool_session || !out || ci->next || ci->faceCount != 1 || ci->arraySize != 1 ||
ci->sampleCount != 1 || ci->mipCount != 1 || !(ci->usageFlags & XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT) ||
(ci->createFlags & ~XR_SWAPCHAIN_CREATE_STATIC_IMAGE_BIT) ||
(ci->format != GL_RGBA8 && ci->format != GL_SRGB8_ALPHA8) || !ci->width || !ci->height ||
ci->width > POOL_SIDE || ci->height > POOL_SIDE)
return 0;
if (!p_eglCreateContext && !emul_load_gl()) return 0;
if (p_eglGetCurrentContext() == EGL_NO_CONTEXT) return 0;
// Like emul_virtual_create: stale application GL errors must not look like allocation failures.
for (int i = 0; i < 8 && p_glGetError() != GL_NO_ERROR; ++i) {
}
int slot = -1;
for (int i = 0; i < POOL_MAX; ++i)
if (!pool_images[i].handle) {
slot = i;
break;
}
if (slot < 0) return 0;
for (int a = 0; a < 2; ++a) {
PoolRect used[POOL_MAX], rect;
int n = 0;
for (int i = 0; i < POOL_MAX; ++i)
if (pool_images[i].handle && pool_images[i].atlas == a) used[n++] = pool_images[i].rect;
if (!pool_place(used, n, POOL_SIDE, ci->width, ci->height, &rect)) continue;
if (!pool_atlas[a].handle) {
XrSwapchainCreateInfo ai = {XR_TYPE_SWAPCHAIN_CREATE_INFO};
ai.width = ai.height = POOL_SIDE;
ai.arraySize = ai.faceCount = ai.sampleCount = ai.mipCount = 1;
ai.format = GL_SRGB8_ALPHA8;
ai.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_TRANSFER_DST_BIT;
PFN_xrCreateSwapchain create = (PFN_xrCreateSwapchain)lookup(active_instance, "xrCreateSwapchain");
PFN_xrEnumerateSwapchainImages enumerate =
(PFN_xrEnumerateSwapchainImages)lookup(active_instance, "xrEnumerateSwapchainImages");
if (XR_FAILED(create(session, &ai, &pool_atlas[a].handle))) return 0;
emul_gles_image images[16];
for (int j = 0; j < 16; ++j) images[j] = (emul_gles_image){XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_ES_KHR};
if (XR_FAILED(enumerate(pool_atlas[a].handle, 16, &pool_atlas[a].count, (void *)images)) ||
!pool_atlas[a].count || pool_atlas[a].count > 16) {
((PFN_xrDestroySwapchain)lookup(active_instance, "xrDestroySwapchain"))(pool_atlas[a].handle);
memset(&pool_atlas[a], 0, sizeof pool_atlas[a]);
return 0;
}
for (uint32_t j = 0; j < pool_atlas[a].count; ++j) pool_atlas[a].images[j] = images[j].image;
}
GLint old;
GLuint tex = 0;
p_glGetIntegerv(GL_TEXTURE_BINDING_2D, &old);
p_glGenTextures(1, &tex);
p_glBindTexture(GL_TEXTURE_2D, tex);
p_glTexStorage2D(GL_TEXTURE_2D, 1, ci->format, ci->width, ci->height);
GLenum err = p_glGetError();
p_glBindTexture(GL_TEXTURE_2D, old);
if (err != GL_NO_ERROR) {
p_glDeleteTextures(1, &tex);
return 0;
}
pool_images[slot].handle = FAKE_HANDLE(XrSwapchain);
pool_images[slot].tex = tex;
pool_images[slot].rect = rect;
pool_images[slot].atlas = a;
*out = pool_images[slot].handle;
note_swapchain_size(*out, ci->width, ci->height);
remember_swapchain(*out);
emul_on_create_swapchain(*out, ci);
LOG("swapchain_pool: virtual %ux%u (%s)", ci->width, ci->height,
limited ? "runtime limit reached" : "runtime limit proactively avoided");
return 1;
}
return 0;
}
static void pool_destroy_locked(int i) {
emul_on_destroy_swapchain(pool_images[i].handle);
if (pool_images[i].fence && p_eglGetCurrentContext && p_eglGetCurrentContext() != EGL_NO_CONTEXT)
p_glDeleteSync(pool_images[i].fence);
if (p_eglGetCurrentContext && p_eglGetCurrentContext() != EGL_NO_CONTEXT)
p_glDeleteTextures(1, &pool_images[i].tex);
if (pool_images[i].read_fence && p_eglGetCurrentContext && p_eglGetCurrentContext() != EGL_NO_CONTEXT)
p_glDeleteSync(pool_images[i].read_fence);
forget_swapchain(pool_images[i].handle);
memset(&pool_images[i], 0, sizeof pool_images[i]);
}
static XrResult pool_enumerate_locked(int i, uint32_t cap, uint32_t *count, XrSwapchainImageBaseHeader *images) {
if (!count) return XR_ERROR_VALIDATION_FAILURE;
*count = 1;
if (!cap) return XR_SUCCESS;
if (!images || images->type != XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_ES_KHR) return XR_ERROR_VALIDATION_FAILURE;
((emul_gles_image *)images)->image = pool_images[i].tex;
return XR_SUCCESS;
}
static XrResult pool_cycle_locked(int i, int op, uint32_t *index) {
int *state = &pool_images[i].state;
if (op == 0) {
if (!index) return XR_ERROR_VALIDATION_FAILURE;
if (*state) return XR_ERROR_CALL_ORDER_INVALID;
if (pool_images[i].read_fence) {
if (p_eglGetCurrentContext() == EGL_NO_CONTEXT) return XR_ERROR_RUNTIME_FAILURE;
p_glWaitSync(pool_images[i].read_fence, 0, GL_TIMEOUT_IGNORED);
}
*index = 0;
*state = 1;
}
if (op == 1) {
if (*state != 1) return XR_ERROR_CALL_ORDER_INVALID;
*state = 2;
}
if (op == 2) {
if (*state != 2) return XR_ERROR_CALL_ORDER_INVALID;
if (p_eglGetCurrentContext() == EGL_NO_CONTEXT) return XR_ERROR_RUNTIME_FAILURE;
if (pool_images[i].fence) p_glDeleteSync(pool_images[i].fence);
pool_images[i].fence = p_glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
if (!pool_images[i].fence) return XR_ERROR_RUNTIME_FAILURE;
p_glFlush();
*state = 0;
pool_images[i].released = 1;
}
return XR_SUCCESS;
}
static void pool_cleanup_locked(XrSession session) {
if (session != pool_session) return;
for (int i = 0; i < POOL_MAX; ++i)
if (pool_images[i].handle) pool_destroy_locked(i);
for (int a = 0; a < 2; ++a)
if (pool_atlas[a].handle) {
((PFN_xrDestroySwapchain)lookup(active_instance, "xrDestroySwapchain"))(pool_atlas[a].handle);
memset(&pool_atlas[a], 0, sizeof pool_atlas[a]);
}
memset(pool_runtime, 0, sizeof pool_runtime);
pool_runtime_count = 0;
pool_session = XR_NULL_HANDLE;
}
// Called on final layers, after strips, flips and rect_clamp. Copy every referenced image
// each frame: runtime atlas images rotate, and untouched areas are not preserved.
static void pool_begin_frame_locked(void) {
for (int i = 0; i < POOL_MAX; ++i) pool_images[i].copied = 0;
}
static int pool_subimage_locked(XrSwapchainSubImage *sub) {
int i = pool_find_locked(sub->swapchain);
if (i < 0) return 1;
int a = pool_images[i].atlas;
if (!pool_images[i].released || pool_images[i].state || sub->imageArrayIndex) return 0;
PFN_xrAcquireSwapchainImage acquire =
(PFN_xrAcquireSwapchainImage)lookup(active_instance, "xrAcquireSwapchainImage");
PFN_xrWaitSwapchainImage wait = (PFN_xrWaitSwapchainImage)lookup(active_instance, "xrWaitSwapchainImage");
if (!pool_atlas[a].acquired) {
XrSwapchainImageAcquireInfo ai = {XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO};
if (XR_FAILED(acquire(pool_atlas[a].handle, &ai, &pool_atlas[a].index))) return 0;
pool_atlas[a].acquired = 1;
}
if (!pool_atlas[a].waited) {
XrSwapchainImageWaitInfo wi = {XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO, NULL, XR_INFINITE_DURATION};
if (wait(pool_atlas[a].handle, &wi) != XR_SUCCESS) return 0;
pool_atlas[a].waited = 1;
}
if (p_eglGetCurrentContext() == EGL_NO_CONTEXT) return 0;
PoolRect r = pool_images[i].rect;
if (!pool_images[i].copied) {
for (int e = 0; e < 8 && p_glGetError() != GL_NO_ERROR; ++e) {
}
if (pool_images[i].fence) p_glWaitSync(pool_images[i].fence, 0, GL_TIMEOUT_IGNORED);
GLint read, draw;
GLuint fb[2];
GLboolean scissor = p_glIsEnabled(GL_SCISSOR_TEST);
p_glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &read);
p_glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &draw);
p_glGenFramebuffers(2, fb);
p_glDisable(GL_SCISSOR_TEST);
p_glBindFramebuffer(GL_READ_FRAMEBUFFER, fb[0]);
p_glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, pool_images[i].tex, 0);
p_glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fb[1]);
p_glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
pool_atlas[a].images[pool_atlas[a].index], 0);
int ok = p_glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE &&
p_glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
if (ok) p_glBlitFramebuffer(0, 0, r.w, r.h, r.x, r.y, r.x + r.w, r.y + r.h, GL_COLOR_BUFFER_BIT, GL_NEAREST);
ok &= p_glGetError() == GL_NO_ERROR;
p_glBindFramebuffer(GL_READ_FRAMEBUFFER, read);
p_glBindFramebuffer(GL_DRAW_FRAMEBUFFER, draw);
if (scissor) p_glEnable(GL_SCISSOR_TEST);
p_glDeleteFramebuffers(2, fb);
if (!ok) return 0;
if (pool_images[i].read_fence) p_glDeleteSync(pool_images[i].read_fence);
pool_images[i].read_fence = p_glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
if (!pool_images[i].read_fence) return 0;
pool_images[i].copied = 1;
}
clamp_rect(&sub->imageRect, r.w, r.h);
if (sub->imageRect.offset.x < 0 || sub->imageRect.offset.y < 0 ||
(int64_t)sub->imageRect.offset.x + sub->imageRect.extent.width > r.w ||
(int64_t)sub->imageRect.offset.y + sub->imageRect.extent.height > r.h)
return 0;
sub->swapchain = pool_atlas[a].handle;
sub->imageRect.offset.x += r.x;
sub->imageRect.offset.y += r.y;
return 1;
}
static int pool_finish_locked(void) {
int ok = 1;
for (int a = 0; a < 2; ++a)
if (pool_atlas[a].acquired && pool_atlas[a].waited) {
p_glFlush();
XrSwapchainImageReleaseInfo ri = {XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
XrResult r = ((PFN_xrReleaseSwapchainImage)lookup(active_instance, "xrReleaseSwapchainImage"))(
pool_atlas[a].handle, &ri);
if (XR_FAILED(r))
ok = 0;
else
pool_atlas[a].acquired = pool_atlas[a].waited = 0;
}
return ok;
}
// Each OpenXR call may run on a different thread, including creates of different panels.
static pthread_mutex_t pool_lock = PTHREAD_MUTEX_INITIALIZER;
static int pool_find(XrSwapchain h) {
pthread_mutex_lock(&pool_lock);
int result = pool_find_locked(h);
pthread_mutex_unlock(&pool_lock);
return result;
}
static int pool_create(XrSession session, const XrSwapchainCreateInfo *ci, XrSwapchain *out, int limited) {
pthread_mutex_lock(&pool_lock);
int result = pool_create_locked(session, ci, out, limited);
pthread_mutex_unlock(&pool_lock);
return result;
}
static void pool_destroy(int i) {
pthread_mutex_lock(&pool_lock);
pool_destroy_locked(i);
pthread_mutex_unlock(&pool_lock);
}
static void pool_cleanup(XrSession session) {
pthread_mutex_lock(&pool_lock);
pool_cleanup_locked(session);
pthread_mutex_unlock(&pool_lock);
}
static XrResult pool_cycle(int i, int op, uint32_t *index) {
pthread_mutex_lock(&pool_lock);
XrResult result = pool_cycle_locked(i, op, index);
pthread_mutex_unlock(&pool_lock);
return result;
}
static XrResult pool_enumerate(int i, uint32_t cap, uint32_t *count, XrSwapchainImageBaseHeader *images) {
pthread_mutex_lock(&pool_lock);
XrResult result = pool_enumerate_locked(i, cap, count, images);
pthread_mutex_unlock(&pool_lock);
return result;
}
static void pool_begin_frame(void) {
pthread_mutex_lock(&pool_lock);
pool_begin_frame_locked();
pthread_mutex_unlock(&pool_lock);
}
static int pool_subimage(XrSwapchainSubImage *sub) {
pthread_mutex_lock(&pool_lock);
int result = pool_subimage_locked(sub);
pthread_mutex_unlock(&pool_lock);
return result;
}
static int pool_finish(void) {
pthread_mutex_lock(&pool_lock);
int result = pool_finish_locked();
pthread_mutex_unlock(&pool_lock);
return result;
}
static void pool_note_runtime(XrSession session, XrSwapchain handle) {
pthread_mutex_lock(&pool_lock);
pool_note_runtime_locked(session, handle);
pthread_mutex_unlock(&pool_lock);
}
static void pool_forget_runtime(XrSwapchain handle) {
pthread_mutex_lock(&pool_lock);
pool_forget_runtime_locked(handle);
pthread_mutex_unlock(&pool_lock);
}
static int pool_should_create(XrSession session) {
pthread_mutex_lock(&pool_lock);
int result = pool_should_create_locked(session);
pthread_mutex_unlock(&pool_lock);
return result;
}
// Caller-owned storage stays live until the runtime's xrEndFrame returns. The input layer
// structs are const: never mutate them, including repeated references from strips or stereo.
typedef struct {
XrCompositionLayerQuad quads[64];
XrCompositionLayerCylinderKHR cylinders[64];
XrCompositionLayerProjection projections[64];
XrCompositionLayerProjectionView views[64][2];
} PoolFrame;
static uint32_t pool_rewrite_layers(const XrCompositionLayerBaseHeader **layers, uint32_t count, PoolFrame *frame) {
pool_begin_frame();
uint32_t n = 0;
for (uint32_t j = 0; j < count; ++j) {
const XrCompositionLayerBaseHeader *l = layers[j];
int ok = 1;
if (l->type == XR_TYPE_COMPOSITION_LAYER_QUAD) {
frame->quads[j] = *(const XrCompositionLayerQuad *)l;
ok = pool_subimage(&frame->quads[j].subImage);
l = (const void *)&frame->quads[j];
} else if (l->type == XR_TYPE_COMPOSITION_LAYER_CYLINDER_KHR) {
frame->cylinders[j] = *(const XrCompositionLayerCylinderKHR *)l;
ok = pool_subimage(&frame->cylinders[j].subImage);
l = (const void *)&frame->cylinders[j];
} else if (l->type == XR_TYPE_COMPOSITION_LAYER_PROJECTION) {
const XrCompositionLayerProjection *p = (const void *)l;
if (p->viewCount <= 2) {
frame->projections[j] = *p;
for (uint32_t v = 0; v < p->viewCount; ++v) {
frame->views[j][v] = p->views[v];
ok &= pool_subimage(&frame->views[j][v].subImage);
}
frame->projections[j].views = frame->views[j];
l = (const void *)&frame->projections[j];
}
}
if (ok)
layers[n++] = l;
else
pool_error();
}
if (!pool_finish()) {
pool_error();
return 0;
}
return n;
}
+6 -15
View File
@@ -10,7 +10,7 @@ timefix OpenXR layer for Proton games (linux-arm64, glibc; the NDK's clang build
OculusHMDConnected helper for Rift games under Proton (win-x64 PE; the NDK's clang + lld-link, no Windows SDK), the
live view's hardware H.264 encoder fp_venc (linux-arm64-bin, static freestanding executable), and rewrites artifacts/SHA256SUMS. Run `frameport parity` afterwards to see which games change.
python native/build.py [--only adapter,bridge,compat,langpack,glshim,eglfmt,glmv,ovrtrace,dex,xrlayer,oculushmd,xrruntime,xrshim,vkshim,zinkfix,foveation,ovrpshim,tlskeys,vrsettings,venc,vroverlay_probe] [--ndk PATH]
python native/build.py [--only adapter,bridge,compat,langpack,glshim,eglfmt,glmv,ovrtrace,dex,xrlayer,oculushmd,xrshim,vkshim,zinkfix,foveation,ovrpshim,tlskeys,vrsettings,venc,vroverlay_probe] [--ndk PATH]
"""
from __future__ import annotations
@@ -209,8 +209,8 @@ def build_roomscan(tc: Path):
run([tc / "bin/clang", "--target=aarch64-linux-gnu", "-ffreestanding", "-nostdlib", "-nostdlibinc", "-static",
"-fno-stack-protector", "-fno-builtin", "-ffp-contract=off", "-fno-math-errno", "-O2", "-Wall", "-Wextra", "-Werror",
"-fuse-ld=lld", "-Wl,--build-id=none", "-Wl,-z,max-page-size=65536", "-e", "_start",
f"-ffile-prefix-map={HERE}=native", "main.c", "fusion.c", "model.c", "mesh.c", "refine.c", "rules.c",
"roomio.c", "platform.c", "-o", out / "fp_roomscan"], cwd=src)
f"-ffile-prefix-map={HERE}=native", "main.c", "fusion.c", "model.c", "mesh.c", "platform.c",
"-o", out / "fp_roomscan"], cwd=src)
OCULUSHMD_IMPORTS = ("CreateEventW", "CreateJobObjectW", "AssignProcessToJobObject", "QueryInformationJobObject",
@@ -343,15 +343,6 @@ def build_ovrtrace(tc: Path):
"-o", ART / "arm64-v8a/libfp_ovrtrace.so"], cwd=src)
def build_xrruntime(tc: Path):
src = HERE / "xrruntime"
run([exe(tc, "aarch64-linux-android29-clang"), "-shared", "-O2", "-fPIC", "-Wall", "-Wextra", "-Werror",
f"-ffile-prefix-map={ROOT}=.", "-fvisibility=hidden", "-Wl,-Bsymbolic",
"-I", openxr_include("adapter"), "-Wl,-soname,libfp_xrruntime.so",
"-Wl,-z,max-page-size=16384", "xrruntime.c", "-ldl", "-llog",
"-o", ART / "arm64-v8a/libfp_xrruntime.so"], cwd=src)
def build_xrshim(tc: Path):
"""FrameBridge extension shim (see xrshim/xrshim.c): DT_NEEDED-injected in front of overport's libopenxr_loader.so."""
src = HERE / "xrshim"
@@ -461,7 +452,7 @@ def write_sums():
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--only", default="adapter,bridge,compat,langpack,glshim,eglfmt,glmv,ovrtrace,dex,xrlayer,oculushmd,xrruntime,xrshim,vkshim,zinkfix,foveation,ovrpshim,tlskeys,luajit,vrsettings,venc,roomscan,a32")
ap.add_argument("--only", default="adapter,bridge,compat,langpack,glshim,eglfmt,glmv,ovrtrace,dex,xrlayer,oculushmd,xrshim,vkshim,zinkfix,foveation,ovrpshim,tlskeys,luajit,vrsettings,venc,roomscan,a32")
ap.add_argument("--ndk")
args = ap.parse_args()
parts = set(args.only.split(","))
@@ -469,7 +460,7 @@ def main():
(ART / d).mkdir(parents=True, exist_ok=True)
tc = clang_dir(ndk(args.ndk)) if parts - {"dex"} else None
steps = {"adapter": lambda: build_adapter(tc), "bridge": lambda: build_bridge(tc), "compat": lambda: build_compat(tc),
"xrruntime": lambda: build_xrruntime(tc), "langpack": lambda: build_langpack(tc),
"langpack": lambda: build_langpack(tc),
"glshim": lambda: build_glshim(tc), "eglfmt": lambda: build_eglfmt(tc), "glmv": lambda: build_glmv(tc), "ovrtrace": lambda: build_ovrtrace(tc), "xrshim": lambda: build_xrshim(tc), "dex": build_dex, "xrlayer": lambda: build_xrlayer(tc),
"oculushmd": lambda: build_oculushmd(tc), "vkshim": lambda: build_vkshim(tc),
"zinkfix": lambda: build_zinkfix(tc),
@@ -479,7 +470,7 @@ def main():
"vrsettings": lambda: build_vrsettings(tc), "venc": lambda: build_venc(tc),
"roomscan": lambda: build_roomscan(tc),
"vroverlay_probe": lambda: build_vroverlay_probe(tc), "a32": lambda: build_a32(tc)}
for name in ("adapter", "bridge", "compat", "langpack", "glshim", "eglfmt", "glmv", "ovrtrace", "xrruntime", "xrshim", "vkshim", "zinkfix", "foveation", "ovrpshim", "tlskeys", "luajit", "dex", "xrlayer", "oculushmd", "vrsettings", "venc",
for name in ("adapter", "bridge", "compat", "langpack", "glshim", "eglfmt", "glmv", "ovrtrace", "xrshim", "vkshim", "zinkfix", "foveation", "ovrpshim", "tlskeys", "luajit", "dex", "xrlayer", "oculushmd", "vrsettings", "venc",
"roomscan", "vroverlay_probe", "a32"):
if name in parts:
log(f"build {name}")
-14
View File
@@ -52,19 +52,6 @@ void warn(Scan *s, const char *m)
for (int i = 0; i < s->nwarnings; i++) if (same(s->warnings[i],m)) return;
if (s->nwarnings < 24) s->warnings[s->nwarnings++] = m;
}
/* The head path (where the user stood and walked) for the refinement's walked-volume pass. Bounded: every
* head_stride-th tracked frame, halving the kept points and doubling the stride when the buffer is full. */
static void record_head(Scan *s, Vec eye)
{
if(!s->head_stride) s->head_stride=1;
if(s->head_frames++%s->head_stride) return;
if(s->nheads==MAX_HEADS) {
for(int i=0;i<MAX_HEADS/2;i++) s->heads[i]=s->heads[2*i];
s->nheads=MAX_HEADS/2;s->head_stride*=2;
if((s->head_frames-1)%s->head_stride) return;
}
s->heads[s->nheads++]=eye;
}
static u32 u32le(const u8 *p) { return (u32)p[0]|(u32)p[1]<<8|(u32)p[2]<<16|(u32)p[3]<<24; }
static float f32le(const u8 *p) { union { u32 u; float f; } v = {u32le(p)}; return v.f; }
/* 1 = complete, 0 = clean EOF, -1 = I/O error or partial record. */
@@ -220,7 +207,6 @@ const char *read_dump(Scan *s, int fuse)
for (int i = 3; i < 7; i++) norm += pose[i]*pose[i];
if (!finite(norm) || ab(norm-1) > QUATERNION_NORM_TOLERANCE) invalid=1;
if(!invalid) for (int i = 3; i < 7; i++) pose[i] /= root(norm);
if(!fuse && !invalid) record_head(s,vec(pose[0],pose[1],pose[2]));
u32 nv = u32le(header+36), ni = u32le(header+40);
if (nv > MAX_FRAME_VERTICES || ni > MAX_FRAME_INDICES || ni%3 || (!nv && ni)) {
error = "invalid frame counts"; break;
+129 -297
View File
@@ -1,10 +1,110 @@
/* SPDX-License-Identifier: GPL-3.0-only */
#include "refine.h"
#include "scan.h"
typedef struct { char data[65536]; usize n; int ok; } Text;
static void text(Text *t, const char *p)
{
while(*p) {
if(t->n>=sizeof(t->data)) { t->ok=0;return; }
t->data[t->n++]=*p++;
}
}
static void integer(Text *t, u64 value)
{
char rev[24];int n=0;
do { rev[n++]=(char)('0'+value%10);value/=10; } while(value);
while(n) { char a[2]={rev[--n],0};text(t,a); }
}
static void decimal(Text *t, float value)
{
if(value<0) { text(t,"-");value=-value; }
u64 scaled=(u64)(value*10000.f+.5f);
integer(t,scaled/10000);text(t,".");
u64 fraction=scaled%10000;
if(fraction<1000) text(t,"0");
if(fraction<100) text(t,"0");
if(fraction<10) text(t,"0");
integer(t,fraction);
}
static void space_number(Text *t, float n) { text(t," ");decimal(t,n); }
static int uuid(Text *t)
{
u8 bytes[16];if(!io_random(bytes,sizeof(bytes))) return 0;
bytes[6]=(bytes[6]&15)|64;bytes[8]=(bytes[8]&63)|128;
const char *hex="0123456789abcdef";
for(int i=0;i<16;i++) { char pair[3]={hex[bytes[i]>>4],hex[bytes[i]&15],0};text(t,pair); }
return 1;
}
static int record(Text *t, const char *name)
{
text(t,name);text(t," ");return uuid(t);
}
static int path(char *out, const char *dir, const char *name)
{
usize n=0;
while(*dir) { if(n>=3900) return 0;out[n++]=*dir++; }
if(n && out[n-1]!='/') out[n++]='/';
while(*name) { if(n>=4095) return 0;out[n++]=*name++; }
out[n]=0;return 1;
}
static void le32(u8 *out,u32 v) { for(int i=0;i<4;i++) out[i]=(u8)(v>>(8*i)); }
static const char *write_files(Scan *s)
{
Text t={.ok=1};text(&t,"frameport-room 1\nuniverse ");integer(&t,s->opt.universe);
text(&t,"\nsource scan\ntime ");integer(&t,(u64)(io_time(0)/1000000000));text(&t,"\n");
if(!record(&t,"room")) return "getrandom failed";
text(&t,"\n");if(!record(&t,"floor")) return "getrandom failed";
space_number(&t,s->floor);text(&t,"\n");if(!record(&t,"ceiling")) return "getrandom failed";
space_number(&t,s->ceiling);text(&t,"\noutline ");integer(&t,(u64)s->noutline);
for(int i=0;i<s->noutline;i++) { space_number(&t,s->outline[i].x);space_number(&t,s->outline[i].z); }
text(&t,"\n");
for(int i=0;i<s->noutline;i++) {
Pt a=s->outline[i],b=s->outline[(i+1)%s->noutline];
if(!record(&t,"wall")) return "getrandom failed";
space_number(&t,a.x);space_number(&t,a.z);space_number(&t,b.x);space_number(&t,b.z);
space_number(&t,s->floor);space_number(&t,s->ceiling);
if(s->invisible[i]) text(&t," INVISIBLE_WALL_FACE");
text(&t,"\n");
}
for(int i=0;i<s->nvolumes;i++) {
Volume *v=&s->volumes[i];
if(!record(&t,"volume")) return "getrandom failed";
text(&t," ");text(&t,v->label);space_number(&t,v->p.x);space_number(&t,v->p.z);
space_number(&t,v->p.y);space_number(&t,v->yaw);space_number(&t,v->w);space_number(&t,v->d);space_number(&t,v->h);
text(&t,"\n");
}
if(!record(&t,"mesh")) return "getrandom failed";
text(&t," room-mesh.bin ");integer(&t,(u64)s->nv);text(&t," ");integer(&t,(u64)s->ni);text(&t,"\n");
if(!t.ok) return "room text exceeds output budget";
Text rname={.ok=1},mname={.ok=1};
text(&rname,"room.txt.");text(&mname,"room-mesh.bin.");
if(!uuid(&rname) || !uuid(&mname)) return "getrandom failed";
text(&rname,".tmp");text(&mname,".tmp");rname.data[rname.n]=0;mname.data[mname.n]=0;
char room[4096],mesh[4096],rtmp[4096],mtmp[4096];
if(!path(room,s->opt.out,"room.txt") || !path(mesh,s->opt.out,"room-mesh.bin") ||
!path(rtmp,s->opt.out,rname.data) || !path(mtmp,s->opt.out,mname.data)) return "output path too long";
if(!io_mkdir(s->opt.out)) return "cannot create output directory";
int fd=io_open(rtmp,1),ok;
if(fd<0) return "cannot open room temporary file";
ok=io_write(fd,t.data,t.n);if(io_close(fd)) ok=0;
if(!ok) { io_remove(rtmp);return "cannot write room temporary file"; }
fd=io_open(mtmp,1);
if(fd<0) { io_remove(rtmp);return "cannot open mesh temporary file"; }
u8 header[16]={'F','P','R','M'};le32(header+4,1);le32(header+8,(u32)s->nv);le32(header+12,(u32)s->ni);
ok=io_write(fd,header,16);
/* Both supported targets are little-endian IEEE float32; parser always decodes explicitly. */
ok=ok && io_write(fd,s->vertices,(usize)s->nv*sizeof(Vec));
ok=ok && io_write(fd,s->indices,(usize)s->ni*sizeof(u32));
if(io_close(fd)) ok=0;
if(!ok) { io_remove(rtmp);io_remove(mtmp);return "cannot write mesh temporary file"; }
if(io_rename(mtmp,mesh)) { io_remove(rtmp);io_remove(mtmp);return "cannot publish mesh"; }
if(io_rename(rtmp,room)) { io_remove(rtmp);return "cannot publish room"; }
return 0;
}
static int error_json(const char *error)
{
Buf t;buf_init(&t,4096);put(&t,"{\"ok\":false,\"error\":\"");put(&t,error);put(&t,"\"}\n");
io_write(1,t.data,t.n);buf_free(&t);return 2;
Text t={.ok=1};text(&t,"{\"ok\":false,\"error\":\"");text(&t,error);text(&t,"\"}\n");
io_write(1,t.data,t.n);return 2;
}
static int parse_float(const char *p,float *out)
{
@@ -25,60 +125,21 @@ static int parse_universe(const char *p,u64 *out)
}
*out=n;return 1;
}
/* "x,y,z,r;x,y,z,r;..." (metres, STAGE space) */
static int parse_erase(const char *value, Refine *o)
{
char *p=(char *)value;o->nerase=0;
while(*p) {
if(o->nerase==MAX_ERASE) return 0;
float v[4];
for(int i=0;i<4;i++) {
if(!parse_number(&p,&v[i])) return 0;
if(i<3) { if(*p!=',') return 0;p++; }
}
if(v[3]<=0 || v[3]>5) return 0;
o->erase[o->nerase++]=(Sphere){v[0],v[1],v[2],v[3]};
while(*p==' ') p++;
if(*p==';') p++;
else if(*p) return 0;
}
return 1;
}
static const char *options(Options *o,int argc,char **argv)
{
*o=(Options){.voxel=.05f,.ceiling_default=2.5f,.truncation=3,.max_edge=.6f,.grazing=.12f,.range=4.f,
.depth_jump=.25f,.depth_ratio=.12f,.min_views=5,.patch_residual=.03f,
.plane_distance=.045f,.min_plane_area=.45f,.wall_height=1.5f,.snap_degrees=5,.floor_band=.35f,
.ceiling_min=1.8f,.min_volume=.05f,.cluster_gap=.10f,.gap=.65f,.horizontal=.85f,
/* The owner's refinement settings (room-scan cleanup page, 2026-10-18). */
.rf={.refine=1,.walls=2,.protect=0,.label_rules=1,.smooth=8,.band=.35f,.facing_deg=60,.band_in=.12f,
.floor_in=.08f,.ceil_in=.25f,.top_strip=.5f,.near=.03f,.clip=.05f,.clip_open=.10f,.island=.20f,
.tops=.10f,.tubes=3,.tube_thick=.15f,.walked=.40f}};
.ceiling_min=1.8f,.min_volume=.05f,.cluster_gap=.10f,.gap=.65f,.horizontal=.85f};
for(int i=1;i<argc;i++) {
const char *arg=argv[i];
if(same(arg,"--json")) continue;
if(i+1==argc) return "option requires a value";
const char *value=argv[++i];
float number=0;
if(same(arg,"--offline")) o->input=value;
else if(same(arg,"--out")) o->out=value;
else if(same(arg,"--hint")) o->hint=value;
else if(same(arg,"--refine-only")) o->refine_only=value;
else if(same(arg,"--universe")) { if(!parse_universe(value,&o->universe)) return "invalid universe"; }
else if(same(arg,"--walls")) {
if(same(value,"off")) o->rf.walls=0;
else if(same(value,"snap")) o->rf.walls=1;
else if(same(value,"panels")) o->rf.walls=2;
else return "option outside supported range";
}
else if(same(arg,"--erase")) { if(!parse_erase(value,&o->rf)) return "invalid erase spheres"; }
else if(same(arg,"--refine") || same(arg,"--protect") || same(arg,"--label-rules") || same(arg,"--smooth")) {
int high=same(arg,"--smooth") ? 50 : 1;
if(!parse_float(value,&number) || number!=(float)(int)number || number>high) return "option outside supported range";
int *dest=same(arg,"--refine") ? &o->rf.refine : same(arg,"--protect") ? &o->rf.protect :
same(arg,"--label-rules") ? &o->rf.label_rules : &o->rf.smooth;
*dest=(int)number;
}
else {
float *dest=0,low=0,high=100;
if(same(arg,"--voxel")) { dest=&o->voxel;low=.04f;high=.2f; }
@@ -101,265 +162,44 @@ static const char *options(Options *o,int argc,char **argv)
else if(same(arg,"--cluster-gap")) { dest=&o->cluster_gap;low=.04f;high=.2f; }
else if(same(arg,"--gap")) { dest=&o->gap;low=.1f;high=1.5f; }
else if(same(arg,"--horizontal")) { dest=&o->horizontal;low=.6f;high=.99f; }
else if(same(arg,"--band")) { dest=&o->rf.band;low=.02f;high=1; }
else if(same(arg,"--facing-deg")) { dest=&o->rf.facing_deg;low=5;high=89; }
else if(same(arg,"--band-in")) { dest=&o->rf.band_in;high=1; }
else if(same(arg,"--floor-in")) { dest=&o->rf.floor_in;high=1; }
else if(same(arg,"--ceil-in")) { dest=&o->rf.ceil_in;high=1; }
else if(same(arg,"--top-strip")) { dest=&o->rf.top_strip;high=2; }
else if(same(arg,"--near")) { dest=&o->rf.near;high=.3f; }
else if(same(arg,"--clip")) { dest=&o->rf.clip;high=1; }
else if(same(arg,"--clip-open")) { dest=&o->rf.clip_open;high=2; }
else if(same(arg,"--island")) { dest=&o->rf.island;high=5; }
else if(same(arg,"--tops")) { dest=&o->rf.tops;high=.5f; }
else if(same(arg,"--tubes")) { dest=&o->rf.tubes;high=20; }
else if(same(arg,"--tube-thick")) { dest=&o->rf.tube_thick;low=.01f;high=1; }
else if(same(arg,"--walked")) { dest=&o->rf.walked;high=1.5f; }
else return "unknown option";
if(!parse_float(value,dest) || *dest<low || *dest>high) return "option outside supported range";
}
}
if(o->rf.walked>0 && o->rf.walked<.05f) return "option outside supported range";
if(o->refine_only) return o->input ? "--refine-only takes no --offline dump" : 0;
return o->input && o->out ? 0 : "required: --offline dump.fprs --out directory";
}
/* ------------------------------------------------------------------------------------------------ refinement */
typedef struct {
Report rep;
Label labels[MAX_VOLUMES];
int nlabels, changed, removed, nsuggest, out_triangles, out_volumes;
float ceiling_y;
i64 ms;
} Outcome;
static const char *copy_name(char *dst, const char *src, usize cap)
static void duration(Text *t,i64 a,i64 b) { decimal(t,(float)(b-a)/1000000); }
static void summary(Scan *s)
{
usize i=0;while(src[i] && i+1<cap) { dst[i]=src[i];i++; }
dst[i]=0;return dst;
Text t={.ok=1};text(&t,"{\"ok\":true,\"frames\":");integer(&t,s->frames);
text(&t,",\"skipped_frames\":");integer(&t,s->skipped_frames);
text(&t,",\"far_vertices\":");integer(&t,s->far_vertices);
text(&t,",\"discontinuity_triangles\":");integer(&t,s->discontinuities);
text(&t,",\"walls\":");integer(&t,(u64)s->noutline);
text(&t,",\"detected_walls\":");integer(&t,(u64)s->nplanes);
text(&t,",\"volumes\":");integer(&t,(u64)s->nvolumes);
text(&t,",\"floor_y\":");decimal(&t,s->floor);text(&t,",\"ceiling_y\":");decimal(&t,s->ceiling);
text(&t,",\"coverage_percent\":");decimal(&t,s->coverage);
text(&t,",\"hint_used\":");text(&t,s->hint_used ? "true" : "false");
text(&t,",\"triangles\":");integer(&t,(u64)s->ni/3);
text(&t,",\"accepted_triangles\":");integer(&t,s->accepted);
text(&t,",\"rejected_triangles\":");integer(&t,s->rejected);
text(&t,",\"warnings\":[");
for(int i=0;i<s->nwarnings;i++) { if(i) text(&t,",");text(&t,"\"");text(&t,s->warnings[i]);text(&t,"\""); }
text(&t,"],\"timings_ms\":{\"read\":");duration(&t,s->t0,s->tread);
text(&t,",\"fusion\":");duration(&t,s->tread,s->tfuse);
text(&t,",\"model\":");duration(&t,s->tfuse,s->tmodel);
text(&t,",\"mesh\":");duration(&t,s->tmodel,s->tmesh);
text(&t,",\"total\":");duration(&t,s->t0,io_time(1));text(&t,"}}\n");io_write(1,t.data,t.n);
}
static void box_json(Buf *b, const RVol *v)
{
put(b,"{\"cx\":");put_dec(b,v->cx);put(b,",\"cz\":");put_dec(b,v->cz);put(b,",\"top_y\":");put_dec(b,v->top);
put(b,",\"yaw_deg\":");put_dec(b,v->yaw);put(b,",\"w\":");put_dec(b,v->w);put(b,",\"d\":");put_dec(b,v->d);
put(b,",\"h\":");put_dec(b,v->h);put(b,"}");
}
static void scan_json(Buf *b, const Refine *o, const Vec *heads, int nheads, const Outcome *r)
{
put(b,"{\"version\":1,\"heads\":");heads_json(b,heads,nheads);
put(b,",\"refine\":");settings_json(b,o);
put(b,",\"report\":");report_json(b,&r->rep,r->nlabels,r->changed,r->removed,r->nsuggest);
put(b,",\"labels\":[");
for(int i=0;i<r->nlabels;i++) {
const Label *l=&r->labels[i];
if(i) put(b,",");
put(b,"{\"from\":\"");put(b,l->from);put(b,"\",\"to\":");
if(l->to[0]) { put(b,"\"");put(b,l->to);put(b,"\""); } else put(b,"null");
put(b,",\"reason\":\"");put(b,l->reason);put(b,"\"");
if(l->suggest[0]) { put(b,",\"suggest\":\"");put(b,l->suggest);put(b,"\""); }
put(b,",\"id\":\"");put(b,l->box.id);put(b,"\",\"box\":");box_json(b,&l->box);put(b,"}");
}
put(b,"],\"suggestions\":[");
int first=1;
for(int i=0;i<r->nlabels;i++) {
const Label *l=&r->labels[i];
if(!l->suggest[0]) continue;
if(!first) put(b,",");
first=0;
put(b,"{\"kind\":\"");put(b,l->suggest);put(b,"\",\"box\":");box_json(b,&l->box);
put(b,",\"wall\":");put_int(b,l->wall);put(b,",\"along\":");put_dec(b,l->along);
put(b,",\"reason\":\"");put(b,l->reason);put(b,"\"}");
}
put(b,"]}\n");
}
/* Refines `raw` (as read back from its own text) and publishes: [raw mesh, raw text,] scan.json, room-mesh.bin,
* room.txt. Everything is computed before the first file is written, so a failure leaves the old files. */
static const char *refine_publish(const char *dir, const Room *raw, const Vec *v, const u32 *idx, int nv, int ni,
const Vec *heads, int nheads, const Refine *o, int write_raw, Outcome *res)
{
static Room refined;
static RVol ruled[MAX_VOLUMES];
const char *error=0;
Prep prep={0};
Vec *rv=0;u32 *ri=0;int rn=0;
u8 *raw_mesh=0,*mesh=0;usize raw_size=0,mesh_size=0;
Buf raw_text,text,json;
buf_init(&raw_text,1u<<20);buf_init(&text,1u<<20);buf_init(&json,MAX_JSON_FILE);
i64 t0=io_time(1);
memset(res,0,sizeof(*res));
refined=*raw;
copy_name(refined.mesh_name,"room-mesh.bin",sizeof(refined.mesh_name));
if(o->refine) {
error=prep_mesh(&prep,v,idx,nv,ni);
int nruled=raw->nvols;
if(!error) {
if(o->label_rules) res->nlabels=rules_apply(raw,&prep,ruled,&nruled,res->labels);
else memcpy(ruled,raw->vols,sizeof(RVol)*(usize)raw->nvols);
}
if(!error) error=refine_run(o,raw,&prep,ruled,nruled,heads,nheads,&rv,&ri,&rn,&res->rep);
if(!error) {
refined.ceiling=res->rep.ceiling_y;
for(int i=0;i<refined.nwalls;i++) if(refined.walls[i].y1==raw->ceiling) refined.walls[i].y1=refined.ceiling;
memcpy(refined.vols,ruled,sizeof(RVol)*(usize)nruled);refined.nvols=nruled;
refined.mesh_nv=refined.mesh_ni=(u32)rn;refined.has_mesh=1;
for(int i=0;i<res->nlabels;i++) {
Label *l=&res->labels[i];
if(l->suggest[0]) res->nsuggest++;
if(!l->to[0]) res->removed++;
else if(!same(l->to,l->from)) res->changed++;
}
}
}
if(!error) error=mesh_bytes(rv ? rv : v,rv ? ri : idx,rv ? rn : nv,rv ? rn : ni,&mesh,&mesh_size);
if(!error && write_raw) error=mesh_bytes(v,idx,nv,ni,&raw_mesh,&raw_size);
if(!error && write_raw && !room_text(raw,&raw_text)) error="room text exceeds output budget";
if(!error && !room_text(&refined,&text)) error="room text exceeds output budget";
if(!error) { scan_json(&json,o,heads,nheads,res);if(!json.ok) error="scan.json exceeds output budget"; }
res->ms=io_time(1)-t0;
res->out_triangles=(int)refined.mesh_ni/3;res->out_volumes=refined.nvols;res->ceiling_y=refined.ceiling;
if(!error && !io_mkdir(dir)) error="cannot create output directory";
if(!error && write_raw) error=publish(dir,"room-mesh-raw.bin",raw_mesh,raw_size);
if(!error && write_raw) error=publish(dir,"room-raw.txt",raw_text.data,raw_text.n);
if(!error) error=publish(dir,"scan.json",json.data,json.n);
if(!error) error=publish(dir,"room-mesh.bin",mesh,mesh_size);
if(!error) error=publish(dir,"room.txt",text.data,text.n);
prep_free(&prep);
io_free(rv,(usize)MAX_MESH_VERTICES*sizeof(Vec));io_free(ri,(usize)MAX_TRIANGLES*3*sizeof(u32));
io_free(raw_mesh,raw_size);io_free(mesh,mesh_size);
buf_free(&raw_text);buf_free(&text);buf_free(&json);
return error;
}
static void refine_summary(Buf *t, const Outcome *r)
{
put(t,"\"refine\":");report_json(t,&r->rep,r->nlabels,r->changed,r->removed,r->nsuggest);
if(t->ok && t->n) t->n--; /* reopen the object for the output counts */
put(t,",\"output_triangles\":");put_int(t,r->out_triangles);
put(t,",\"output_volumes\":");put_int(t,r->out_volumes);put(t,"}");
}
/* ------------------------------------------------------------------------------------------------ summaries */
static void duration(Buf *t,i64 a,i64 b) { put_dec(t,(float)(b-a)/1000000); }
static void warnings_json(Buf *t, const char **w, int n)
{
put(t,",\"warnings\":[");
for(int i=0;i<n;i++) { if(i) put(t,",");put(t,"\"");put(t,w[i]);put(t,"\""); }
put(t,"]");
}
static void summary(Scan *s, const Outcome *r)
{
Buf t;buf_init(&t,1u<<16);put(&t,"{\"ok\":true,\"frames\":");put_u64(&t,s->frames);
put(&t,",\"skipped_frames\":");put_u64(&t,s->skipped_frames);
put(&t,",\"far_vertices\":");put_u64(&t,s->far_vertices);
put(&t,",\"discontinuity_triangles\":");put_u64(&t,s->discontinuities);
put(&t,",\"walls\":");put_u64(&t,(u64)s->noutline);
put(&t,",\"detected_walls\":");put_u64(&t,(u64)s->nplanes);
put(&t,",\"volumes\":");put_u64(&t,(u64)s->nvolumes);
put(&t,",\"floor_y\":");put_dec(&t,s->floor);put(&t,",\"ceiling_y\":");put_dec(&t,s->ceiling);
put(&t,",\"coverage_percent\":");put_dec(&t,s->coverage);
put(&t,",\"hint_used\":");put(&t,s->hint_used ? "true" : "false");
put(&t,",\"triangles\":");put_u64(&t,(u64)s->ni/3);
put(&t,",\"accepted_triangles\":");put_u64(&t,s->accepted);
put(&t,",\"rejected_triangles\":");put_u64(&t,s->rejected);
put(&t,",\"heads\":");put_int(&t,s->nheads);put(&t,",");
refine_summary(&t,r);
warnings_json(&t,s->warnings,s->nwarnings);
put(&t,",\"timings_ms\":{\"read\":");duration(&t,s->t0,s->tread);
put(&t,",\"fusion\":");duration(&t,s->tread,s->tfuse);
put(&t,",\"model\":");duration(&t,s->tfuse,s->tmodel);
put(&t,",\"mesh\":");duration(&t,s->tmodel,s->tmesh);
put(&t,",\"refine\":");duration(&t,s->tmesh,s->trefine);
put(&t,",\"total\":");duration(&t,s->t0,io_time(1));put(&t,"}}\n");io_write(1,t.data,t.n);buf_free(&t);
}
/* ------------------------------------------------------------------------------------------------ scan path */
static const char *scan_outputs(Scan *s, Outcome *out)
{
static Room built,raw;
static Vec heads[MAX_HEADS];
const char *error=room_from_scan(s,&built);
if(error) return error;
copy_name(built.mesh_name,"room-mesh-raw.bin",sizeof(built.mesh_name));
/* Refine what the raw files will hold, so --refine-only on them gives the same refined files. */
Buf t;buf_init(&t,1u<<20);
if(!room_text(&built,&t)) { buf_free(&t);return "room text exceeds output budget"; }
error=room_parse(t.data,t.n,&raw);
buf_free(&t);
if(error) return error;
Buf h;buf_init(&h,1u<<20);heads_json(&h,s->heads,s->nheads);put(&h,"}");
Buf wrap;buf_init(&wrap,1u<<20);put(&wrap,"{\"heads\":");put_n(&wrap,h.data,h.n);
int nheads=wrap.ok ? heads_parse(wrap.data,wrap.n,heads) : 0;
buf_free(&h);buf_free(&wrap);
return refine_publish(s->opt.out,&raw,s->vertices,s->indices,s->nv,s->ni,heads,nheads,&s->opt.rf,1,out);
}
/* ------------------------------------------------------------------------------------------------ --refine-only */
static int refine_only(Options *o)
{
static Room raw;
static Vec heads[MAX_HEADS];
static Outcome res;
const char *warns[4];int nwarn=0;
i64 t0=io_time(1);
const char *dir=o->refine_only,*error=0;
char path[4096];
u8 *text=0,*bin=0,*json=0;usize text_n=0,bin_n=0,json_n=0;
Vec *v=0;u32 *idx=0;int nv=0,ni=0,nheads=0,write_raw=0;
if(!join_path(path,dir,"room-raw.txt")) return error_json("output path too long");
error=read_file(path,1u<<20,&text,&text_n);
if(error) {
/* An older room folder: its room.txt + mesh are the unrefined scan. */
write_raw=1;
if(!join_path(path,dir,"room.txt")) return error_json("output path too long");
error=read_file(path,1u<<20,&text,&text_n);
if(error) return error_json("room folder has neither room-raw.txt nor room.txt");
}
error=room_parse((const char *)text,text_n,&raw);
io_free(text,(1u<<20)+1);
if(error) return error_json(error);
if(!raw.has_mesh) return error_json("room has no mesh to refine");
if(!join_path(path,dir,write_raw ? raw.mesh_name : "room-mesh-raw.bin")) return error_json("output path too long");
error=read_file(path,MAX_RAW_FILE,&bin,&bin_n);
if(!error) error=mesh_parse(bin,bin_n,&v,&idx,&nv,&ni);
io_free(bin,MAX_RAW_FILE+1);
if(error) return error_json(error);
if((u32)nv!=raw.mesh_nv || (u32)ni!=raw.mesh_ni) {
io_free(v,(usize)MAX_MESH_VERTICES*sizeof(Vec));io_free(idx,(usize)MAX_TRIANGLES*3*sizeof(u32));
return error_json("mesh record does not match the mesh file");
}
copy_name(raw.mesh_name,"room-mesh-raw.bin",sizeof(raw.mesh_name));
if(join_path(path,dir,"scan.json") && !read_file(path,MAX_JSON_FILE,&json,&json_n)) {
nheads=heads_parse((const char *)json,json_n,heads);
io_free(json,MAX_JSON_FILE+1);
}
if(!nheads && o->rf.walked>0) warns[nwarn++]="no head path; walked volume skipped";
if(write_raw) warns[nwarn++]="no raw files; the current room was taken as the unrefined scan";
error=refine_publish(dir,&raw,v,idx,nv,ni,heads,nheads,&o->rf,write_raw,&res);
io_free(v,(usize)MAX_MESH_VERTICES*sizeof(Vec));io_free(idx,(usize)MAX_TRIANGLES*3*sizeof(u32));
if(error) return error_json(error);
if(res.rep.truncated) warns[nwarn++]="refined mesh truncated to the triangle budget";
Buf t;buf_init(&t,1u<<16);
put(&t,"{\"ok\":true,\"refine_only\":true,\"raw_triangles\":");put_int(&t,ni/3);
put(&t,",\"volumes\":");put_int(&t,raw.nvols);put(&t,",\"floor_y\":");put_dec(&t,raw.floor);
put(&t,",\"ceiling_y\":");put_dec(&t,res.ceiling_y);put(&t,",\"heads\":");put_int(&t,nheads);put(&t,",");
refine_summary(&t,&res);
warnings_json(&t,warns,nwarn);
put(&t,",\"timings_ms\":{\"refine\":");put_dec(&t,(float)res.ms/1000000);
put(&t,",\"total\":");duration(&t,t0,io_time(1));put(&t,"}}\n");
io_write(1,t.data,t.n);buf_free(&t);
return 0;
}
int main(int argc,char **argv)
{
if(argc==2 && same(argv[1],"--help")) {
const char usage[]="fp_roomscan --offline dump.fprs --out directory [--universe N] [--voxel 0.05]\n"
" [--ceiling-default 2.5] [--hint outline.txt] [--json]\n"
"fp_roomscan --refine-only room-directory [refinement options] [--json]\n"
"Tuning: --truncation --max-edge --grazing --range --depth-jump --depth-ratio --plane-distance --min-plane-area\n"
" --min-views --patch-residual --wall-height --snap-degrees --floor-band --ceiling-min\n"
" --min-volume --cluster-gap --gap --horizontal\n"
"Refinement: --refine 0|1 --walls off|snap|panels --band --facing-deg --band-in --floor-in --ceil-in\n"
" --top-strip --near --clip --clip-open --island --smooth --tops --protect 0|1 --tubes --tube-thick\n"
" --walked --label-rules 0|1 --erase \"x,y,z,r;...\"\n"
"Always prints one JSON summary; errors exit 2. See docs/mr/scan.md for units and ranges.\n";
io_write(1,usage,sizeof(usage)-1);return 0;
}
@@ -367,11 +207,6 @@ int main(int argc,char **argv)
if(!s) return error_json("scan allocation failed");
s->rng=0x714ce3u;s->t0=io_time(1);
const char *error=options(&s->opt,argc,argv);
if(!error && s->opt.refine_only) {
int result=refine_only(&s->opt);
io_free(s,sizeof(Scan));return result;
}
static Outcome outcome;
if(!error) error=read_dump(s,0);
s->tread=io_time(1);
if(!error) error=init_grid(s);
@@ -382,13 +217,10 @@ int main(int argc,char **argv)
s->tmodel=io_time(1);
if(!error) error=make_mesh(s);
s->tmesh=io_time(1);
/* The grid is not needed after the mesh: freeing it keeps the refinement inside the same memory budget. */
io_free(s->cells,(usize)s->count*sizeof(Cell));s->cells=0;
if(!error) error=scan_outputs(s,&outcome);
s->trefine=io_time(1);
if(!error && outcome.rep.truncated) warn(s,"refined mesh truncated to the triangle budget");
if(!error) error=write_files(s);
int result=0;
if(error) result=error_json(error);else summary(s,&outcome);
if(error) result=error_json(error);else summary(s);
io_free(s->cells,(usize)s->count*sizeof(Cell));
io_free(s->vertices,(usize)MAX_MESH_VERTICES*sizeof(Vec));
io_free(s->indices,(usize)MAX_TRIANGLES*3*sizeof(u32));
io_free(s,sizeof(Scan));return result;
+2 -2
View File
@@ -596,7 +596,7 @@ const char *make_model(Scan *s)
return error;
}
int parse_number(char **cursor, float *value)
static int number(char **cursor, float *value)
{
char *p=*cursor;while (*p==' ' || *p=='\t' || *p=='\r') p++;
int sign=1,digits=0;if (*p=='-' || *p=='+') { if (*p=='-') sign=-1;p++; }
@@ -631,7 +631,7 @@ static const char *hint(Scan *s)
while (*p==' ' || *p=='\t' || *p=='\r' || *p=='\n') p++;
if (!*p) break;
if (*p=='#') { while (*p && *p!='\n') p++;continue; }
if (n==MAX_OUTLINE || !parse_number(&p,&pts[n].x) || !parse_number(&p,&pts[n].z)) return "invalid outline hint";
if (n==MAX_OUTLINE || !number(&p,&pts[n].x) || !number(&p,&pts[n].z)) return "invalid outline hint";
n++;while (*p==' ' || *p=='\t' || *p=='\r') p++;
if (*p && *p!='\n' && *p!='#') return "invalid outline hint";
if (*p=='#') while (*p && *p!='\n') p++;
-548
View File
@@ -1,548 +0,0 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Mesh refinement: the passes of the owner's tuned cleanup page (2026-10-18), in its order and with its rules:
* plane offsets re-estimated from the scan, structural (wall/floor/ceiling) triangles classified with one-sided
* bands, erase spheres, the walked volume, flat panels or snapping, furniture tops, clipping outside the outline,
* small islands, thin floating pieces, Taubin smoothing. docs/mr/scan.md "Refinement" describes each pass. */
#include "refine.h"
#define WELD_MM 1000.0 /* vertices closer than 1 mm (quantized) are one vertex */
#define WELD_BIAS 16384 /* key offset, as the reference: +-16 m */
#define FURNITURE_PAD .1 /* m around a box that still counts as the box */
#define MIN_SHIFT_SAMPLES 50 /* plane offsets move only with more facing triangles than this */
#define WALKED_BELOW .2 /* m above the floor: the walked column starts here (feet, rugs stay) */
#define WALKED_ABOVE .25 /* m above the head: the column ends here */
#define TOP_NORMAL_Y .8 /* a triangle this upward-facing near a box top is part of the top */
#define TUBE_MIN_TRIANGLES 10
#define TUBE_MAX_AREA 1.0 /* m^2: bigger pieces are never thin floating pieces */
#define TUBE_MAX_FURNITURE .5 /* share of a piece inside furniture boxes that keeps it */
#define TAUBIN_LAMBDA .5
#define TAUBIN_MU -.53
double dfloor(double x) { i64 i=(i64)x; return x<(double)i ? (double)(i-1) : (double)i; }
double droot(double x) { return x>0 ? __builtin_sqrt(x) : 0; }
/* Double-precision sine (range reduction + Taylor series to x^19, error < 1e-15), as Math.sin. */
double dsin(double x)
{
const double pi=3.14159265358979323846;
x-=dfloor(x/(2*pi)+.5)*(2*pi);
if(x>pi/2) x=pi-x;
if(x<-pi/2) x=-pi-x;
double q=x*x,term=x,sum=x;
for(int k=1;k<10;k++) { term*=-q/((2*k)*(2*k+1));sum+=term; }
return sum;
}
double dcos(double x) { return dsin(x+3.14159265358979323846/2); }
/* Math.hypot as V8 computes it (scaled, Kahan-compensated): distance ties at outline corners break the same way. */
double dhypot(double a, double b)
{
a=a<0 ? -a : a;b=b<0 ? -b : b;
double m=a>b ? a : b;
if(m==0) return 0;
double sum=0,comp=0,v[2]={a/m,b/m};
for(int i=0;i<2;i++) { double s=v[i]*v[i]-comp,p=sum+s;comp=(p-sum)-s;sum=p; }
return droot(sum)*m;
}
static double dab(double x) { return x<0 ? -x : x; }
static void *alloc(usize n) { return n ? io_alloc(n) : 0; }
/* ------------------------------------------------------------------------------------------------ welding */
static u64 mix(u64 k) { k^=k>>33;k*=0xff51afd7ed558ccdull;k^=k>>33;k*=0xc4ceb9fe1a85ec53ull;k^=k>>33;return k; }
static i64 quant(float v) { return (i64)dfloor((double)v*WELD_MM+.5)+WELD_BIAS; }
const char *prep_mesh(Prep *p, const Vec *v, const u32 *idx, int nv, int ni)
{
*p=(Prep){0};
int T=ni/3;
usize cap=1024;while(cap<(usize)nv*2) cap*=2;
i64 *keys=alloc(cap*sizeof(i64));
int *slot=alloc(cap*sizeof(int)),*wid=alloc((usize)(nv ? nv : 1)*sizeof(int));
double *sums=alloc((usize)(nv ? nv : 1)*4*sizeof(double));
const char *error=0;
if(!keys || !slot || !wid || !sums) { error="refine allocation failed";goto done; }
int W=0;
for(int i=0;i<nv;i++) {
i64 key=(quant(v[i].x)*32768+quant(v[i].y))*32768+quant(v[i].z);
usize h=(usize)mix((u64)key)&(cap-1);
while(slot[h] && keys[h]!=key) h=(h+1)&(cap-1);
if(!slot[h]) { keys[h]=key;slot[h]=++W; }
int w=slot[h]-1;wid[i]=w;
sums[4*w]+=v[i].x;sums[4*w+1]+=v[i].y;sums[4*w+2]+=v[i].z;sums[4*w+3]+=1;
}
p->T=T;p->W=W;
p->pos=alloc((usize)(W ? W : 1)*3*sizeof(double));
p->F=alloc((usize)(T ? T : 1)*3*sizeof(int));
p->fn=alloc((usize)(T ? T : 1)*3*sizeof(float));
p->cen=alloc((usize)(T ? T : 1)*3*sizeof(float));
p->area=alloc((usize)(T ? T : 1)*sizeof(float));
if(!p->pos || !p->F || !p->fn || !p->cen || !p->area) { error="refine allocation failed";goto done; }
for(int w=0;w<W;w++) for(int k=0;k<3;k++) p->pos[3*w+k]=sums[4*w+k]/sums[4*w+3];
for(int k=0;k<T*3;k++) p->F[k]=wid[idx[k]];
for(int t=0;t<T;t++) {
const double *a=p->pos+3*p->F[3*t],*b=p->pos+3*p->F[3*t+1],*c=p->pos+3*p->F[3*t+2];
double ux=b[0]-a[0],uy=b[1]-a[1],uz=b[2]-a[2],wx=c[0]-a[0],wy=c[1]-a[1],wz=c[2]-a[2];
double nx=uy*wz-uz*wy,ny=uz*wx-ux*wz,nz=ux*wy-uy*wx,l=droot(nx*nx+ny*ny+nz*nz);
if(!(l>0)) l=1e-12;
p->fn[3*t]=(float)(nx/l);p->fn[3*t+1]=(float)(ny/l);p->fn[3*t+2]=(float)(nz/l);p->area[t]=(float)(l/2);
for(int k=0;k<3;k++) p->cen[3*t+k]=(float)((a[k]+b[k]+c[k])/3);
}
done:
io_free(keys,cap*sizeof(i64));io_free(slot,cap*sizeof(int));
io_free(wid,(usize)(nv ? nv : 1)*sizeof(int));io_free(sums,(usize)(nv ? nv : 1)*4*sizeof(double));
if(error) prep_free(p);
return error;
}
void prep_free(Prep *p)
{
int T=p->T ? p->T : 1,W=p->W ? p->W : 1;
io_free(p->pos,(usize)W*3*sizeof(double));io_free(p->F,(usize)T*3*sizeof(int));
io_free(p->fn,(usize)T*3*sizeof(float));io_free(p->cen,(usize)T*3*sizeof(float));io_free(p->area,(usize)T*sizeof(float));
*p=(Prep){0};
}
/* ------------------------------------------------------------------------------------------------ helpers */
typedef struct {
int kind, wall; /* 0 wall (index into room->walls), 1 floor, 2 ceiling */
double n[3], off, shift;
double ox, oz, ux, uz, L;
} RPlane;
typedef struct { double cs, sn; const RVol *v; } Box;
static void box_local(const Box *b, double x, double z, double *lx, double *lz)
{
double dx=x-b->v->cx,dz=z-b->v->cz;
*lx=b->cs*dx-b->sn*dz;*lz=b->sn*dx+b->cs*dz;
}
static double s_dist(const RPlane *pl, const float *c) { return pl->n[0]*c[0]+pl->n[1]*c[1]+pl->n[2]*c[2]-pl->off; }
static int in_ext(const RPlane *pl, const float *c, double m)
{
if(pl->kind) return 1;
double s=(c[0]-pl->ox)*pl->ux+(c[2]-pl->oz)*pl->uz;
return s>-m && s<pl->L+m;
}
static int point_in_poly(double x, double z, const DPt *poly, int n)
{
int inside=0;
for(int i=0,j=n-1;i<n;j=i++) {
double xi=poly[i].x,zi=poly[i].z,xj=poly[j].x,zj=poly[j].z;
if((zi>z)!=(zj>z) && x<(xj-xi)*(z-zi)/(zj-zi+1e-12)+xi) inside=!inside;
}
return inside;
}
/* Distance outside the outline (0 inside) and the extra allowance of the nearest wall (unseen walls). */
static double outside_info(const Room *r, double x, double z, double open_extra, double *allow)
{
double best=1e300;*allow=0;
for(int i=0;i<r->nwalls;i++) {
const RWall *w=&r->walls[i];
double abx=w->x1-w->x0,abz=w->z1-w->z0,l2=abx*abx+abz*abz;
double t=l2>0 ? ((x-w->x0)*abx+(z-w->z0)*abz)/l2 : 0;
t=t<0 ? 0 : t>1 ? 1 : t;
double dx=x-(w->x0+t*abx),dz=z-(w->z0+t*abz),d=dhypot(dx,dz);
if(d<best) { best=d;*allow=w->invisible ? open_extra : 0; }
}
return point_in_poly(x,z,r->outline,r->noutline) ? 0 : best;
}
static void nth(float *a, int n, int k)
{
int lo=0,hi=n-1;
while(lo<hi) {
float pivot=a[(lo+hi)/2];int i=lo,j=hi;
while(i<=j) {
while(a[i]<pivot) i++;
while(a[j]>pivot) j--;
if(i<=j) { float t=a[i];a[i++]=a[j];a[j--]=t; }
}
if(k<=j) hi=j;else if(k>=i) lo=i;else break;
}
}
static int find(int *parent, int x)
{
while(parent[x]!=x) { parent[x]=parent[parent[x]];x=parent[x]; }
return x;
}
static void unite(int *parent, const int *F, const u8 *keep, int T, int W)
{
for(int i=0;i<W;i++) parent[i]=i;
for(int t=0;t<T;t++) if(keep[t]) {
int r=find(parent,F[3*t]);
parent[find(parent,F[3*t+1])]=r;parent[find(parent,F[3*t+2])]=r;
}
}
/* Eigenvalues of a symmetric 3x3 matrix by Jacobi rotations, as the reference's eigSym3. */
static void eig3(double a[3][3], double out[3])
{
static const int pairs[3][2]={{0,1},{0,2},{1,2}};
for(int sweep=0;sweep<20;sweep++) {
double off=dab(a[0][1])+dab(a[0][2])+dab(a[1][2]);
if(off<1e-14) break;
for(int k=0;k<3;k++) {
int p=pairs[k][0],q=pairs[k][1];
if(dab(a[p][q])<1e-18) continue;
double th=(a[q][q]-a[p][p])/(2*a[p][q]);
double t=(th<0 ? -1 : 1)/(dab(th)+droot(th*th+1)),c=1/droot(t*t+1),s=t*c;
for(int m=0;m<3;m++) { double akp=a[m][p],akq=a[m][q];a[m][p]=c*akp-s*akq;a[m][q]=s*akp+c*akq; }
for(int m=0;m<3;m++) { double apk=a[p][m],aqk=a[q][m];a[p][m]=c*apk-s*aqk;a[q][m]=s*apk+c*aqk; }
}
}
out[0]=a[0][0];out[1]=a[1][1];out[2]=a[2][2];
}
/* Ear clipping of the (positively wound) outline, exactly the reference's order. */
static int ear_clip(const DPt *poly, int n, int tris[][3])
{
int idx[MAX_OUTLINE],m=n,count=0,guard=0;
for(int i=0;i<n;i++) idx[i]=i;
#define A2(a,b,c) ((poly[b].x-poly[a].x)*(poly[c].z-poly[a].z)-(poly[b].z-poly[a].z)*(poly[c].x-poly[a].x))
while(m>3 && guard++<10000) {
int cut=0;
for(int k=0;k<m;k++) {
int i0=idx[(k-1+m)%m],i1=idx[k],i2=idx[(k+1)%m];
if(A2(i0,i1,i2)<=0) continue;
int blocked=0;
for(int q=0;q<m && !blocked;q++) {
int j=idx[q];
if(j!=i0 && j!=i1 && j!=i2 && A2(i0,i1,j)>0 && A2(i1,i2,j)>0 && A2(i2,i0,j)>0) blocked=1;
}
if(blocked) continue;
tris[count][0]=i0;tris[count][1]=i1;tris[count][2]=i2;count++;
for(int q=k;q+1<m;q++) idx[q]=idx[q+1];
m--;cut=1;break;
}
if(!cut) break;
}
#undef A2
if(m==3) { tris[count][0]=idx[0];tris[count][1]=idx[1];tris[count][2]=idx[2];count++; }
return count;
}
/* ------------------------------------------------------------------------------------------------ passes */
typedef struct {
u8 *furn, *keep;
signed char *owner;
int *parent, *start, *nb, *fill, *comp, *stamp;
double *pos, *tmp, *acc;
float *ds;
int nb_n, comp_n;
} Work;
static void work_free(Work *w, int T, int W)
{
usize t=(usize)(T ? T : 1),v=(usize)(W ? W : 1);
io_free(w->furn,t);io_free(w->keep,t);io_free(w->owner,t);io_free(w->parent,v*sizeof(int));
io_free(w->start,(v+1)*sizeof(int));io_free(w->nb,(usize)(w->nb_n ? w->nb_n : 1)*sizeof(int));
io_free(w->fill,v*sizeof(int));io_free(w->comp,v*sizeof(int));io_free(w->stamp,v*sizeof(int));
io_free(w->pos,v*3*sizeof(double));io_free(w->tmp,v*3*sizeof(double));
io_free(w->acc,(usize)(w->comp_n ? w->comp_n : 1)*12*sizeof(double));io_free(w->ds,t*sizeof(float));
}
/* Heads binned on a horizontal grid with cells of at least the walked radius: the 3 x 3 cells around a point hold
* every head within the radius, so this gives the same result as testing every head, with far fewer tests. */
typedef struct { double x0, z0, cell; int nx, nz; int *start, *list; } HeadGrid;
static int head_cell(const HeadGrid *g, double v, double v0) { return (int)dfloor((v-v0)/g->cell); }
static int head_grid(HeadGrid *g, const Vec *heads, int n, double radius)
{
double x0=1e300,x1=-1e300,z0=1e300,z1=-1e300;
for(int i=0;i<n;i++) { x0=heads[i].x<x0 ? heads[i].x : x0;x1=heads[i].x>x1 ? heads[i].x : x1;
z0=heads[i].z<z0 ? heads[i].z : z0;z1=heads[i].z>z1 ? heads[i].z : z1; }
g->cell=radius;
if((x1-x0)/256>g->cell) g->cell=(x1-x0)/256;
if((z1-z0)/256>g->cell) g->cell=(z1-z0)/256;
g->x0=x0;g->z0=z0;g->nx=head_cell(g,x1,x0)+1;g->nz=head_cell(g,z1,z0)+1;
g->start=alloc((usize)(g->nx*g->nz+1)*sizeof(int));g->list=alloc((usize)n*sizeof(int));
int *fill=alloc((usize)(g->nx*g->nz)*sizeof(int));
if(!g->start || !g->list || !fill) { io_free(fill,(usize)(g->nx*g->nz)*sizeof(int));return 0; }
for(int i=0;i<n;i++) g->start[head_cell(g,heads[i].z,z0)*g->nx+head_cell(g,heads[i].x,x0)+1]++;
for(int c=0;c<g->nx*g->nz;c++) g->start[c+1]+=g->start[c];
for(int i=0;i<n;i++) {
int c=head_cell(g,heads[i].z,z0)*g->nx+head_cell(g,heads[i].x,x0);
g->list[g->start[c]+fill[c]++]=i;
}
io_free(fill,(usize)(g->nx*g->nz)*sizeof(int));
return 1;
}
static void head_grid_free(HeadGrid *g, int n)
{
io_free(g->start,(usize)(g->nx*g->nz+1)*sizeof(int));io_free(g->list,(usize)n*sizeof(int));
}
static int walked_hit(const HeadGrid *g, const Vec *heads, double x, double y, double z, double r2)
{
int cx=head_cell(g,x,g->x0),cz=head_cell(g,z,g->z0);
for(int v=cz-1;v<=cz+1;v++) for(int u=cx-1;u<=cx+1;u++) {
if(u<0 || v<0 || u>=g->nx || v>=g->nz) continue;
for(int k=g->start[v*g->nx+u];k<g->start[v*g->nx+u+1];k++) {
const Vec *h=&heads[g->list[k]];
if(y<h->y+WALKED_ABOVE && (x-h->x)*(x-h->x)+(z-h->z)*(z-h->z)<=r2) return 1;
}
}
return 0;
}
const char *refine_run(const Refine *o, const Room *room, const Prep *P, const RVol *vols, int nvols,
const Vec *heads, int nheads, Vec **out_v, u32 **out_i, int *out_n, Report *rep)
{
int T=P->T,W=P->W;
const float *cen=P->cen,*fn=P->fn,*area=P->area;
const int *F=P->F;
Work w={0};
const char *error=0;
*out_v=0;*out_i=0;*out_n=0;
memset(rep,0,sizeof(*rep));rep->triangles=T;
usize tb=(usize)(T ? T : 1),wb=(usize)(W ? W : 1);
w.furn=alloc(tb);w.keep=alloc(tb);w.owner=alloc(tb);w.ds=alloc(tb*sizeof(float));
w.parent=alloc(wb*sizeof(int));w.pos=alloc(wb*3*sizeof(double));w.stamp=alloc(wb*sizeof(int));
if(!w.furn || !w.keep || !w.owner || !w.ds || !w.parent || !w.pos || !w.stamp) { error="refine allocation failed";goto done; }
memcpy(w.pos,P->pos,(usize)W*3*sizeof(double));
double *pos=w.pos;
/* furniture flags: the ruled boxes (+10 cm) */
Box boxes[MAX_VOLUMES];
for(int i=0;i<nvols;i++) {
double a=vols[i].yaw*3.14159265358979323846/180;
boxes[i]=(Box){dcos(a),dsin(a),&vols[i]};
}
for(int i=0;i<nvols;i++) {
const RVol *v=&vols[i];
for(int t=0;t<T;t++) {
double lx,lz,y=cen[3*t+1];
box_local(&boxes[i],cen[3*t],cen[3*t+2],&lx,&lz);
if(dab(lx)<=v->w/2.+FURNITURE_PAD && dab(lz)<=v->d/2.+FURNITURE_PAD &&
y<=v->top+FURNITURE_PAD && y>=v->top-v->h-FURNITURE_PAD) w.furn[t]=1;
}
}
/* 1. planes: seen walls, floor, ceiling; offsets re-estimated (median of facing triangles within the band) */
RPlane planes[MAX_OUTLINE+2];int np=0;
for(int i=0;i<room->nwalls;i++) {
const RWall *wl=&room->walls[i];
if(wl->invisible) continue;
double dx=wl->x1-wl->x0,dz=wl->z1-wl->z0,L=dhypot(dx,dz);
if(!(L>0)) continue;
RPlane *pl=&planes[np++];
*pl=(RPlane){0,i,{-dz/L,0,dx/L},0,0,wl->x0,wl->z0,dx/L,dz/L,L};
pl->off=pl->n[0]*wl->x0+pl->n[2]*wl->z0;
}
planes[np++]=(RPlane){1,-1,{0,1,0},room->floor,0,0,0,0,0,0};
planes[np++]=(RPlane){2,-1,{0,-1,0},-(double)room->ceiling,0,0,0,0,0,0};
double cos_lim=dcos(o->facing_deg*3.14159265358979323846/180);
#define FACING(pl,t) ((pl)->n[0]*fn[3*(t)]+(pl)->n[1]*fn[3*(t)+1]+(pl)->n[2]*fn[3*(t)+2]>cos_lim)
for(int k=0;k<np;k++) {
RPlane *pl=&planes[k];int n=0;
for(int t=0;t<T;t++) {
if(w.furn[t]) continue;
double d=s_dist(pl,cen+3*t);
if(dab(d)<o->band && FACING(pl,t) && in_ext(pl,cen+3*t,o->band)) w.ds[n++]=(float)d;
}
if(n>MIN_SHIFT_SAMPLES) { nth(w.ds,n,n>>1);pl->shift=w.ds[n>>1]; }
pl->off+=pl->shift;
rep->shifts[rep->nshifts++]=(Shift){pl->kind,pl->wall,(float)pl->shift};
}
double floor_y=planes[np-2].off,ceil_y=-planes[np-1].off;
rep->floor_y=(float)floor_y;rep->ceiling_y=(float)ceil_y;
/* 2. structural triangles: behind a surface = band; in front = band_in (walls; the full band in the wall's top
* strip), floor_in, ceil_in */
for(int t=0;t<T;t++) w.owner[t]=-1;
if(o->walls) for(int t=0;t<T;t++) {
if(w.furn[t]) continue;
double best=1e300;
for(int k=0;k<np;k++) {
const RPlane *pl=&planes[k];double d=s_dist(pl,cen+3*t);
double front=!pl->kind ? (cen[3*t+1]>ceil_y-o->top_strip ? o->band : o->band_in) :
pl->kind==2 ? o->ceil_in : o->floor_in;
if(d>-o->band && d<front && dab(d)<best && FACING(pl,t) && in_ext(pl,cen+3*t,o->band)) {
best=dab(d);w.owner[t]=(signed char)k;
}
}
}
#undef FACING
for(int t=0;t<T;t++) w.keep[t]=1;
/* erase spheres */
for(int e=0;e<o->nerase;e++) {
const Sphere *s=&o->erase[e];
for(int t=0;t<T;t++) {
double dx=cen[3*t]-s->x,dy=cen[3*t+1]-s->y,dz=cen[3*t+2]-s->z;
if(w.keep[t] && dx*dx+dy*dy+dz*dz<=(double)s->r*s->r) { w.keep[t]=0;rep->erased++; }
}
}
/* the walked volume: a column under each head position, never inside furniture */
if(o->walked>0 && nheads) {
HeadGrid g={0};
if(!head_grid(&g,heads,nheads,o->walked)) { head_grid_free(&g,nheads);error="refine allocation failed";goto done; }
double r2=(double)o->walked*o->walked;
for(int t=0;t<T;t++) {
if(!w.keep[t] || w.furn[t]) continue;
double y=cen[3*t+1];
if(y<room->floor+WALKED_BELOW) continue;
if(walked_hit(&g,heads,cen[3*t],y,cen[3*t+2],r2)) { w.keep[t]=0;rep->walked++; }
}
head_grid_free(&g,nheads);
} else if(o->walked>0) rep->walked_skipped=1;
for(int t=0;t<T;t++) if(w.owner[t]>=0) rep->structural++;
if(o->walls==2) {
for(int t=0;t<T;t++) if(w.owner[t]>=0) w.keep[t]=0;
if(o->near>0) for(int t=0;t<T;t++) {
if(!w.keep[t] || w.furn[t]) continue;
for(int k=0;k<np;k++) if(dab(s_dist(&planes[k],cen+3*t))<o->near && in_ext(&planes[k],cen+3*t,o->near)) {
w.keep[t]=0;rep->near++;break;
}
}
} else if(o->walls==1) {
/* snap: a vertex goes onto the plane of the last structural triangle that uses it */
int *vown=w.stamp;
for(int i=0;i<W;i++) vown[i]=-1;
for(int t=0;t<T;t++) if(w.owner[t]>=0) for(int j=0;j<3;j++) vown[F[3*t+j]]=w.owner[t];
for(int i=0;i<W;i++) {
if(vown[i]<0) continue;
const RPlane *pl=&planes[vown[i]];
double d=pl->n[0]*pos[3*i]+pl->n[1]*pos[3*i+1]+pl->n[2]*pos[3*i+2]-pl->off;
for(int k=0;k<3;k++) pos[3*i+k]-=d*pl->n[k];
}
}
/* 2b. furniture tops: upward scan near a box's top, inside its footprint, goes onto the top */
if(o->tops>0) {
for(int i=0;i<W;i++) w.stamp[i]=0;
for(int i=0;i<nvols;i++) {
const RVol *v=&vols[i];int mark=i+1;
for(int t=0;t<T;t++) {
if(!w.keep[t]) continue;
const double *a=pos+3*F[3*t],*b=pos+3*F[3*t+1],*c=pos+3*F[3*t+2];
double cy=(a[1]+b[1]+c[1])/3;
if(dab(cy-v->top)>=o->tops) continue;
double lx,lz;
box_local(&boxes[i],(a[0]+b[0]+c[0])/3,(a[2]+b[2]+c[2])/3,&lx,&lz);
if(dab(lx)>v->w/2. || dab(lz)>v->d/2.) continue;
double ux=b[0]-a[0],uy=b[1]-a[1],uz=b[2]-a[2],wx=c[0]-a[0],wy=c[1]-a[1],wz=c[2]-a[2];
double nx=uy*wz-uz*wy,ny=uz*wx-ux*wz,nz=ux*wy-uy*wx,l=droot(nx*nx+ny*ny+nz*nz);
if(ny/(l>0 ? l : 1)<=TOP_NORMAL_Y) continue;
for(int j=0;j<3;j++) w.stamp[F[3*t+j]]=mark;
rep->tops++;
}
for(int k=0;k<W;k++) if(w.stamp[k]==mark) pos[3*k+1]=v->top;
}
}
/* 3. clip outside the room (unseen walls allow clip_open more) and above/below the planes */
if(o->clip<1 || o->clip_open<2) for(int t=0;t<T;t++) {
if(!w.keep[t]) continue;
double allow,out=outside_info(room,cen[3*t],cen[3*t+2],o->clip_open,&allow),y=cen[3*t+1];
if(out>o->clip+allow || y>ceil_y+o->clip || y<floor_y-o->clip) { w.keep[t]=0;rep->outside++; }
}
/* 4. islands: connected pieces below the minimum area */
if(o->island>0) {
double *comp=w.tmp=alloc(wb*3*sizeof(double));
if(!comp) { error="refine allocation failed";goto done; }
unite(w.parent,F,w.keep,T,W);
for(int t=0;t<T;t++) if(w.keep[t]) comp[find(w.parent,F[3*t])]+=area[t];
for(int t=0;t<T;t++)
if(w.keep[t] && comp[find(w.parent,F[3*t])]<o->island && !(o->protect && w.furn[t])) { w.keep[t]=0;rep->islands++; }
io_free(w.tmp,wb*3*sizeof(double));w.tmp=0;
}
/* 4b. thin floating pieces: elongated (PCA), thin, small, not furniture */
if(o->tubes>0) {
unite(w.parent,F,w.keep,T,W);
w.comp=alloc(wb*sizeof(int));
if(!w.comp) { error="refine allocation failed";goto done; }
for(int i=0;i<W;i++) w.comp[i]=-1;
int nc=0;
for(int t=0;t<T;t++) if(w.keep[t]) { int r=find(w.parent,F[3*t]);if(w.comp[r]<0) w.comp[r]=nc++; }
w.comp_n=nc;
w.acc=alloc((usize)(nc ? nc : 1)*12*sizeof(double));
if(!w.acc) { error="refine allocation failed";goto done; }
for(int t=0;t<T;t++) {
if(!w.keep[t]) continue;
double *a=w.acc+12*w.comp[find(w.parent,F[3*t])];
for(int j=0;j<3;j++) {
const double *q=pos+3*F[3*t+j];double x=q[0],y=q[1],z=q[2];
a[0]++;a[1]+=x;a[2]+=y;a[3]+=z;a[4]+=x*x;a[5]+=x*y;a[6]+=x*z;a[7]+=y*y;a[8]+=y*z;a[9]+=z*z;
}
a[10]+=area[t];a[11]+=w.furn[t] ? 1 : 0;
}
u8 *drop=(u8 *)w.stamp; /* nc <= T bytes fit in the W ints */
for(int c=0;c<nc;c++) {
double *a=w.acc+12*c,n=a[0],tris=n/3;drop[c]=0;
if(tris<TUBE_MIN_TRIANGLES || a[11]/tris>TUBE_MAX_FURNITURE || a[10]>=TUBE_MAX_AREA) continue;
double mx_=a[1]/n,my=a[2]/n,mz=a[3]/n;
double C[3][3]={{a[4]/n-mx_*mx_,a[5]/n-mx_*my,a[6]/n-mx_*mz},{0,a[7]/n-my*my,a[8]/n-my*mz},{0,0,a[9]/n-mz*mz}};
C[1][0]=C[0][1];C[2][0]=C[0][2];C[2][1]=C[1][2];
double ev[3];eig3(C,ev);
for(int k=0;k<3;k++) ev[k]=droot(ev[k]>1e-12 ? ev[k] : 1e-12);
for(int i=0;i<2;i++) for(int j=0;j<2-i;j++) if(ev[j]>ev[j+1]) { double s=ev[j];ev[j]=ev[j+1];ev[j+1]=s; }
if(ev[2]/(ev[1]>1e-6 ? ev[1] : 1e-6)>=o->tubes && ev[1]<=o->tube_thick) drop[c]=1;
}
for(int t=0;t<T;t++) if(w.keep[t] && drop[w.comp[find(w.parent,F[3*t])]]) { w.keep[t]=0;rep->tubes++; }
}
/* 5. Taubin smoothing over the kept triangles (lambda/mu pairs keep the volume) */
if(o->smooth>0) {
w.start=alloc((wb+1)*sizeof(int));w.fill=alloc(wb*sizeof(int));w.tmp=alloc(wb*3*sizeof(double));
if(!w.start || !w.fill || !w.tmp) { error="refine allocation failed";goto done; }
for(int t=0;t<T;t++) if(w.keep[t]) for(int j=0;j<3;j++) w.start[F[3*t+j]+1]+=2;
for(int i=0;i<W;i++) w.start[i+1]+=w.start[i];
w.nb_n=w.start[W];
w.nb=alloc((usize)(w.nb_n ? w.nb_n : 1)*sizeof(int));
if(!w.nb) { error="refine allocation failed";goto done; }
for(int i=0;i<W;i++) w.fill[i]=w.start[i];
for(int t=0;t<T;t++) if(w.keep[t]) for(int j=0;j<3;j++) {
int a=F[3*t+j],b=F[3*t+(j+1)%3],c=F[3*t+(j+2)%3];
w.nb[w.fill[a]++]=b;w.nb[w.fill[a]++]=c;
}
for(int it=0;it<o->smooth;it++) for(int pass=0;pass<2;pass++) {
double f=pass ? TAUBIN_MU : TAUBIN_LAMBDA;
for(int i=0;i<W;i++) {
int s0=w.start[i],e=w.start[i+1];
if(s0==e) { for(int k=0;k<3;k++) w.tmp[3*i+k]=pos[3*i+k];continue; }
double x=0,y=0,z=0;
for(int k=s0;k<e;k++) { const double *q=pos+3*w.nb[k];x+=q[0];y+=q[1];z+=q[2]; }
int n=e-s0;
w.tmp[3*i]=pos[3*i]+f*(x/n-pos[3*i]);w.tmp[3*i+1]=pos[3*i+1]+f*(y/n-pos[3*i+1]);
w.tmp[3*i+2]=pos[3*i+2]+f*(z/n-pos[3*i+2]);
}
memcpy(pos,w.tmp,(usize)W*3*sizeof(double));
}
}
/* 6. output: the kept scan, then flat panels for seen walls, the floor and the ceiling */
for(int t=0;t<T;t++) rep->kept+=w.keep[t];
Vec panel[(MAX_OUTLINE*2+MAX_OUTLINE*2)*3];int npanel=0;
if(o->walls==2) {
for(int k=0;k<np;k++) {
const RPlane *pl=&planes[k];
if(pl->kind) continue;
const RWall *wl=&room->walls[pl->wall];double sh=pl->shift;
double a0x=wl->x0+pl->n[0]*sh,a0z=wl->z0+pl->n[2]*sh,a1x=wl->x1+pl->n[0]*sh,a1z=wl->z1+pl->n[2]*sh;
Vec q[4]={vec((float)a0x,(float)floor_y,(float)a0z),vec((float)a1x,(float)floor_y,(float)a1z),
vec((float)a1x,(float)ceil_y,(float)a1z),vec((float)a0x,(float)ceil_y,(float)a0z)};
double ux=a1x-a0x,uz=a1z-a0z,vy=ceil_y-floor_y;
int ok=(-uz*vy)*pl->n[0]+(ux*vy)*pl->n[2]>0;
int order[2][6]={{0,1,2,0,2,3},{1,0,3,1,3,2}};
for(int j=0;j<6;j++) panel[npanel*3+j]=q[order[!ok][j]];
npanel+=2;
}
int ears[MAX_OUTLINE][3];
int ne=ear_clip(room->outline,room->noutline,ears);
for(int e=0;e<ne;e++) {
DPt A=room->outline[ears[e][0]],B=room->outline[ears[e][1]],C=room->outline[ears[e][2]];
panel[npanel*3]=vec((float)A.x,(float)floor_y,(float)A.z);panel[npanel*3+1]=vec((float)C.x,(float)floor_y,(float)C.z);
panel[npanel*3+2]=vec((float)B.x,(float)floor_y,(float)B.z);npanel++;
panel[npanel*3]=vec((float)A.x,(float)ceil_y,(float)A.z);panel[npanel*3+1]=vec((float)B.x,(float)ceil_y,(float)B.z);
panel[npanel*3+2]=vec((float)C.x,(float)ceil_y,(float)C.z);npanel++;
}
}
rep->panels=npanel;
int room_left=MAX_TRIANGLES-npanel,kept_out=rep->kept<room_left ? rep->kept : room_left;
rep->truncated=rep->kept-kept_out;
*out_v=io_alloc((usize)MAX_MESH_VERTICES*sizeof(Vec));
*out_i=io_alloc((usize)MAX_TRIANGLES*3*sizeof(u32));
if(!*out_v || !*out_i) { error="refine allocation failed";goto done; }
int o_n=0;
for(int t=0;t<T && o_n<kept_out*3;t++) {
if(!w.keep[t]) continue;
for(int j=0;j<3;j++) {
const double *q=pos+3*F[3*t+j];
(*out_v)[o_n]=vec((float)q[0],(float)q[1],(float)q[2]);(*out_i)[o_n]=(u32)o_n;o_n++;
}
}
for(int k=0;k<npanel*3;k++) { (*out_v)[o_n]=panel[k];(*out_i)[o_n]=(u32)o_n;o_n++; }
*out_n=o_n;
done:
if(error) {
io_free(*out_v,(usize)MAX_MESH_VERTICES*sizeof(Vec));io_free(*out_i,(usize)MAX_TRIANGLES*3*sizeof(u32));
*out_v=0;*out_i=0;*out_n=0;
}
work_free(&w,T,W);
return error;
}
-101
View File
@@ -1,101 +0,0 @@
/* SPDX-License-Identifier: GPL-3.0-only */
#ifndef FP_ROOMSCAN_REFINE_H
#define FP_ROOMSCAN_REFINE_H
#include "scan.h"
/* Room cleanup after the mesh (docs/mr/scan.md "Refinement"): the scanner's unrefined output is kept as
* room-raw.txt + room-mesh-raw.bin, the refined one replaces room.txt + room-mesh.bin, scan.json holds the head
* path, the settings used, the per-pass report, the label-rule decisions and door/closet suggestions. */
#define MAX_EXTRA_TEXT 8192
#define MAX_LABEL 24
#define MAX_REASON 240
#define MAX_RAW_FILE (16u+(usize)MAX_MESH_VERTICES*12u+(usize)MAX_TRIANGLES*12u)
#define MAX_JSON_FILE (1u<<20)
/* Growable text buffer (io_alloc backed). ok=0 after an allocation failure or the cap. */
typedef struct { char *data; usize n, cap, limit; int ok; } Buf;
void buf_init(Buf *b, usize limit);
void buf_free(Buf *b);
void put(Buf *b, const char *s);
void put_n(Buf *b, const char *s, usize n);
void put_int(Buf *b, i64 v);
void put_u64(Buf *b, u64 v);
void put_dec(Buf *b, float v); /* 4 decimals, the room.txt number format */
void put_fixed(Buf *b, double v, int places); /* JavaScript's toFixed */
void put_round(Buf *b, double v); /* JavaScript's Math.round */
typedef struct { double x, z; } DPt;
/* Room numbers are doubles parsed exactly from their decimal text, as the reference page reads them. */
typedef struct { double x0, z0, x1, z1, y0, y1; int invisible; char id[33]; } RWall;
typedef struct { double cx, cz, top, yaw, w, d, h; char label[MAX_LABEL]; char id[33]; } RVol;
typedef struct {
u64 universe, time;
char source[16];
char room_id[33], floor_id[33], ceiling_id[33], mesh_id[33];
double floor, ceiling;
DPt outline[MAX_OUTLINE];
int noutline;
RWall walls[MAX_OUTLINE];
int nwalls;
RVol vols[MAX_VOLUMES];
int nvols;
char mesh_name[64];
u32 mesh_nv, mesh_ni;
int has_mesh;
char extra[MAX_EXTRA_TEXT]; /* records this scanner does not model (plane, ...), kept verbatim */
usize nextra;
} Room;
/* The mesh with its vertices welded at 1 mm (as the reference page does), per-triangle normals/centroids/areas. */
typedef struct {
int T, W;
double *pos;
int *F;
float *fn, *cen, *area;
} Prep;
typedef struct { int kind, wall; float shift; } Shift; /* kind: 0 wall, 1 floor, 2 ceiling */
typedef struct {
int triangles, kept, structural, near, outside, islands, tubes, erased, walked, tops, panels, truncated;
int walked_skipped;
float floor_y, ceiling_y;
Shift shifts[MAX_OUTLINE+2];
int nshifts;
} Report;
typedef struct {
char from[MAX_LABEL], to[MAX_LABEL], suggest[8], reason[MAX_REASON];
RVol box;
int wall;
float along;
} Label;
/* roomio.c */
const char *room_from_scan(Scan *s, Room *r);
const char *room_parse(const char *text, usize n, Room *r);
int room_text(const Room *r, Buf *b);
const char *read_file(const char *path, usize limit, u8 **data, usize *size);
const char *publish(const char *dir, const char *name, const void *data, usize size);
int join_path(char *out, const char *dir, const char *name);
const char *mesh_parse(const u8 *data, usize size, Vec **vertices, u32 **indices, int *nv, int *ni);
const char *mesh_bytes(const Vec *vertices, const u32 *indices, int nv, int ni, u8 **data, usize *size);
void heads_json(Buf *b, const Vec *heads, int n);
int heads_parse(const char *text, usize n, Vec *heads);
void settings_json(Buf *b, const Refine *o);
void report_json(Buf *b, const Report *r, int nlabels, int changed, int removed, int nsuggest);
/* refine.c */
const char *prep_mesh(Prep *p, const Vec *vertices, const u32 *indices, int nv, int ni);
void prep_free(Prep *p);
const char *refine_run(const Refine *o, const Room *room, const Prep *p, const RVol *vols, int nvols,
const Vec *heads, int nheads, Vec **out_v, u32 **out_i, int *out_n, Report *rep);
double dfloor(double x);
double droot(double x);
double dsin(double x);
double dcos(double x);
double dhypot(double a, double b);
/* rules.c */
int rules_apply(const Room *room, const Prep *p, RVol *out, int *nout, Label *labels);
#endif
-476
View File
@@ -1,476 +0,0 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Room files: the room.txt writer/reader (docs/mr/ROOM_FORMAT.md), mesh sidecars, atomic publishing and the JSON
* pieces of scan.json. Every number goes through put_dec, so a room read back from its own text is bit-identical
* to the one the scan path refines (that is what makes --refine-only reproduce a scan's refined output). */
#include "refine.h"
void buf_init(Buf *b, usize limit) { *b=(Buf){0};b->limit=limit;b->ok=1; }
void buf_free(Buf *b) { io_free(b->data,b->cap);*b=(Buf){0}; }
void put_n(Buf *b, const char *s, usize n)
{
if(!b->ok) return;
if(b->n+n+1>b->cap) {
usize cap=b->cap ? b->cap : 256;
while(cap<b->n+n+1) cap*=2;
if(cap>b->limit) cap=b->limit;
if(cap<b->n+n+1) { b->ok=0;return; }
char *data=io_alloc(cap);
if(!data) { b->ok=0;return; }
if(b->n) memcpy(data,b->data,b->n);
io_free(b->data,b->cap);b->data=data;b->cap=cap;
}
memcpy(b->data+b->n,s,n);b->n+=n;b->data[b->n]=0;
}
void put(Buf *b, const char *s) { usize n=0;while(s[n]) n++;put_n(b,s,n); }
void put_u64(Buf *b, u64 value)
{
char rev[24];int n=0;
do { rev[n++]=(char)('0'+value%10);value/=10; } while(value);
char out[24];for(int i=0;i<n;i++) out[i]=rev[n-1-i];
put_n(b,out,(usize)n);
}
void put_int(Buf *b, i64 v) { if(v<0) { put(b,"-");v=-v; } put_u64(b,(u64)v); }
void put_dec(Buf *b, float value)
{
/* Same arithmetic as the scanner's original writer: the --refine 0 output stays byte-for-byte unchanged. */
if(value<0) { put(b,"-");value=-value; }
u64 scaled=(u64)(value*10000.f+.5f);
put_u64(b,scaled/10000);put(b,".");
u64 fraction=scaled%10000;
if(fraction<1000) put(b,"0");
if(fraction<100) put(b,"0");
if(fraction<10) put(b,"0");
put_u64(b,fraction);
}
void put_fixed(Buf *b, double v, int places)
{
/* Exact, like Number.prototype.toFixed: v*10^places from the double's bits in 128-bit integers, ties up. */
double scale=1;for(int i=0;i<places;i++) scale*=10;
int negative=v<0;if(negative) v=-v;
if(!(v<1e9)) v=0;
union { double d; u64 u; } bits={v};
int e=(int)((bits.u>>52)&0x7ff);
u64 m=bits.u&((1ull<<52)-1);
if(e) m|=1ull<<52;else e=1;
e-=1075; /* v = m * 2^e */
unsigned __int128 big=(unsigned __int128)m*(u64)scale;
u64 scaled;
if(e>=0) scaled=(u64)(big<<e);
else if(e<=-127) scaled=0;
else {
unsigned __int128 half=(unsigned __int128)1<<(-e-1),rest=big&(((unsigned __int128)1<<-e)-1);
scaled=(u64)(big>>-e)+(rest>=half);
}
if(negative && scaled) put(b,"-");
put_u64(b,(u64)(scaled/(u64)scale));
if(places) {
put(b,".");u64 fraction=scaled%(u64)scale;
for(u64 d=(u64)scale/10;d>1 && fraction<d;d/=10) put(b,"0");
put_u64(b,fraction);
}
}
void put_round(Buf *b, double v) { put_int(b,(i64)dfloor(v+.5)); }
static int uuid_hex(char out[33])
{
u8 bytes[16];if(!io_random(bytes,sizeof(bytes))) return 0;
bytes[6]=(bytes[6]&15)|64;bytes[8]=(bytes[8]&63)|128;
const char *hex="0123456789abcdef";
for(int i=0;i<16;i++) { out[2*i]=hex[bytes[i]>>4];out[2*i+1]=hex[bytes[i]&15]; }
out[32]=0;return 1;
}
static void copy(char *dst, const char *src, usize cap)
{
usize i=0;while(src[i] && i+1<cap) { dst[i]=src[i];i++; }
dst[i]=0;
}
const char *room_from_scan(Scan *s, Room *r)
{
memset(r,0,sizeof(*r));
r->universe=s->opt.universe;r->time=(u64)(io_time(0)/1000000000);copy(r->source,"scan",sizeof(r->source));
if(!uuid_hex(r->room_id) || !uuid_hex(r->floor_id) || !uuid_hex(r->ceiling_id)) return "getrandom failed";
r->floor=s->floor;r->ceiling=s->ceiling;r->noutline=s->noutline;
for(int i=0;i<s->noutline;i++) {
Pt a=s->outline[i],b=s->outline[(i+1)%s->noutline];
r->outline[i]=(DPt){a.x,a.z};
r->walls[i]=(RWall){a.x,a.z,b.x,b.z,s->floor,s->ceiling,s->invisible[i],{0}};
if(!uuid_hex(r->walls[i].id)) return "getrandom failed";
}
r->nwalls=s->noutline;
for(int i=0;i<s->nvolumes;i++) {
Volume *v=&s->volumes[i];RVol *o=&r->vols[i];
*o=(RVol){v->p.x,v->p.z,v->p.y,v->yaw,v->w,v->d,v->h,{0},{0}};
copy(o->label,v->label,sizeof(o->label));
if(!uuid_hex(o->id)) return "getrandom failed";
}
r->nvols=s->nvolumes;
if(!uuid_hex(r->mesh_id)) return "getrandom failed";
copy(r->mesh_name,"room-mesh.bin",sizeof(r->mesh_name));
r->has_mesh=1;r->mesh_nv=(u32)s->nv;r->mesh_ni=(u32)s->ni;
return 0;
}
static void sp(Buf *b, double v) { put(b," ");put_dec(b,(float)v); }
int room_text(const Room *r, Buf *b)
{
put(b,"frameport-room 1\nuniverse ");put_u64(b,r->universe);
put(b,"\nsource ");put(b,r->source);put(b,"\ntime ");put_u64(b,r->time);
put(b,"\nroom ");put(b,r->room_id);
put(b,"\nfloor ");put(b,r->floor_id);sp(b,r->floor);
put(b,"\nceiling ");put(b,r->ceiling_id);sp(b,r->ceiling);
put(b,"\noutline ");put_u64(b,(u64)r->noutline);
for(int i=0;i<r->noutline;i++) { sp(b,r->outline[i].x);sp(b,r->outline[i].z); }
put(b,"\n");
for(int i=0;i<r->nwalls;i++) {
const RWall *w=&r->walls[i];
put(b,"wall ");put(b,w->id);sp(b,w->x0);sp(b,w->z0);sp(b,w->x1);sp(b,w->z1);sp(b,w->y0);sp(b,w->y1);
if(w->invisible) put(b," INVISIBLE_WALL_FACE");
put(b,"\n");
}
for(int i=0;i<r->nvols;i++) {
const RVol *v=&r->vols[i];
put(b,"volume ");put(b,v->id);put(b," ");put(b,v->label);
sp(b,v->cx);sp(b,v->cz);sp(b,v->top);sp(b,v->yaw);sp(b,v->w);sp(b,v->d);sp(b,v->h);put(b,"\n");
}
put_n(b,r->extra,r->nextra);
if(r->has_mesh) {
put(b,"mesh ");put(b,r->mesh_id);put(b," ");put(b,r->mesh_name);put(b," ");
put_u64(b,r->mesh_nv);put(b," ");put_u64(b,r->mesh_ni);put(b,"\n");
}
return b->ok;
}
/* ------------------------------------------------------------------------------------------------ reading */
typedef struct { char *p[64]; int n; } Fields;
static int is_space(char c) { return c==' ' || c=='\t' || c=='\r'; }
/* Semantic labels are Meta's upper-case names; anything else would also be unsafe in scan.json strings. */
static int field_label(const char *p)
{
int n=0;
for(;p[n];n++) if(!((p[n]>='A' && p[n]<='Z') || (p[n]>='0' && p[n]<='9') || p[n]=='_')) return 0;
return n>0 && n<MAX_LABEL;
}
static int field_uuid(const char *p)
{
int n=0;
for(;p[n];n++) if(!((p[n]>='0' && p[n]<='9') || (p[n]>='a' && p[n]<='f'))) return 0;
return n==32;
}
/* Decimal text to double, correctly rounded for the room format's numbers (<= 17 significant digits): the
* mantissa and the power of ten are exact doubles, so one division/multiplication rounds once, as parseFloat. */
static int field_double(const char *p, double *out)
{
int negative=0,digits=0,exponent=0;u64 mantissa=0;
if(*p=='-' || *p=='+') negative=*p++=='-';
for(;*p>='0' && *p<='9';p++,digits++) { if(mantissa<100000000000000000ull) mantissa=mantissa*10+(u64)(*p-'0');else exponent++; }
if(*p=='.') for(p++;*p>='0' && *p<='9';p++,digits++)
if(mantissa<100000000000000000ull) { mantissa=mantissa*10+(u64)(*p-'0');exponent--; }
if(!digits) return 0;
if(*p=='e' || *p=='E') {
p++;int sign=1,e=0,ed=0;
if(*p=='-' || *p=='+') sign=*p++=='-' ? -1 : 1;
for(;*p>='0' && *p<='9';p++,ed++) { e=e*10+*p-'0';if(e>400) return 0; }
if(!ed) return 0;
exponent+=sign*e;
}
if(*p) return 0;
double v=(double)mantissa,scale=1;
int e=exponent<0 ? -exponent : exponent;
for(int i=0;i<e && scale<1e300;i++) scale*=10;
v=exponent<0 ? v/scale : v*scale;
if(!(v<=1e7)) return 0; /* also rejects NaN; room coordinates are metres */
*out=negative ? -v : v;return 1;
}
static int field_u64(const char *p, u64 *out)
{
u64 n=0;if(!*p) return 0;
for(;*p;p++) {
if(*p<'0' || *p>'9') return 0;
unsigned digit=(unsigned)(*p-'0');
if(n>(~(u64)0-digit)/10) return 0;
n=n*10+digit;
}
*out=n;return 1;
}
static int floats(Fields *f, int from, double *out, int n)
{
if(f->n<from+n) return 0;
for(int i=0;i<n;i++) if(!field_double(f->p[from+i],&out[i])) return 0;
return 1;
}
const char *room_parse(const char *text, usize n, Room *r)
{
static char line[16384];
memset(r,0,sizeof(*r));
int have_floor=0,have_ceiling=0,first=1;
usize at=0;
while(at<n) {
usize start=at,len=0;
while(at<n && text[at]!='\n') at++;
len=at-start;at++;
if(len>=sizeof(line)) return "room line too long";
memcpy(line,text+start,len);line[len]=0;
usize raw_len=len;
for(usize i=0;i<len;i++) if(line[i]=='#') { line[i]=0;len=i;break; }
Fields f={{0},0};
char *p=line;
while(*p) {
while(is_space(*p)) *p++=0;
if(!*p) break;
if(f.n==64) return "room line has too many fields";
f.p[f.n++]=p;
while(*p && !is_space(*p)) p++;
}
if(first) {
if(f.n!=2 || !same(f.p[0],"frameport-room") || !same(f.p[1],"1")) return "not a frameport-room 1 file";
first=0;continue;
}
if(!f.n) continue;
const char *k=f.p[0];
if(same(k,"universe")) { if(f.n<2 || !field_u64(f.p[1],&r->universe)) return "invalid universe record"; }
else if(same(k,"time")) { if(f.n<2 || !field_u64(f.p[1],&r->time)) return "invalid time record"; }
else if(same(k,"source")) { if(f.n<2) return "invalid source record";copy(r->source,f.p[1],sizeof(r->source)); }
else if(same(k,"room")) { if(f.n<2 || !field_uuid(f.p[1])) return "invalid room record";copy(r->room_id,f.p[1],33); }
else if(same(k,"floor") || same(k,"ceiling")) {
int c=same(k,"ceiling");
if(f.n<3 || !field_uuid(f.p[1]) || !field_double(f.p[2],c ? &r->ceiling : &r->floor)) return "invalid floor/ceiling record";
copy(c ? r->ceiling_id : r->floor_id,f.p[1],33);
if(c) have_ceiling=1;else have_floor=1;
} else if(same(k,"outline")) {
u64 count;
if(f.n<2 || !field_u64(f.p[1],&count) || count<3 || count>MAX_OUTLINE || (u64)f.n!=2+2*count) return "invalid outline record";
for(int i=0;i<(int)count;i++)
if(!field_double(f.p[2+2*i],&r->outline[i].x) || !field_double(f.p[3+2*i],&r->outline[i].z)) return "invalid outline record";
r->noutline=(int)count;
} else if(same(k,"wall")) {
double v[6];
if(r->nwalls==MAX_OUTLINE) return "too many walls";
if(f.n<8 || !field_uuid(f.p[1]) || !floats(&f,2,v,6)) return "invalid wall record";
RWall *w=&r->walls[r->nwalls++];
*w=(RWall){v[0],v[1],v[2],v[3],v[4],v[5],f.n>8 && same(f.p[8],"INVISIBLE_WALL_FACE"),{0}};
copy(w->id,f.p[1],33);
} else if(same(k,"volume")) {
double v[7];
if(r->nvols==MAX_VOLUMES) return "too many volumes";
if(f.n<10 || !field_uuid(f.p[1]) || !field_label(f.p[2]) || !floats(&f,3,v,7)) return "invalid volume record";
RVol *o=&r->vols[r->nvols++];
*o=(RVol){v[0],v[1],v[2],v[3],v[4],v[5],v[6],{0},{0}};
copy(o->label,f.p[2],sizeof(o->label));copy(o->id,f.p[1],33);
} else if(same(k,"mesh")) {
u64 nv,ni;
if(f.n<5 || !field_uuid(f.p[1]) || !field_u64(f.p[3],&nv) || !field_u64(f.p[4],&ni) ||
nv>MAX_MESH_VERTICES || ni>(u64)MAX_TRIANGLES*3) return "invalid mesh record";
for(const char *c=f.p[2];*c;c++) if(*c=='/' || *c=='\\') return "invalid mesh file name";
copy(r->mesh_id,f.p[1],33);copy(r->mesh_name,f.p[2],sizeof(r->mesh_name));
r->mesh_nv=(u32)nv;r->mesh_ni=(u32)ni;r->has_mesh=1;
} else {
/* Forward compatibility: unknown records (plane, ...) are kept as they were written. */
if(r->nextra+raw_len+1>=MAX_EXTRA_TEXT) return "too many extra room records";
memcpy(r->extra+r->nextra,text+start,raw_len);r->nextra+=raw_len;r->extra[r->nextra++]='\n';
}
}
if(first) return "empty room file";
if(!r->source[0]) copy(r->source,"scan",sizeof(r->source));
if(!r->room_id[0] && !uuid_hex(r->room_id)) return "getrandom failed";
if(!have_floor && !uuid_hex(r->floor_id)) return "getrandom failed";
if(!have_ceiling) { if(!uuid_hex(r->ceiling_id)) return "getrandom failed";r->ceiling=r->floor+2.5; }
if(!r->noutline && r->nwalls>=3) {
double x0=1e10,x1=-1e10,z0=1e10,z1=-1e10;
for(int i=0;i<r->nwalls;i++) {
RWall *w=&r->walls[i];
double lx=w->x0<w->x1 ? w->x0 : w->x1,hx=w->x0<w->x1 ? w->x1 : w->x0;
double lz=w->z0<w->z1 ? w->z0 : w->z1,hz=w->z0<w->z1 ? w->z1 : w->z0;
x0=lx<x0 ? lx : x0;x1=hx>x1 ? hx : x1;z0=lz<z0 ? lz : z0;z1=hz>z1 ? hz : z1;
}
r->outline[0]=(DPt){x0,z0};r->outline[1]=(DPt){x1,z0};r->outline[2]=(DPt){x1,z1};r->outline[3]=(DPt){x0,z1};
r->noutline=4;
}
if(r->noutline<3) return "room has no outline";
double twice=0;
for(int i=0;i<r->noutline;i++) {
DPt a=r->outline[i],b=r->outline[(i+1)%r->noutline];twice+=a.x*b.z-b.x*a.z;
}
if(twice<0) for(int i=0;i<r->noutline/2;i++) { /* readers reverse a negative outline (ROOM_FORMAT.md) */
DPt t=r->outline[i];r->outline[i]=r->outline[r->noutline-1-i];r->outline[r->noutline-1-i]=t;
}
if(!r->nwalls) {
for(int i=0;i<r->noutline;i++) {
DPt a=r->outline[i],b=r->outline[(i+1)%r->noutline];
r->walls[i]=(RWall){a.x,a.z,b.x,b.z,r->floor,r->ceiling,0,{0}};
if(!uuid_hex(r->walls[i].id)) return "getrandom failed";
}
r->nwalls=r->noutline;
}
return 0;
}
/* ------------------------------------------------------------------------------------------------ files */
int join_path(char *out, const char *dir, const char *name)
{
usize n=0;
while(*dir) { if(n>=3900) return 0;out[n++]=*dir++; }
if(n && out[n-1]!='/') out[n++]='/';
while(*name) { if(n>=4095) return 0;out[n++]=*name++; }
out[n]=0;return 1;
}
const char *read_file(const char *path, usize limit, u8 **data, usize *size)
{
*data=0;*size=0;
int fd=io_open(path,0);
if(fd<0) return "cannot open file";
usize cap=limit+1;
u8 *buf=io_alloc(cap);
if(!buf) { io_close(fd);return "file allocation failed"; }
usize n=0;
for(;;) {
long r=io_read(fd,buf+n,cap-n);
if(r<0) { io_close(fd);io_free(buf,cap);return "cannot read file"; }
if(!r) break;
n+=(usize)r;
if(n>limit) { io_close(fd);io_free(buf,cap);return "file too large"; }
}
io_close(fd);
/* The caller frees with io_free(data, limit+1); the size is the buffer, not the content. */
*data=buf;*size=n;return 0;
}
const char *publish(const char *dir, const char *name, const void *data, usize size)
{
char id[33],tmpname[256],final[4096],tmp[4096];
if(!uuid_hex(id)) return "getrandom failed";
usize n=0;
for(const char *p=name;*p && n<180;p++) tmpname[n++]=*p;
tmpname[n++]='.';
for(int i=0;i<32;i++) tmpname[n++]=id[i];
for(const char *p=".tmp";*p;p++) tmpname[n++]=*p;
tmpname[n]=0;
if(!join_path(final,dir,name) || !join_path(tmp,dir,tmpname)) return "output path too long";
int fd=io_open(tmp,1);
if(fd<0) return "cannot open temporary file";
int ok=io_write(fd,data,size);
if(io_close(fd)) ok=0;
if(!ok) { io_remove(tmp);return "cannot write temporary file"; }
if(io_rename(tmp,final)) { io_remove(tmp);return "cannot publish file"; }
return 0;
}
static u32 rd32(const u8 *p) { return (u32)p[0]|(u32)p[1]<<8|(u32)p[2]<<16|(u32)p[3]<<24; }
static void wr32(u8 *p, u32 v) { for(int i=0;i<4;i++) p[i]=(u8)(v>>(8*i)); }
const char *mesh_parse(const u8 *data, usize size, Vec **vertices, u32 **indices, int *nv, int *ni)
{
*vertices=0;*indices=0;*nv=*ni=0;
if(size<16 || data[0]!='F' || data[1]!='P' || data[2]!='R' || data[3]!='M' || rd32(data+4)!=1) return "invalid mesh header";
u32 v=rd32(data+8),i=rd32(data+12);
if(v>MAX_MESH_VERTICES || i>(u32)MAX_TRIANGLES*3 || i%3 || size!=16+(usize)v*12+(usize)i*4) return "invalid mesh size";
Vec *pv=io_alloc((usize)MAX_MESH_VERTICES*sizeof(Vec));
u32 *pi=io_alloc((usize)MAX_TRIANGLES*3*sizeof(u32));
if(!pv || !pi) { io_free(pv,(usize)MAX_MESH_VERTICES*sizeof(Vec));io_free(pi,(usize)MAX_TRIANGLES*3*sizeof(u32));return "mesh allocation failed"; }
for(u32 k=0;k<v*3;k++) {
union { u32 u; float f; } c={rd32(data+16+4*(usize)k)};
if(!finite(c.f)) { io_free(pv,(usize)MAX_MESH_VERTICES*sizeof(Vec));io_free(pi,(usize)MAX_TRIANGLES*3*sizeof(u32));return "non-finite mesh vertex"; }
((float *)pv)[k]=c.f;
}
for(u32 k=0;k<i;k++) {
pi[k]=rd32(data+16+(usize)v*12+4*(usize)k);
if(pi[k]>=v) { io_free(pv,(usize)MAX_MESH_VERTICES*sizeof(Vec));io_free(pi,(usize)MAX_TRIANGLES*3*sizeof(u32));return "mesh index outside vertex array"; }
}
*vertices=pv;*indices=pi;*nv=(int)v;*ni=(int)i;return 0;
}
const char *mesh_bytes(const Vec *vertices, const u32 *indices, int nv, int ni, u8 **data, usize *size)
{
*size=16+(usize)nv*12+(usize)ni*4;
*data=io_alloc(*size);
if(!*data) return "mesh allocation failed";
u8 *p=*data;
p[0]='F';p[1]='P';p[2]='R';p[3]='M';wr32(p+4,1);wr32(p+8,(u32)nv);wr32(p+12,(u32)ni);
/* Both supported targets are little-endian IEEE float32, like the scanner's original writer. */
memcpy(p+16,vertices,(usize)nv*12);
memcpy(p+16+(usize)nv*12,indices,(usize)ni*4);
return 0;
}
/* ------------------------------------------------------------------------------------------------ scan.json */
void heads_json(Buf *b, const Vec *heads, int n)
{
put(b,"[");
for(int i=0;i<n;i++) {
if(i) put(b,",");
put(b,"[");put_dec(b,heads[i].x);put(b,",");put_dec(b,heads[i].y);put(b,",");put_dec(b,heads[i].z);put(b,"]");
}
put(b,"]");
}
int heads_parse(const char *text, usize n, Vec *heads)
{
/* Finds "heads":[[x,y,z],...] in a scan.json written by heads_json; anything else gives no heads. */
const char *key="\"heads\"";
usize at=0;
for(;at+7<=n;at++) {
usize k=0;while(k<7 && text[at+k]==key[k]) k++;
if(k==7) break;
}
if(at+7>n) return 0;
char *p=(char *)text+at+7;const char *end=text+n;
while(p<end && (is_space(*p) || *p=='\n' || *p==':')) p++;
if(p>=end || *p!='[') return 0;
p++;
int count=0;
while(p<end && count<MAX_HEADS) {
while(p<end && (is_space(*p) || *p=='\n' || *p==',')) p++;
if(p>=end || *p==']') break;
if(*p!='[') return 0;
p++;
float v[3];
for(int i=0;i<3;i++) {
while(p<end && (is_space(*p) || *p==',')) p++;
if(!parse_number(&p,&v[i])) return 0;
}
while(p<end && is_space(*p)) p++;
if(p>=end || *p!=']') return 0;
p++;
heads[count++]=vec(v[0],v[1],v[2]);
}
return count;
}
static void key_float(Buf *b, const char *k, float v) { put(b,",\"");put(b,k);put(b,"\":");put_dec(b,v); }
static void key_int(Buf *b, const char *k, i64 v, int first)
{
if(!first) put(b,",");
put(b,"\"");put(b,k);put(b,"\":");put_int(b,v);
}
void settings_json(Buf *b, const Refine *o)
{
static const char *walls[]={"off","snap","panels"};
put(b,"{");key_int(b,"refine",o->refine,1);
put(b,",\"walls\":\"");put(b,walls[o->walls]);put(b,"\"");
key_float(b,"band",o->band);key_float(b,"facing_deg",o->facing_deg);key_float(b,"band_in",o->band_in);
key_float(b,"floor_in",o->floor_in);key_float(b,"ceil_in",o->ceil_in);key_float(b,"top_strip",o->top_strip);
key_float(b,"near",o->near);key_float(b,"clip",o->clip);key_float(b,"clip_open",o->clip_open);
key_float(b,"island",o->island);key_int(b,"smooth",o->smooth,0);key_float(b,"tops",o->tops);
key_int(b,"protect",o->protect,0);key_float(b,"tubes",o->tubes);key_float(b,"tube_thick",o->tube_thick);
key_float(b,"walked",o->walked);key_int(b,"label_rules",o->label_rules,0);
put(b,",\"erase\":[");
for(int i=0;i<o->nerase;i++) {
const Sphere *e=&o->erase[i];
if(i) put(b,",");
put(b,"[");put_dec(b,e->x);put(b,",");put_dec(b,e->y);put(b,",");put_dec(b,e->z);put(b,",");put_dec(b,e->r);put(b,"]");
}
put(b,"]}");
}
void report_json(Buf *b, const Report *r, int nlabels, int changed, int removed, int nsuggest)
{
static const char *kinds[]={"wall","floor","ceiling"};
put(b,"{");key_int(b,"triangles",r->triangles,1);key_int(b,"kept",r->kept,0);
key_int(b,"structural",r->structural,0);key_int(b,"near_walls",r->near,0);key_int(b,"outside",r->outside,0);
key_int(b,"small_pieces",r->islands,0);key_int(b,"thin_pieces",r->tubes,0);key_int(b,"erased",r->erased,0);
key_int(b,"walked",r->walked,0);key_int(b,"tops",r->tops,0);key_int(b,"panel_triangles",r->panels,0);
key_int(b,"truncated",r->truncated,0);
put(b,",\"walked_skipped\":");put(b,r->walked_skipped ? "true" : "false");
key_float(b,"floor_y",r->floor_y);key_float(b,"ceiling_y",r->ceiling_y);
key_int(b,"boxes",nlabels,0);key_int(b,"relabelled",changed,0);key_int(b,"removed_boxes",removed,0);
key_int(b,"suggestions",nsuggest,0);
put(b,",\"plane_shifts\":[");
for(int i=0;i<r->nshifts;i++) {
if(i) put(b,",");
put(b,"{\"kind\":\"");put(b,kinds[r->shifts[i].kind]);put(b,"\"");
if(!r->shifts[i].kind) { put(b,",\"wall\":");put_int(b,r->shifts[i].wall); }
key_float(b,"shift",r->shifts[i].shift);put(b,"}");
}
put(b,"]}");
}
-255
View File
@@ -1,255 +0,0 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Label rules on the scanner's furniture boxes (the owner's tuned rules, 2026-10-18): boxes outside the room,
* boxes that merged several things, boxes nested in a bigger one are removed; the rest are relabelled BED, COUCH,
* TABLE, STORAGE, SCREEN or OTHER by shape; tall thin slabs from the floor become door/closet suggestions (never
* applied automatically). Every decision is reported with a reason in scan.json. */
#include "refine.h"
#define R_INSIDE .1f /* m: a centre further outside the outline is behind a wall */
#define R_NEAR_WALL .35f
#define R_BED_AREA 2.f
#define R_BED_LONG 1.8f
#define R_BED_MAX 3.f
#define R_BED_TOP .65f
#define R_BED_MIN_TOP .25f
#define R_COUCH_MIN_TOP .35f
#define R_COUCH_TOP .6f
#define R_COUCH_LONG 1.4f
#define R_TABLE_MIN .6f
#define R_TABLE_MAX 1.1f
#define R_STORAGE_H 1.1f
#define R_STORAGE_D .25f
#define R_SCREEN_BOTTOM .3f
#define R_SCREEN_TOP 2.2f
#define R_SCREEN_ASPECT 1.2f
#define R_SCREEN_PARALLEL 15.f /* degrees off the nearest wall */
#define R_SCREEN_FLAT .03f /* m: spread of the scan's depth inside the box */
#define R_THIN .22f
#define R_TALL_MIN 1.8f
#define R_TALL_FLOOR .3f /* m: a tall slab's bottom at most this far above the floor */
#define R_TALL_BEHIND .5f
#define R_DOOR_MAX 1.1f
#define R_MERGE .6f /* share of a box's footprint inside another that counts as held by it */
#define R_MERGE_APART 1.f
#define R_NESTED .75f
#define FLAT_MIN_TRIANGLES 30
#define FLAT_DEPTH_PAD .05f
#define FOOTPRINT_STEP .05
typedef struct { double cs, sn; } Rot;
static Rot rot(const RVol *v) { double a=v->yaw*3.14159265358979323846/180; return (Rot){dcos(a),dsin(a)}; }
static void local(const RVol *v, Rot r, double x, double z, double *lx, double *lz)
{
double dx=x-v->cx,dz=z-v->cz;
*lx=r.cs*dx-r.sn*dz;*lz=r.sn*dx+r.cs*dz;
}
static double dabs(double x) { return x<0 ? -x : x; }
static void corners(const RVol *v, Rot r, double out[4][2])
{
static const int sg[4][2]={{-1,-1},{1,-1},{1,1},{-1,1}};
for(int k=0;k<4;k++) {
double x=sg[k][0]*v->w/2.,z=sg[k][1]*v->d/2.;
out[k][0]=v->cx+r.cs*x+r.sn*z;out[k][1]=v->cz-r.sn*x+r.cs*z;
}
}
static int point_in_poly(double x, double z, const DPt *poly, int n)
{
int inside=0;
for(int i=0,j=n-1;i<n;j=i++) {
double xi=poly[i].x,zi=poly[i].z,xj=poly[j].x,zj=poly[j].z;
if((zi>z)!=(zj>z) && x<(xj-xi)*(z-zi)/(zj-zi+1e-12)+xi) inside=!inside;
}
return inside;
}
typedef struct { double dist, t, L, ux, uz; int i; } Near;
static Near nearest_wall(const Room *r, double x, double z)
{
Near best={1e300,0,0,1,0,-1};
for(int i=0;i<r->nwalls;i++) {
const RWall *w=&r->walls[i];
double abx=w->x1-w->x0,abz=w->z1-w->z0,l2=abx*abx+abz*abz,L=dhypot(abx,abz);
if(!(l2>0)) continue;
double t=((x-w->x0)*abx+(z-w->z0)*abz)/l2;
t=t<0 ? 0 : t>1 ? 1 : t;
double dx=x-(w->x0+t*abx),dz=z-(w->z0+t*abz),d=dhypot(dx,dz);
if(d<best.dist) best=(Near){d,t,L,abx/L,abz/L,i};
}
return best;
}
static double outside(const Room *r, double x, double z)
{
return point_in_poly(x,z,r->outline,r->noutline) ? 0 : nearest_wall(r,x,z).dist;
}
/* Spread (std) of the scan's depth inside a thin upright box; <0 = too little scan to tell. */
static double flatness(const Prep *p, const RVol *v)
{
Rot r=rot(v);
double sum=0,sum2=0;int n=0;
for(int pass=0;pass<2;pass++) {
double mean=n ? sum/n : 0;
for(int t=0;t<p->T;t++) {
double y=p->cen[3*t+1];
if(y>v->top || y<v->top-v->h) continue;
double lx,lz;local(v,r,p->cen[3*t],p->cen[3*t+2],&lx,&lz);
double a=lx,b=lz,half=v->w/2.,depth=v->d/2.+FLAT_DEPTH_PAD;
if(v->d>v->w) { a=lz;b=lx;half=v->d/2.;depth=v->w/2.+FLAT_DEPTH_PAD; }
if(dabs(a)<=half && dabs(b)<=depth) {
if(!pass) { sum+=b;n++; } else sum2+=(b-mean)*(b-mean);
}
}
if(!pass && n<FLAT_MIN_TRIANGLES) return -1;
}
return droot(sum2/n);
}
static void cm(Buf *b, double x) { put_round(b,x*100);put(b," cm"); }
static void lower(Buf *b, const char *s)
{
char c[2]={0,0};
for(;*s;s++) { c[0]=*s>='A' && *s<='Z' ? (char)(*s+32) : *s;put(b,c); }
}
static void set_text(char *dst, usize cap, Buf *b)
{
usize n=b->ok ? b->n : 0;
if(n>=cap) { /* cut at a UTF-8 boundary */
n=cap-1;
while(n && ((u8)b->data[n]&0xc0)==0x80) n--;
}
if(n) memcpy(dst,b->data,n);
dst[n]=0;
}
static void copy(char *dst, const char *src, usize cap)
{
usize i=0;while(src[i] && i+1<cap) { dst[i]=src[i];i++; }
dst[i]=0;
}
/* label "" = remove, suggest "door"/"closet" or "" */
static void classify(const Room *room, const Prep *p, const RVol *v, char *label, char *suggest, Buf *why)
{
double out=outside(room,v->cx,v->cz);
double top=v->top-room->floor,bottom=top-v->h,lng=v->w>v->d ? v->w : v->d,shrt=v->w<v->d ? v->w : v->d;
label[0]=suggest[0]=0;
if(shrt<R_THIN && v->h>=R_TALL_MIN && bottom<=R_TALL_FLOOR && out<=R_TALL_BEHIND) {
int door=lng<=R_DOOR_MAX;
copy(suggest,door ? "door" : "closet",8);
put(why,"tall thin slab from the floor (");put_fixed(why,v->h,2);put(why," m)");
if(out>0) { put(why,", ");cm(why,out);put(why," behind the wall line"); }
put(why,door ? ": a door or closet door?" : ": a closet front?");
return;
}
if(out>R_INSIDE) { put(why,"centre ");cm(why,out);put(why," outside the room (behind a wall)");return; }
Near wall=nearest_wall(room,v->cx,v->cz);
if(shrt<R_THIN) {
Rot r=rot(v);
double ax=v->w>=v->d ? r.cs : r.sn,az=v->w>=v->d ? -r.sn : r.cs;
double c=dabs(ax*wall.ux+az*wall.uz);c=c<1 ? c : 1;
double off=angle((float)droot(1-c*c),(float)c)*180/PI;
double fl=flatness(p,v);
Buf b;buf_init(&b,1024);int n=0;
if(bottom<R_SCREEN_BOTTOM) { put(&b,"starts ");cm(&b,bottom>0 ? bottom : 0);put(&b," off the floor");n++; }
if(top>R_SCREEN_TOP) { if(n++) put(&b,", ");put(&b,"top at ");put_fixed(&b,top,2);put(&b," m"); }
if(lng/v->h<R_SCREEN_ASPECT) { if(n++) put(&b,", ");put(&b,"taller than wide (");put_fixed(&b,lng/v->h,2);put(&b,")"); }
if(wall.dist<R_NEAR_WALL && off>R_SCREEN_PARALLEL) { if(n++) put(&b,", ");put_round(&b,off);put(&b,"\xc2\xb0 off the wall"); }
if(fl>=0 && fl>R_SCREEN_FLAT) { if(n++) put(&b,", ");put(&b,"not flat (");cm(&b,fl);put(&b," spread)"); }
if(!n) {
copy(label,"SCREEN",MAX_LABEL);put(why,"thin, raised, screen-shaped");
if(fl>=0) { put(why,", flat (");cm(why,fl);put(why,")"); }
} else { put(why,"thin upright slab: ");put_n(why,b.data,b.n); }
buf_free(&b);
return;
}
if(top<=R_BED_TOP && v->w*v->d>=R_BED_AREA && lng>=R_BED_LONG && lng<=R_BED_MAX && top>=R_BED_MIN_TOP) {
copy(label,"BED",MAX_LABEL);put_fixed(why,(double)v->w*v->d,1);put(why," m\xc2\xb2, ");put_fixed(why,lng,1);
put(why," m long, top ");put_fixed(why,top,2);put(why," m");return;
}
if(top>=R_COUCH_MIN_TOP && top<=R_COUCH_TOP && lng>=R_COUCH_LONG) {
copy(label,"COUCH",MAX_LABEL);put(why,"seat-height top ");put_fixed(why,top,2);put(why," m, ");
put_fixed(why,lng,1);put(why," m long");return;
}
if(top>=R_TABLE_MIN && top<=R_TABLE_MAX) { copy(label,"TABLE",MAX_LABEL);put(why,"top ");put_fixed(why,top,2);put(why," m");return; }
if(v->h>=R_STORAGE_H && shrt>=R_STORAGE_D && wall.dist<R_NEAR_WALL) {
copy(label,"STORAGE",MAX_LABEL);put_fixed(why,v->h,2);put(why," m tall by a wall");return;
}
if(top>R_TABLE_MAX) { put_fixed(why,top,2);put(why," m tall, matches nothing");return; }
copy(label,"OTHER",MAX_LABEL);put(why,"top ");put_fixed(why,top,2);put(why," m, ");put_fixed(why,lng,1);
put(why," \xc3\x97 ");put_fixed(why,shrt,1);put(why," m");
}
/* Share of a's and b's footprint they have in common, sampled on a 5 cm grid over both (as the reference). */
static void footprint_overlap(const RVol *a, const RVol *b, double *fa, double *fb)
{
Rot ra=rot(a),rb=rot(b);
double ca[4][2],cb[4][2],x0=1e300,x1=-1e300,z0=1e300,z1=-1e300;
corners(a,ra,ca);corners(b,rb,cb);
for(int k=0;k<4;k++) {
x0=ca[k][0]<x0 ? ca[k][0] : x0;x1=ca[k][0]>x1 ? ca[k][0] : x1;z0=ca[k][1]<z0 ? ca[k][1] : z0;z1=ca[k][1]>z1 ? ca[k][1] : z1;
x0=cb[k][0]<x0 ? cb[k][0] : x0;x1=cb[k][0]>x1 ? cb[k][0] : x1;z0=cb[k][1]<z0 ? cb[k][1] : z0;z1=cb[k][1]>z1 ? cb[k][1] : z1;
}
double na=0,nb=0,both=0;
for(double x=x0;x<=x1;x+=FOOTPRINT_STEP) for(double z=z0;z<=z1;z+=FOOTPRINT_STEP) {
double ax,az,bx,bz;local(a,ra,x,z,&ax,&az);local(b,rb,x,z,&bx,&bz);
int ia=dabs(ax)<=a->w/2. && dabs(az)<=a->d/2.,ib=dabs(bx)<=b->w/2. && dabs(bz)<=b->d/2.;
na+=ia;nb+=ib;both+=ia && ib;
}
*fa=both/(na>1 ? na : 1);*fb=both/(nb>1 ? nb : 1);
}
static int boxes_apart(const RVol *a, const RVol *b)
{
/* footprints whose bounding circles don't touch share nothing (skips the sampling, same result) */
double ra=droot((double)a->w*a->w+(double)a->d*a->d)/2,rb=droot((double)b->w*b->w+(double)b->d*b->d)/2;
double dx=a->cx-b->cx,dz=a->cz-b->cz;
return dx*dx+dz*dz>(ra+rb+.1)*(ra+rb+.1);
}
int rules_apply(const Room *room, const Prep *p, RVol *out, int *nout, Label *labels)
{
int n=room->nvols;const RVol *box=room->vols;
typedef struct { int i, j; double fa, fb; } Ov;
static Ov ov[MAX_VOLUMES*(MAX_VOLUMES-1)/2];
int nov=0,removed[MAX_VOLUMES]={0};
static char reason[MAX_VOLUMES][MAX_REASON];
for(int i=0;i<n;i++) for(int j=i+1;j<n;j++) {
if(boxes_apart(&box[i],&box[j])) continue;
double fa,fb;footprint_overlap(&box[i],&box[j],&fa,&fb);
if(fa>0 || fb>0) ov[nov++]=(Ov){i,j,fa,fb};
}
/* merged: a box that holds most of two boxes far apart covers several things */
for(int k=0;k<n;k++) {
int held[MAX_VOLUMES],nh=0;
for(int q=0;q<nov;q++) if(ov[q].i==k && ov[q].fb>=R_MERGE) held[nh++]=ov[q].j;
for(int q=0;q<nov;q++) if(ov[q].j==k && ov[q].fa>=R_MERGE) held[nh++]=ov[q].i;
for(int x=0;x<nh && !removed[k];x++) for(int y=0;y<nh && !removed[k];y++) {
const RVol *a=&box[held[x]],*b=&box[held[y]];
double dx=a->cx-b->cx,dz=a->cz-b->cz,d=dhypot(dx,dz);
if(held[x]<held[y] && d>=R_MERGE_APART) {
Buf t;buf_init(&t,1024);
put(&t,"holds a ");lower(&t,a->label);put(&t," and a ");lower(&t,b->label);put(&t," ");
put_fixed(&t,d,1);put(&t," m apart: several things merged");
set_text(reason[k],MAX_REASON,&t);buf_free(&t);removed[k]=1;
}
}
}
/* nested: mostly inside a bigger box with a lower top = part of it */
for(int q=0;q<nov;q++) for(int side=0;side<2;side++) {
int s=side ? ov[q].j : ov[q].i,g=side ? ov[q].i : ov[q].j;double f=side ? ov[q].fb : ov[q].fa;
const RVol *S=&box[s],*G=&box[g];
if(removed[s] || removed[g] || f<R_NESTED || (double)S->w*S->d>=(double)G->w*G->d || S->top>G->top+.05) continue;
Buf t;buf_init(&t,1024);
put_round(&t,f*100);put(&t,"% inside the bigger ");lower(&t,G->label);put(&t," box: part of it");
set_text(reason[s],MAX_REASON,&t);buf_free(&t);removed[s]=1;
}
int count=0;
for(int i=0;i<n;i++) {
Label *l=&labels[i];
memset(l,0,sizeof(*l));
copy(l->from,box[i].label,MAX_LABEL);l->box=box[i];l->wall=-1;
if(removed[i]) { copy(l->reason,reason[i],MAX_REASON);continue; }
Buf why;buf_init(&why,1024);
classify(room,p,&box[i],l->to,l->suggest,&why);
set_text(l->reason,MAX_REASON,&why);buf_free(&why);
if(l->suggest[0]) {
Near nw=nearest_wall(room,box[i].cx,box[i].cz);
l->wall=nw.i;l->along=(float)nw.t;
}
if(l->to[0]) { out[count]=box[i];copy(out[count].label,l->to,MAX_LABEL);count++; }
}
*nout=count;
return n;
}
+2 -17
View File
@@ -13,8 +13,6 @@
#define MAX_VOLUMES 128
#define MAX_TRIANGLES 200000
#define MAX_MESH_VERTICES 600000
#define MAX_HEADS 2000
#define MAX_ERASE 64
#define PI 3.14159265358979323846f
typedef struct { float x, y, z; } Vec;
@@ -31,17 +29,8 @@ typedef struct {
int used;
} Plane;
typedef struct { Vec p; float yaw, w, d, h; const char *label; } Volume;
typedef struct { float x, y, z, r; } Sphere;
/* Refinement + label rules (refine.c, rules.c); defaults = the owner's tuned settings (2026-10-18). */
typedef struct {
int refine, walls, protect, label_rules, smooth, nerase; /* walls: 0 off, 1 snap, 2 panels */
float band, facing_deg, band_in, floor_in, ceil_in, top_strip, near, clip, clip_open, island, tops;
float tubes, tube_thick, walked;
Sphere erase[MAX_ERASE];
} Refine;
typedef struct {
const char *input, *out, *hint, *refine_only;
Refine rf;
const char *input, *out, *hint;
u64 universe;
float voxel, ceiling_default, truncation, max_edge, grazing, range, depth_jump, depth_ratio, min_views;
float patch_residual;
@@ -67,13 +56,10 @@ typedef struct {
const char *warnings[24];
int nwarnings, hint_used;
u32 rng;
Vec heads[MAX_HEADS]; /* head path of tracked frames; the stride doubles whenever the buffer is full */
int nheads;
unsigned head_stride, head_frames;
Vec *vertices;
u32 *indices;
int nv, ni;
i64 t0, tread, tfuse, tmodel, tmesh, trefine;
i64 t0, tread, tfuse, tmodel, tmesh;
} Scan;
static inline float ab(float x) { return x < 0 ? -x : x; }
@@ -103,6 +89,5 @@ void finish_fusion(Scan *s);
const char *make_model(Scan *s);
const char *make_mesh(Scan *s);
int same(const char *a, const char *b);
int parse_number(char **cursor, float *value);
#endif
-40
View File
@@ -1,40 +0,0 @@
/* SPDX-License-Identifier: MIT */
#include <openxr/openxr.h>
#include <openxr/openxr_loader_negotiation.h>
#include <assert.h>
#include <string.h>
extern XrResult xrNegotiateLoaderRuntimeInterface(const XrNegotiateLoaderInfo *, XrNegotiateRuntimeRequest *);
extern XrResult xrInitializeLoaderKHR(const XrLoaderInitInfoBaseHeaderKHR *);
int main(int argc, char **argv) {
XrNegotiateLoaderInfo li = {XR_LOADER_INTERFACE_STRUCT_LOADER_INFO, XR_LOADER_INFO_STRUCT_VERSION,
sizeof(li), 1, 1, XR_MAKE_VERSION(1,0,0), XR_CURRENT_API_VERSION};
XrNegotiateRuntimeRequest r = {XR_LOADER_INTERFACE_STRUCT_RUNTIME_REQUEST, XR_RUNTIME_INFO_STRUCT_VERSION, sizeof(r)};
assert(xrNegotiateLoaderRuntimeInterface(NULL, &r) < 0);
assert(xrNegotiateLoaderRuntimeInterface(&li, NULL) < 0);
li.minInterfaceVersion = 2;
assert(xrNegotiateLoaderRuntimeInterface(&li, &r) < 0);
li.minInterfaceVersion = 1;
r.structSize--;
assert(xrNegotiateLoaderRuntimeInterface(&li, &r) < 0);
r.structSize++;
li.minApiVersion = XR_MAKE_VERSION(2,0,0);
assert(xrNegotiateLoaderRuntimeInterface(&li, &r) < 0);
li.minApiVersion = XR_MAKE_VERSION(1,0,0);
li.minApiVersion = XR_MAKE_VERSION(1,0,1);
li.maxApiVersion = XR_MAKE_VERSION(1,0,0);
assert(xrNegotiateLoaderRuntimeInterface(&li, &r) < 0);
li.minApiVersion = XR_MAKE_VERSION(1,0,0);
li.maxApiVersion = XR_CURRENT_API_VERSION;
XrResult result = xrNegotiateLoaderRuntimeInterface(&li, &r);
if (argc > 1 && !strcmp(argv[1], "missing")) {
assert(result == XR_ERROR_INITIALIZATION_FAILED);
return 0;
}
assert(result == XR_SUCCESS && r.getInstanceProcAddr && r.runtimeInterfaceVersion == 1);
assert(r.runtimeApiVersion <= li.maxApiVersion && r.runtimeApiVersion >= li.minApiVersion);
XrLoaderInitInfoBaseHeaderKHR info = {XR_TYPE_LOADER_INIT_INFO_ANDROID_KHR, (void *)0x1234};
assert(xrInitializeLoaderKHR(&info) == XR_SUCCESS);
info.next = NULL;
assert(xrInitializeLoaderKHR(&info) == XR_ERROR_RUNTIME_FAILURE);
return 0;
}
-16
View File
@@ -1,16 +0,0 @@
/* SPDX-License-Identifier: MIT */
#include <openxr/openxr.h>
#include <string.h>
static XrResult init(const XrLoaderInitInfoBaseHeaderKHR *info) {
return info->type == XR_TYPE_LOADER_INIT_INFO_ANDROID_KHR && info->next == (void *)0x1234 ?
XR_SUCCESS : XR_ERROR_RUNTIME_FAILURE;
}
XrResult xrGetInstanceProcAddr(XrInstance instance, const char *name, PFN_xrVoidFunction *fn) {
(void)instance;
if (!strcmp(name, "xrInitializeLoaderKHR")) {
*fn = (PFN_xrVoidFunction)init;
return XR_SUCCESS;
}
*fn = NULL;
return XR_ERROR_FUNCTION_UNSUPPORTED;
}
-60
View File
@@ -1,60 +0,0 @@
/* SPDX-License-Identifier: MIT
* Runtime endpoint for an application's embedded Khronos loader.
* Only the application's loader paths change: the inner loader still finds SteamVR.
*/
#include <openxr/openxr.h>
#include <openxr/openxr_loader_negotiation.h>
#include <dlfcn.h>
#include <pthread.h>
#include <string.h>
#ifdef __ANDROID__
#include <android/log.h>
#define LOG(...) __android_log_print(ANDROID_LOG_INFO, "fp_xrruntime", __VA_ARGS__)
#else
#include <stdio.h>
#define LOG(...) do { fprintf(stderr, "fp_xrruntime: "); fprintf(stderr, __VA_ARGS__); fputc('\n', stderr); } while (0)
#endif
#define EXPORT __attribute__((visibility("default")))
static pthread_once_t once = PTHREAD_ONCE_INIT;
static PFN_xrGetInstanceProcAddr chain;
static void resolve(void) {
/* RTLD_LOCAL and handle-specific dlsym prevent binding to the game's own xr* exports. */
void *bridge = dlopen("libopenxr_loader_generic.so", RTLD_NOW | RTLD_LOCAL);
if (bridge) chain = (PFN_xrGetInstanceProcAddr)dlsym(bridge, "xrGetInstanceProcAddr");
if (chain) LOG("chain resolved: FrameBridge");
else LOG("chain resolution failed: %s", dlerror());
}
EXPORT XrResult XRAPI_CALL xrInitializeLoaderKHR(const XrLoaderInitInfoBaseHeaderKHR *info) {
pthread_once(&once, resolve);
if (!chain || !info) return XR_ERROR_INITIALIZATION_FAILED;
PFN_xrVoidFunction proc = NULL;
XrResult result = chain(XR_NULL_HANDLE, "xrInitializeLoaderKHR", &proc);
if (XR_SUCCEEDED(result) && proc)
result = ((PFN_xrInitializeLoaderKHR)proc)(info);
else if (XR_SUCCEEDED(result)) result = XR_ERROR_INITIALIZATION_FAILED;
LOG("xrInitializeLoaderKHR forwarded to FrameBridge: %d", result);
return result;
}
EXPORT XrResult XRAPI_CALL xrNegotiateLoaderRuntimeInterface(
const XrNegotiateLoaderInfo *loader, XrNegotiateRuntimeRequest *runtime) {
if (!loader || !runtime ||
loader->structType != XR_LOADER_INTERFACE_STRUCT_LOADER_INFO ||
loader->structVersion != XR_LOADER_INFO_STRUCT_VERSION || loader->structSize != sizeof(*loader) ||
runtime->structType != XR_LOADER_INTERFACE_STRUCT_RUNTIME_REQUEST ||
runtime->structVersion != XR_RUNTIME_INFO_STRUCT_VERSION || runtime->structSize != sizeof(*runtime) ||
loader->minInterfaceVersion > 1 || loader->maxInterfaceVersion < 1 ||
loader->minApiVersion > loader->maxApiVersion ||
loader->minApiVersion > XR_CURRENT_API_VERSION || loader->maxApiVersion < XR_MAKE_VERSION(1, 0, 0))
return XR_ERROR_INITIALIZATION_FAILED;
pthread_once(&once, resolve);
if (!chain) return XR_ERROR_INITIALIZATION_FAILED;
runtime->runtimeInterfaceVersion = 1;
runtime->runtimeApiVersion = loader->maxApiVersion < XR_CURRENT_API_VERSION ?
loader->maxApiVersion : XR_CURRENT_API_VERSION;
runtime->getInstanceProcAddr = chain;
LOG("negotiated runtime interface 1: FrameBridge");
return XR_SUCCESS;
}
+2 -2
View File
@@ -76,8 +76,8 @@ def images() -> dict[str, bytes]:
"every wall and your furniture.", seen=n, accent=SECONDARY)
for n in range(DIRECTIONS + 1): # everything seen before the minimum scan time: the ring is the time left
out[f"finish_{n:02d}"] = panel("Almost done", "Walls, ceiling and floor are covered.\n"
"Look closely at your furniture\nuntil the ring fills.", seen=n, accent=OK,
ring=OK, label=f"{(DIRECTIONS - n) * 5}s")
"Look closely at your furniture\nuntil the ring fills.", seen=n, accent=OK, ring=OK,
label=f"{(DIRECTIONS - n) * 5}s")
data = {}
for key, img in out.items():
buf = io.BytesIO()
+2 -6
View File
@@ -12,10 +12,6 @@ BOX_OUTLINE = [(-2, -1.5), (2, -1.5), (2, 1.5), (-2, 1.5)]
L_OUTLINE = [(-2, -2), (2, -2), (2, 0), (0, 0), (0, 2), (-2, 2)]
TABLE = ((-.6, 0, -.25), (1.2, .75, .7))
COUCH = ((.6, 0, .95), (1.6, .65, .65))
# Clutter for the refinement tests: a thin pole floating in the air (a stereo streak) and a column where the user
# stood (a "ghost" of the user's own body or a passer-by in the depth stream).
TUBE = ((1.3, 1.35, -.5), (.12, .12, 1.4))
GHOST = ((.35, .45, 0), (.3, .9, .3))
ROOMVIEW_WIDTH, ROOMVIEW_HEIGHT = 132, 128
ROOMVIEW_NEAR, ROOMVIEW_FAR = .3, 4.0
@@ -108,7 +104,7 @@ def cast(eye, direction, polygon, ceiling, furniture, missing_wall):
def render(path: Path, *, shape="box", frames=30, width=None, height=None, noise=.004,
holes=.025, smear=.012, floor_bias=.008, no_ceiling=False, seed=7,
yaw=0.0, offset=(0.0, 0.0, 0.0), missing_wall=-1, objects=True, profile="roomview",
invalid_start=None, smoothing=3, extra=()):
invalid_start=None, smoothing=3):
"""Each frame has its own moving pose; no ground-truth room data is put in the dump."""
rng = random.Random(seed)
roomview = profile == "roomview"
@@ -117,7 +113,7 @@ def render(path: Path, *, shape="box", frames=30, width=None, height=None, noise
invalid_start = (2 if roomview else 0) if invalid_start is None else invalid_start
polygon = BOX_OUTLINE if shape == "box" else L_OUTLINE
ceiling = 2.6
furniture = ([TABLE, COUCH] if objects and shape == "box" else []) + list(extra)
furniture = [TABLE, COUCH] if objects and shape == "box" else []
rotation = (0, math.sin(yaw / 2), 0, math.cos(yaw / 2))
rays = list(fan(width, height, roomview))
with path.open("wb") as out:
+48 -111
View File
@@ -8,7 +8,6 @@ Quest builds are "installed" (the 8 largest).
from __future__ import annotations
import json
import math
import os
import random
import threading
@@ -31,98 +30,61 @@ def frame_state() -> dict:
return {}
def fake_room_mesh(room, cell: float = 0.2, seed: int = 7):
def fake_room_mesh(room, cell: float = 0.25, seed: int = 7):
"""A made-up scan of `room`: its floor, walls and furniture as noisy triangle grids, wound like a real scan's
(counter-clockwise seen from inside the room). Like a real scan it has holes where the headset sees nothing:
the floor under furniture that stands on it, and window glass."""
from frameport.mr.room import Mesh, plane_corners
(counter-clockwise seen from inside the room)."""
from frameport.mr.room import Mesh
from frameport.mr.view3d import box_corners
rnd = random.Random(seed)
verts, idx = [], []
def surface(o, u, v, noise=0.012, keep=None): # o + s*u + t*v, normal = u x v; keep(point) False = a hole
def surface(o, u, v): # o + s*u + t*v, normal = u x v
lu = max(1, round((sum(c * c for c in u) ** 0.5) / cell))
lv = max(1, round((sum(c * c for c in v) ** 0.5) / cell))
base = len(verts)
for i in range(lv + 1):
for j in range(lu + 1):
n = noise if 0 < i < lv and 0 < j < lu else 0.0
n = 0.015 if 0 < i < lv and 0 < j < lu else 0.0
verts.append(tuple(o[k] + u[k] * j / lu + v[k] * i / lv + rnd.uniform(-n, n) for k in range(3)))
for i in range(lv):
for j in range(lu):
mid = tuple(o[k] + u[k] * (j + 0.5) / lu + v[k] * (i + 0.5) / lv for k in range(3))
if keep is not None and not keep(mid):
continue
a = base + i * (lu + 1) + j
b, c, d = a + 1, a + lu + 1, a + lu + 2
idx.extend((a, b, d, a, d, c))
def inside(vol, x, z): # (x, z) on the box's footprint
a = math.radians(vol.yaw_deg)
dx, dz = x - vol.cx, z - vol.cz
lx, lz = math.cos(a) * dx - math.sin(a) * dz, math.sin(a) * dx + math.cos(a) * dz
return abs(lx) < vol.w / 2 and abs(lz) < vol.d / 2
y0 = room.floor
standing = [v for v in room.volumes if v.top_y - v.h - y0 < 0.05 and v.h > 0.3] # on the floor, no gap below
windows = [plane_corners(p) for p in room.planes if p.label == "WINDOW_FRAME"]
def floor_seen(p):
return not any(inside(v, p[0], p[2]) for v in standing)
def wall_seen(p): # not through a window's glass
for c in windows:
xs, ys, zs = [q[0] for q in c], [q[1] for q in c], [q[2] for q in c]
if (min(ys) + 0.05 < p[1] < max(ys) - 0.05 and min(xs) - 0.05 <= p[0] <= max(xs) + 0.05
and min(zs) - 0.05 <= p[2] <= max(zs) + 0.05):
return False
return True
xs, zs = [x for x, _ in room.outline], [z for _, z in room.outline]
surface((min(xs), y0, max(zs)), (max(xs) - min(xs), 0, 0), (0, 0, min(zs) - max(zs)), 0.008,
floor_seen) # floor, facing up
pts = room.outline
xs, zs = [x for x, _ in pts], [z for _, z in pts]
surface((min(xs), y0, max(zs)), (max(xs) - min(xs), 0, 0), (0, 0, min(zs) - max(zs))) # floor, facing up
for w in room.walls:
if w.label == "INVISIBLE_WALL_FACE":
continue
surface((w.x0, w.y0, w.z0), (w.x1 - w.x0, 0, w.z1 - w.z0), (0, w.y1 - w.y0, 0), 0.012,
wall_seen) # facing into the room
surface((w.x0, w.y0, w.z0), (w.x1 - w.x0, 0, w.z1 - w.z0), (0, w.y1 - w.y0, 0)) # facing into the room
for vol in room.volumes:
c = box_corners(vol.cx, vol.cz, vol.yaw_deg, vol.w, vol.d)
top, bottom = vol.top_y, vol.top_y - vol.h
soft = 0.03 if vol.label in ("BED", "COUCH") else 0.015 # (a duvet isn't flat)
surface((c[0][0], top, c[0][1]), (c[3][0] - c[0][0], 0, c[3][1] - c[0][1]),
(c[1][0] - c[0][0], 0, c[1][1] - c[0][1]), soft) # top, facing up
(c[1][0] - c[0][0], 0, c[1][1] - c[0][1])) # top, facing up
for i in range(4):
a, b = c[i], c[(i + 1) % 4]
surface((a[0], bottom, a[1]), (0, top - bottom, 0), (b[0] - a[0], 0, b[1] - a[1]), 0.012) # facing out
surface((a[0], bottom, a[1]), (0, top - bottom, 0), (b[0] - a[0], 0, b[1] - a[1])) # facing out
return Mesh(vertices=verts, indices=idx)
def fake_room():
"""The pretend Frame's scanned room, made up from scratch (no real scan): a 3.6 x 3.2 m bedroom with a bed and a
nightstand under a wall-mounted TV, a closet, a desk with two monitors under the window, a door, a lamp and a
plant. Walls: 0 = the bed's (-Z, forward), 1 = right (+X), 2 = behind (+Z), 3 = the desk's and the door's (-X);
the 3D view looks at walls 0 and 3."""
from frameport.mr.room import Volume, box_room, plane_on_wall
"""The pretend Frame's scanned room: a 5 x 4 m living room with a couch, a table, a TV and shelves."""
from frameport.mr.room import Volume, box_room
room = box_room(3.6, 3.2, 2.5)
room = box_room(5, 4, 2.6)
room.source, room.universe, room.time = "scan", 7, int(SHOTS_AT)
room.walls = [type(w)(f"{0xa0 + i:032x}", w.x0, w.z0, w.x1, w.z1, w.y0, w.y1, w.label)
for i, w in enumerate(room.walls)] # (fixed ids: the same room in every render)
room.uuid, room.floor_uuid, room.ceiling_uuid = (f"{0xf0 + i:032x}" for i in range(3))
room.walls[1].label = "INVISIBLE_WALL_FACE" # an open side (a doorway the scan closed)
vid = iter(f"{i:032x}" for i in range(1, 99))
room.volumes = [Volume(next(vid), "BED", 0.6, -0.58, 0.55, 0, 1.45, 2.05, 0.55),
Volume(next(vid), "SCREEN", 0.6, -1.55, 1.78, 0, 1.12, 0.07, 0.66), # TV above the bed
Volume(next(vid), "TABLE", -1.44, -0.25, 0.75, 90, 1.4, 0.7, 0.75), # desk
Volume(next(vid), "SCREEN", -1.6, -0.56, 1.2, 82, 0.6, 0.06, 0.36), # monitors on it
Volume(next(vid), "SCREEN", -1.6, 0.06, 1.2, 98, 0.6, 0.06, 0.36),
Volume(next(vid), "STORAGE", -1.25, -1.3, 2.1, 0, 1.0, 0.6, 2.1), # closet
Volume(next(vid), "TABLE", -0.42, -1.38, 0.52, 0, 0.45, 0.4, 0.52), # nightstand
Volume(next(vid), "LAMP", -0.42, -1.42, 0.9, 0, 0.2, 0.2, 0.38),
Volume(next(vid), "PLANT", 1.56, -1.37, 0.95, 0, 0.36, 0.36, 0.95)] # in the corner
room.planes = [plane_on_wall(room, 3, 0.17, 0.0, 0.85, 2.05, "DOOR_FRAME", f"{0xd0:032x}"),
plane_on_wall(room, 3, 0.58, 1.22, 1.05, 1.05, "WINDOW_FRAME", f"{0xd1:032x}")]
room.volumes = [Volume(next(vid), "COUCH", -1.2, 1.3, 0.85, 0, 2.2, 0.9, 0.85),
Volume(next(vid), "TABLE", -0.9, 0.1, 0.45, 0, 1.1, 0.6, 0.45),
Volume(next(vid), "SCREEN", -1.0, -1.85, 1.5, 0, 1.4, 0.12, 0.8),
Volume(next(vid), "STORAGE", 1.9, -1.4, 1.8, 90, 1.2, 0.4, 1.8),
Volume(next(vid), "TABLE", 1.6, 1.2, 0.75, 20, 1.2, 0.7, 0.75)]
return room
@@ -137,7 +99,6 @@ class FakeRoom:
self.home = home
self.scan: dict = {"state": "none"}
self.polls = 0
self.mesh_triangles = 0
if ROOM_DIR:
from pathlib import Path
@@ -145,17 +106,13 @@ class FakeRoom:
src = Path(ROOM_DIR)
self.room = parse((src / "room.txt").read_text())
self.mesh = (src / self.room.mesh.filename).read_bytes() if self.room.mesh else None
if self.room.mesh:
self.mesh_triangles = self.room.mesh.index_count // 3
else:
from frameport.mr.room import Mesh
self.room = fake_room()
m = fake_room_mesh(self.room)
self.room.mesh = Mesh(uuid=f"{0xe0:032x}", filename="room-mesh.bin", vertex_count=len(m.vertices),
index_count=len(m.indices))
self.room.mesh = Mesh(filename="room-mesh.bin", vertex_count=len(m.vertices), index_count=len(m.indices))
self.mesh = write_mesh(m)
self.mesh_triangles = len(m.indices) // 3
self.text = dumps(self.room)
self.refine: dict = {} # the saved clean-up settings (non-defaults)
self.edited = False
@@ -173,57 +130,37 @@ class FakeRoom:
with open(os.path.join(self._folder(), self.room.mesh.filename), "wb") as f:
f.write(self.mesh)
# the scan clean-up (scan.json, docs/mr/ROOM_FORMAT.md): what the label rules decided for each of the scanner's
# boxes (in its order; to = None: removed), and the door / closet suggestions (never applied by the scanner)
# the scan clean-up (scan.json): what the label rules decided for the scanner's boxes, door / closet suggestions
LABELS = [
{"from": "BED", "to": "BED", "reason": "flat top at 0.55 m, 2.05 x 1.45 m against a wall: a bed",
"box": {"w": 1.45, "d": 2.05, "h": 0.55}},
{"from": "TABLE", "to": "SCREEN", "reason": "7 cm thin, flat on the wall, 1.1 m above the floor: a screen",
"box": {"w": 1.12, "d": 0.07, "h": 0.66}},
{"from": "TABLE", "to": "TABLE", "reason": "top at 0.75 m by a wall: a desk",
"box": {"w": 1.4, "d": 0.7, "h": 0.75}},
{"from": "OTHER", "to": "SCREEN", "reason": "6 cm thin and upright, standing on a table: a screen",
"box": {"w": 0.6, "d": 0.06, "h": 0.36}},
{"from": "SCREEN", "to": "SCREEN", "reason": "upright on a table, facing the room: a screen",
"box": {"w": 0.6, "d": 0.06, "h": 0.36}},
{"from": "STORAGE", "to": "STORAGE", "reason": "2.1 m high and 0.6 m deep in a corner: storage",
"box": {"w": 1.0, "d": 0.6, "h": 2.1}},
{"from": "STORAGE", "to": "TABLE", "reason": "top at 0.52 m, 0.45 m wide by the bed: a nightstand",
"box": {"w": 0.45, "d": 0.4, "h": 0.52}},
{"from": "LAMP", "to": "LAMP", "reason": "0.38 m high, standing on a table: a lamp",
"box": {"w": 0.2, "d": 0.2, "h": 0.38}},
{"from": "PLANT", "to": "PLANT", "reason": "0.95 m high and round from above: a plant",
"box": {"w": 0.36, "d": 0.36, "h": 0.95}},
{"from": "OTHER", "to": None, "reason": "0.5 m behind the wall: outside the room",
"box": {"w": 0.6, "d": 0.4, "h": 0.8}},
{"from": "TABLE", "to": None, "reason": "only 3 cm above the floor: a rug, not furniture",
"box": {"w": 1.6, "d": 1.1, "h": 0.03}},
{"from": "STORAGE", "to": None, "suggest": "door", "reason": "2.0 m high, 10 cm thin, flat on the wall",
"box": {"w": 0.8, "d": 0.1, "h": 2.0}}]
SUGGESTIONS = [{"kind": "door", "box": {"cx": 1.75, "cz": 1.0, "top_y": 2.0, "yaw_deg": 90, "w": 0.8, "d": 0.1,
"h": 2.0}, "wall": 1, "along": 0.81,
"reason": "A tall, thin slab on the wall, 0.8 m wide: a door, or a closet's front"}]
{"from": "COUCH", "to": "COUCH", "reason": "seat top 0.85 m, 2.2 m long by a wall: a couch",
"box": {"w": 2.2, "d": 0.9, "h": 0.85}},
{"from": "COUCH", "to": "TABLE", "reason": "top at 0.45 m and only 1.1 m long: a low table, not a couch",
"box": {"w": 1.1, "d": 0.6, "h": 0.45}},
{"from": "TABLE", "to": "SCREEN", "reason": "thin, raised 0.7 m, parallel to the wall, flat (1 cm): a screen",
"box": {"w": 1.4, "d": 0.12, "h": 0.8}},
{"from": "STORAGE", "to": "STORAGE", "reason": "1.8 m high and 0.4 m deep by a wall: storage",
"box": {"w": 1.2, "d": 0.4, "h": 1.8}},
{"from": "TABLE", "to": "TABLE", "reason": "top at 0.75 m: a table", "box": {"w": 1.2, "d": 0.7, "h": 0.75}},
{"from": "OTHER", "to": None, "reason": "0.6 m behind the wall: outside the room",
"box": {"w": 0.5, "d": 0.4, "h": 0.9}},
{"from": "TABLE", "to": None, "reason": "holds a couch and a table 1.4 m apart: several things merged",
"box": {"w": 3.1, "d": 1.6, "h": 0.8}},
{"from": "STORAGE", "to": None, "suggest": "door", "reason": "2.05 m high, 12 cm thin, flat on the wall",
"box": {"w": 0.95, "d": 0.12, "h": 2.05}}]
SUGGESTIONS = [{"kind": "door", "box": {"cx": 2.45, "cz": 0.9, "top_y": 2.05, "yaw_deg": 90, "w": 0.95, "d": 0.12,
"h": 2.05}, "wall": 1, "along": 0.72,
"reason": "A tall, thin slab on the wall, 0.95 m wide: a door, or a closet's front"}]
def report(self, settings: dict | None = None) -> dict:
"""scan.json's report (its keys: docs/mr/ROOM_FORMAT.md), consistent with the room's own mesh: `kept` is
the mesh's triangle count with the recommended settings, other settings remove more or less."""
s = {**{"walked": 0.4, "smooth": 8, "island": 0.2, "walls": "panels"}, **(settings or {})}
def removed(s):
return {"walked": int(1900 * s["walked"]), "small_pieces": int(3600 * s["island"]),
"near_walls": 140 if s["walls"] == "panels" else 40}
now, default = removed(s), removed({"walked": 0.4, "island": 0.2, "walls": "panels"})
fixed = {"outside": 1260, "thin_pieces": 184, "erased": 0}
kept = self.mesh_triangles - (sum(now.values()) - sum(default.values()))
return {"triangles": self.mesh_triangles + sum(default.values()) + sum(fixed.values()), "kept": kept,
"structural": int(self.mesh_triangles * 0.62) if s["walls"] != "off" else 0, **now, **fixed,
"tops": 410, "panel_triangles": 64 if s["walls"] == "panels" else 0, "truncated": 0,
"walked_skipped": False, "floor_y": 0.0, "ceiling_y": 2.5,
"boxes": sum(1 for lb in self.LABELS if lb["to"]),
"relabelled": sum(1 for lb in self.LABELS if lb["to"] and lb["to"] != lb["from"]),
"removed_boxes": sum(1 for lb in self.LABELS if not lb["to"]),
"suggestions": len(self.SUGGESTIONS), "plane_shifts": [{"kind": "wall", "wall": 3, "shift": 0.02}]}
removed = int(5200 * s["walked"]) + int(9000 * s["island"]) + (2400 if s["walls"] == "panels" else 600)
# scan.json's report keys (docs/mr/ROOM_FORMAT.md)
return {"triangles": 48210, "kept": 48210 - removed, "structural": 21480 if s["walls"] != "off" else 0,
"near_walls": 310, "outside": 1840, "small_pieces": int(9000 * s["island"]), "thin_pieces": 212,
"erased": 0, "walked": int(5200 * s["walked"]), "tops": 640,
"panel_triangles": 96 if s["walls"] == "panels" else 0, "truncated": 0, "walked_skipped": False,
"floor_y": 0.0, "ceiling_y": 2.6, "boxes": 7, "relabelled": 2, "removed_boxes": 2, "suggestions": 1,
"plane_shifts": [{"kind": "wall", "wall": 0, "shift": 0.02}]}
def status(self) -> dict:
from frameport.mr.room import summary
+1 -12
View File
@@ -399,17 +399,6 @@ def _long_upload(session, package: str, seconds: float = 30.0, after: float = 2.
app.jobs.submit(Job(f"Install {app._title(package)} on Frame", run, package, "install"))
def _open_room(session) -> None:
"""The Room tab with the pretend Frame's scanned room loaded and drawn (3D view, cards)."""
fakes.open_room(session.app)
def _room_editing(session, which: str = "box") -> None:
"""The Room tab's editing cards scrolled into view (fakes.show_room_editing: cleanup, more, box = a box selected,
doors, suggestion = the door suggestion taken)."""
fakes.show_room_editing(session.app, which)
def _press_keys(session, text: str, gap: float = 0.28) -> None:
"""Keys typed on the Type on Frame tab (the pretend keyboard), one by one."""
view = session.app.keyboard_view
@@ -425,4 +414,4 @@ HOOKS = {"fake_install": _fake_install, "wait_jobs": _wait_jobs, "held_install":
"frame_asks": _frame_asks,
"easter_eggs": _easter_eggs, "fps_dip": _fps_dip, "perfect_pacing": _perfect_pacing,
"battery": _battery, "long_upload": _long_upload, "press_keys": _press_keys,
"monitor_settled": _monitor_settled, "open_room": _open_room, "room_editing": _room_editing}
"monitor_settled": _monitor_settled}
+1 -1
View File
@@ -10,7 +10,7 @@ const site = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const repo = resolve(site, '..');
const SHOTS = ['library.png', 'game.png', 'monitor.png', 'live-view.png', 'files.png', 'type-on-frame.png',
'screenshots.png', 'install-progress.png', 'patches.png', 'room.png'];
'screenshots.png', 'install-progress.png', 'patches.png'];
const PUBLIC = {
'docs/images': ['tour-teaser.webp'],
'docs/media': ['frameport-tour.mp4', 'frameport-tour.jpg', 'frameport-install.mp4', 'frameport-install.jpg'],
-2
View File
@@ -13,7 +13,6 @@ import keys from '../assets/media/type-on-frame.png';
import screenshots from '../assets/media/screenshots.png';
import progress from '../assets/media/install-progress.png';
import patches from '../assets/media/patches.png';
import room from '../assets/media/room.png';
import { url } from '../data/site';
interface Props { games: MockGame[] }
@@ -22,7 +21,6 @@ const shots = [
{ img: library, title: 'Library', text: 'Your games with their artwork. Search, filter, install several at once.' },
{ img: game, title: 'Game page', text: 'One click to install or play. Patches under Customize.' },
{ img: progress, title: 'Installing', text: 'Each stage, with speed and time left.' },
{ img: room, title: 'Room (experimental)', text: 'Your scanned room in 3D for mixed-reality games. Clean up the scan, fix the labels.' },
{ img: monitor, title: 'Monitor', text: "Frame rate, load, temperature and battery, live." },
{ img: live, title: 'Live view', text: 'What the headset shows, with sound, in your browser.' },
{ img: screenshots, title: 'Screenshots', text: 'Your headset screenshots, by game and day.' },
+97 -187
View File
@@ -1,32 +1,14 @@
{
"updated": "2026-10-19",
"updated": "2026-10-17",
"people": [
{
"login": "spoopyghosty0",
"avatar": "https://avatars.githubusercontent.com/u/336754034?v=4",
"url": "https://github.com/spoopyghosty0",
"commits": 783,
"commits": 707,
"issues": 6,
"prs": 5,
"total": 826
},
{
"login": "Izimzizi",
"avatar": "https://avatars.githubusercontent.com/u/77581918?v=4",
"url": "https://github.com/Izimzizi",
"commits": 0,
"issues": 21,
"prs": 0,
"total": 63
},
{
"login": "tkp206093",
"avatar": "https://avatars.githubusercontent.com/u/29872453?v=4",
"url": "https://github.com/tkp206093",
"commits": 0,
"issues": 21,
"prs": 0,
"total": 63
"prs": 4,
"total": 745
},
{
"login": "Klownicle",
@@ -37,6 +19,15 @@
"prs": 0,
"total": 60
},
{
"login": "tkp206093",
"avatar": "https://avatars.githubusercontent.com/u/29872453?v=4",
"url": "https://github.com/tkp206093",
"commits": 0,
"issues": 18,
"prs": 0,
"total": 54
},
{
"login": "UnicronNL",
"avatar": "https://avatars.githubusercontent.com/u/1267745?v=4",
@@ -55,24 +46,6 @@
"prs": 0,
"total": 45
},
{
"login": "Genosystem",
"avatar": "https://avatars.githubusercontent.com/u/40860703?v=4",
"url": "https://github.com/Genosystem",
"commits": 0,
"issues": 13,
"prs": 0,
"total": 39
},
{
"login": "Lucas-Mathieu",
"avatar": "https://avatars.githubusercontent.com/u/184943223?v=4",
"url": "https://github.com/Lucas-Mathieu",
"commits": 12,
"issues": 0,
"prs": 5,
"total": 37
},
{
"login": "Sheebalove",
"avatar": "https://avatars.githubusercontent.com/u/176067652?v=4",
@@ -118,6 +91,24 @@
"prs": 0,
"total": 30
},
{
"login": "Lucas-Mathieu",
"avatar": "https://avatars.githubusercontent.com/u/184943223?v=4",
"url": "https://github.com/Lucas-Mathieu",
"commits": 9,
"issues": 0,
"prs": 4,
"total": 29
},
{
"login": "Genosystem",
"avatar": "https://avatars.githubusercontent.com/u/40860703?v=4",
"url": "https://github.com/Genosystem",
"commits": 0,
"issues": 9,
"prs": 0,
"total": 27
},
{
"login": "inseend",
"avatar": "https://avatars.githubusercontent.com/u/11666993?v=4",
@@ -127,6 +118,15 @@
"prs": 0,
"total": 27
},
{
"login": "Izimzizi",
"avatar": "https://avatars.githubusercontent.com/u/77581918?v=4",
"url": "https://github.com/Izimzizi",
"commits": 0,
"issues": 9,
"prs": 0,
"total": 27
},
{
"login": "jim2011",
"avatar": "https://avatars.githubusercontent.com/u/15848374?v=4",
@@ -136,15 +136,6 @@
"prs": 0,
"total": 24
},
{
"login": "Jmckenzie84",
"avatar": "https://avatars.githubusercontent.com/u/71196491?v=4",
"url": "https://github.com/Jmckenzie84",
"commits": 0,
"issues": 8,
"prs": 0,
"total": 24
},
{
"login": "coah80",
"avatar": "https://avatars.githubusercontent.com/u/100182500?v=4",
@@ -155,14 +146,23 @@
"total": 20
},
{
"login": "broli95",
"avatar": "https://avatars.githubusercontent.com/u/22918304?v=4",
"url": "https://github.com/broli95",
"login": "Jmckenzie84",
"avatar": "https://avatars.githubusercontent.com/u/71196491?v=4",
"url": "https://github.com/Jmckenzie84",
"commits": 0,
"issues": 6,
"prs": 0,
"total": 18
},
{
"login": "broli95",
"avatar": "https://avatars.githubusercontent.com/u/22918304?v=4",
"url": "https://github.com/broli95",
"commits": 0,
"issues": 5,
"prs": 0,
"total": 15
},
{
"login": "buildmine10",
"avatar": "https://avatars.githubusercontent.com/u/33100363?v=4",
@@ -199,15 +199,6 @@
"prs": 0,
"total": 15
},
{
"login": "jeliastam",
"avatar": "https://avatars.githubusercontent.com/u/2113009?v=4",
"url": "https://github.com/jeliastam",
"commits": 0,
"issues": 5,
"prs": 0,
"total": 15
},
{
"login": "Scoota23",
"avatar": "https://avatars.githubusercontent.com/u/27732436?v=4",
@@ -235,6 +226,15 @@
"prs": 0,
"total": 15
},
{
"login": "jeliastam",
"avatar": "https://avatars.githubusercontent.com/u/2113009?v=4",
"url": "https://github.com/jeliastam",
"commits": 0,
"issues": 4,
"prs": 0,
"total": 12
},
{
"login": "Loafmeister",
"avatar": "https://avatars.githubusercontent.com/u/60628625?v=4",
@@ -253,15 +253,6 @@
"prs": 1,
"total": 12
},
{
"login": "Natouille04",
"avatar": "https://avatars.githubusercontent.com/u/60348431?v=4",
"url": "https://github.com/Natouille04",
"commits": 0,
"issues": 4,
"prs": 0,
"total": 12
},
{
"login": "nickwilde77555",
"avatar": "https://avatars.githubusercontent.com/u/180547421?v=4",
@@ -361,15 +352,6 @@
"prs": 0,
"total": 9
},
{
"login": "wanwanwanwano",
"avatar": "https://avatars.githubusercontent.com/u/341975109?v=4",
"url": "https://github.com/wanwanwanwano",
"commits": 0,
"issues": 3,
"prs": 0,
"total": 9
},
{
"login": "Akimya49",
"avatar": "https://avatars.githubusercontent.com/u/46827872?v=4",
@@ -379,33 +361,6 @@
"prs": 0,
"total": 6
},
{
"login": "crointer",
"avatar": "https://avatars.githubusercontent.com/u/165038970?v=4",
"url": "https://github.com/crointer",
"commits": 0,
"issues": 2,
"prs": 0,
"total": 6
},
{
"login": "DerMueller",
"avatar": "https://avatars.githubusercontent.com/u/82468245?v=4",
"url": "https://github.com/DerMueller",
"commits": 0,
"issues": 2,
"prs": 0,
"total": 6
},
{
"login": "DexgamingX",
"avatar": "https://avatars.githubusercontent.com/u/85261590?v=4",
"url": "https://github.com/DexgamingX",
"commits": 0,
"issues": 2,
"prs": 0,
"total": 6
},
{
"login": "DJ3thenew23",
"avatar": "https://avatars.githubusercontent.com/u/3744521?v=4",
@@ -433,15 +388,6 @@
"prs": 0,
"total": 6
},
{
"login": "Gabiga71",
"avatar": "https://avatars.githubusercontent.com/u/20774946?v=4",
"url": "https://github.com/Gabiga71",
"commits": 0,
"issues": 2,
"prs": 0,
"total": 6
},
{
"login": "ganona",
"avatar": "https://avatars.githubusercontent.com/u/112239200?v=4",
@@ -478,15 +424,6 @@
"prs": 0,
"total": 6
},
{
"login": "mamasboi2000",
"avatar": "https://avatars.githubusercontent.com/u/344123932?v=4",
"url": "https://github.com/mamasboi2000",
"commits": 0,
"issues": 2,
"prs": 0,
"total": 6
},
{
"login": "marcoszaballos",
"avatar": "https://avatars.githubusercontent.com/u/44778619?v=4",
@@ -523,6 +460,15 @@
"prs": 0,
"total": 6
},
{
"login": "Natouille04",
"avatar": "https://avatars.githubusercontent.com/u/60348431?v=4",
"url": "https://github.com/Natouille04",
"commits": 0,
"issues": 2,
"prs": 0,
"total": 6
},
{
"login": "peetzweg",
"avatar": "https://avatars.githubusercontent.com/u/839848?v=4",
@@ -550,6 +496,15 @@
"prs": 0,
"total": 6
},
{
"login": "wanwanwanwano",
"avatar": "https://avatars.githubusercontent.com/u/341975109?v=4",
"url": "https://github.com/wanwanwanwano",
"commits": 0,
"issues": 2,
"prs": 0,
"total": 6
},
{
"login": "yzfsoldier",
"avatar": "https://avatars.githubusercontent.com/u/35410319?v=4",
@@ -685,6 +640,24 @@
"prs": 0,
"total": 3
},
{
"login": "DerMueller",
"avatar": "https://avatars.githubusercontent.com/u/82468245?v=4",
"url": "https://github.com/DerMueller",
"commits": 0,
"issues": 1,
"prs": 0,
"total": 3
},
{
"login": "DexgamingX",
"avatar": "https://avatars.githubusercontent.com/u/85261590?v=4",
"url": "https://github.com/DexgamingX",
"commits": 0,
"issues": 1,
"prs": 0,
"total": 3
},
{
"login": "DolphinOfficial",
"avatar": "https://avatars.githubusercontent.com/u/92276540?v=4",
@@ -712,15 +685,6 @@
"prs": 0,
"total": 3
},
{
"login": "ehnobigdeal0",
"avatar": "https://avatars.githubusercontent.com/u/344167176?v=4",
"url": "https://github.com/ehnobigdeal0",
"commits": 0,
"issues": 1,
"prs": 0,
"total": 3
},
{
"login": "felis-lynx",
"avatar": "https://avatars.githubusercontent.com/u/106830854?v=4",
@@ -748,15 +712,6 @@
"prs": 0,
"total": 3
},
{
"login": "Haseo-sudo",
"avatar": "https://avatars.githubusercontent.com/u/344139069?v=4",
"url": "https://github.com/Haseo-sudo",
"commits": 0,
"issues": 1,
"prs": 0,
"total": 3
},
{
"login": "hokey1981",
"avatar": "https://avatars.githubusercontent.com/u/42741984?v=4",
@@ -856,24 +811,6 @@
"prs": 0,
"total": 3
},
{
"login": "MThomason122",
"avatar": "https://avatars.githubusercontent.com/u/25598648?v=4",
"url": "https://github.com/MThomason122",
"commits": 0,
"issues": 1,
"prs": 0,
"total": 3
},
{
"login": "Nikoroq",
"avatar": "https://avatars.githubusercontent.com/u/135438574?v=4",
"url": "https://github.com/Nikoroq",
"commits": 0,
"issues": 1,
"prs": 0,
"total": 3
},
{
"login": "njorbie",
"avatar": "https://avatars.githubusercontent.com/u/340724129?v=4",
@@ -892,15 +829,6 @@
"prs": 0,
"total": 3
},
{
"login": "persuaded9041",
"avatar": "https://avatars.githubusercontent.com/u/464398?v=4",
"url": "https://github.com/persuaded9041",
"commits": 0,
"issues": 1,
"prs": 0,
"total": 3
},
{
"login": "pinbear-creator",
"avatar": "https://avatars.githubusercontent.com/u/194687704?v=4",
@@ -1018,15 +946,6 @@
"prs": 0,
"total": 3
},
{
"login": "Zomby2D",
"avatar": "https://avatars.githubusercontent.com/u/5513720?v=4",
"url": "https://github.com/Zomby2D",
"commits": 0,
"issues": 1,
"prs": 0,
"total": 3
},
{
"login": "zoomiti",
"avatar": "https://avatars.githubusercontent.com/u/34012653?v=4",
@@ -1035,15 +954,6 @@
"issues": 1,
"prs": 0,
"total": 3
},
{
"login": "codex",
"avatar": "https://avatars.githubusercontent.com/u/267193182?v=4",
"url": "https://github.com/codex",
"commits": 2,
"issues": 0,
"prs": 0,
"total": 2
}
]
}
-15
View File
@@ -94,11 +94,6 @@
"title": "Colour space support",
"group": "Game settings"
},
"adapter.fixed_resolution": {
"anchor": "adapter-fixed_resolution",
"title": "Keep full resolution (no dynamic resolution)",
"group": "Game settings"
},
"adapter.flip_dump": {
"anchor": "adapter-flip_dump",
"title": "Upside-down panel images (diagnostics)",
@@ -339,11 +334,6 @@
"title": "Retry refused picture formats",
"group": "Game settings"
},
"adapter.swapchain_pool": {
"anchor": "adapter-swapchain_pool",
"title": "Share panel images (swapchain limit)",
"group": "Game settings"
},
"adapter.sync_guard": {
"anchor": "adapter-sync_guard",
"title": "Steady controller input after focus",
@@ -624,11 +614,6 @@
"title": "Start straight in VR (skip the 2D launcher)",
"group": "Steam Frame patches"
},
"frame.static_openxr_loader": {
"anchor": "frame-static_openxr_loader",
"title": "Embedded OpenXR loader: use FrameBridge",
"group": "Steam Frame patches"
},
"frame.swapchain_limit": {
"anchor": "frame-swapchain_limit",
"title": "Allow swapchains larger than 4096 px",
+1 -9
View File
@@ -1,14 +1,6 @@
{
"updated": "2026-10-19",
"updated": "2026-10-17",
"releases": [
{
"tag": "v1.3.0",
"name": "FramePort v1.3.0",
"date": "2026-10-19",
"url": "https://github.com/spoopyghosty0/frameport/releases/tag/v1.3.0",
"dev": false,
"notes": "FramePort v1.3.0: Windows (x64), macOS (Apple Silicon) and Linux (x64, ARM64).\n\n## What's new\n\n- **Mixed reality (experimental):** a new Room tab scans your room in the headset, shows it in 3D, cleans up the scan and lets you fix furniture, screen and door labels; mixed-reality games get the room through full Meta scene emulation (thanks @alicecatpaw).\n- **Quest 3 by default:** games see a Quest 3; the headset model, game language, foveation and render resolution can be set per game.\n- **Save backups:** choose the backup folder and keep automatic snapshots made on the Frame when a game closes.\n- **Steam library:** add new games to Steam now, at Steam's next start, or not at all.\n- **Files:** SD cards and other drives, plus sections for PC VR games and Linux apps.\n- **Controllers:** an experimental Quest button mode, and own button layouts for the gamepad in 2D apps.\n- **Game fixes:** Batman: Arkham Shadow's edge line and audio hooks (thanks @Lucas-Mathieu), PrimedGun, 4XVR's menus, Halo CE VR closing during the intro, OutRun, Transformers Beyond Reality, cube loading screens, and Unreal games whose install stopped at a memory patch check.\n- **Fixes:** USB setup without a flashing console, store details after a rename.\n- **22 new recipes.**\n\nAlready have FramePort? It offers this update itself (Library → **Update now**).\n\n## Install\n\nDownload the file for your PC, unpack it and start FramePort. Guide:\n[Install and first steps](https://frameport.app/docs/install/).\n\n"
},
{
"tag": "v1.2.1",
"name": "FramePort v1.2.1",
+1 -1
View File
@@ -1,2 +1,2 @@
# The FramePort version: the single source (pyproject reads it via hatch; CI checks a release tag matches it).
__version__ = "1.3.0"
__version__ = "1.2.1"
+5 -9
View File
@@ -9,7 +9,7 @@ from pathlib import Path
from ..apk import axml
from ..core.models import Analysis
from . import elf, static_openxr
from . import elf
from .luajit import xlua_47bit, xlua_sha256
from .unity_split import split_build as unity_split_build
@@ -56,10 +56,9 @@ IL2CPP_METADATA = "assets/bin/Data/Managed/Metadata/global-metadata.dat"
# GitHub #349) (2026-10)
# 23: separate_process (MAIN activities in another process than the main one, which Lepton doesn't watch:
# frame.main_process, GitHub #178) (2026-10)
# 24: statically linked Khronos OpenXR loaders (frame.static_openxr_loader, GitHub #254) (2026-10)
# 25: meta_depth_occlusion (Unity games with Meta's depth-occlusion keywords: frame.meta_depth_occlusion, PR #246)
# 24: meta_depth_occlusion (Unity games with Meta's depth-occlusion keywords: frame.meta_depth_occlusion, PR #246)
# (2026-10)
ANALYSIS_VERSION = 25
ANALYSIS_VERSION = 24
# Android versions by API level (for messages); the Frame's Lepton container runs Android 11 (API 30)
@@ -344,8 +343,7 @@ def analyze(path: Path, deep: bool = True, data_bytes: int | None = None) -> Ana
libset = set(libs)
engine = ("Unreal" if libset & {"libUE4.so", "libUnreal.so"} else "Unity" if "libunity.so" in libset
else "CryEngine" if "libCrySystem.so" in libset or cry_project else "Other")
static_loaders = [n for n, d in lib_bytes.items() if static_openxr.detected(n, d)] if abi == "arm64-v8a" else []
has_openxr, has_vrapi = bool(static_loaders) or "libopenxr_loader.so" in libset, "libvrapi.so" in libset
has_openxr, has_vrapi = "libopenxr_loader.so" in libset, "libvrapi.so" in libset
xr = "OpenXR+VrApi" if has_openxr and has_vrapi else "OpenXR" if has_openxr else "VrApi" if has_vrapi else "?"
is_overport = "libopenxr_loader_generic.so" in libset or "liboverport.config.so" in libset
# VrApi called directly by the engine (no OVRPlugin): overport cannot translate it.
@@ -410,7 +408,6 @@ def analyze(path: Path, deep: bool = True, data_bytes: int | None = None) -> Ana
is_overport_output=is_overport,
debuggable=bool(axml.Axml(manifest).get_bool("application", "debuggable")),
extra={
"static_openxr_loader": static_loaders,
"analysis_version": ANALYSIS_VERSION,
"xlua_47bit": xlua_47bit(lib_bytes.get("libxlua.so", b"")),
"xlua_sha256": xlua_sha256(lib_bytes.get("libxlua.so", b"")),
@@ -438,8 +435,7 @@ def analyze(path: Path, deep: bool = True, data_bytes: int | None = None) -> Ana
"unreal_version": unreal_version,
"oculus_os_refs": sorted(oculus_os_refs),
"xr_layer_exts": sorted(layer_exts), # composition-layer extensions the game's own libraries request
"vr_kind": ("openxr" if static_loaders
and vr_kind(libset, manifest_strings) == "none" else vr_kind(libset, manifest_strings)),
"vr_kind": vr_kind(libset, manifest_strings),
"headset": headset(meta, abis, vr_kind(libset, manifest_strings), requires_6dof(manifest)),
# a Google Daydream / Cardboard build (GVR / Cardboard SDK): no path to the Frame's headset (GitHub #335)
"google_vr": google_vr(libset, cats, manifest_strings, gvr_java, boot),
-80
View File
@@ -1,80 +0,0 @@
"""Khronos loaders embedded in game libraries (never the separately shipped loader)."""
from __future__ import annotations
import io
import re
from elftools.common.exceptions import ELFError
from elftools.elf.elffile import ELFFile
from . import elf
MANIFEST = "/sdcard/fp_openxr_runtime.json"
# Some builds embed complete paths. Match every partition and ABI spelling, rather than a fixed list.
PATH = re.compile(rb"/(?:[A-Za-z0-9_-]+/)*etc/openxr/1/active_runtime(?:\.[A-Za-z0-9_-]+)?\.json\x00")
COMPOSED_MANIFEST = "/sdcard/fp/1/active_runtime.json"
PARTITIONS = ("/product", "/odm", "/oem", "/vendor", "/system")
SUBDIR = "/etc/openxr/"
BROKERS = ("org.khronos.openxr.runtime_broker", "org.khronos.openxr.system_runtime_broker")
def paths(data: bytes) -> list[str]:
if not elf.is_elf(data):
return []
sec = ELFFile(io.BytesIO(data)).get_section_by_name(".rodata")
if sec is None:
return []
raw = sec.data()
return sorted({m.group()[:-1].decode() for m in PATH.finditer(raw)
if m.start() == 0 or raw[m.start() - 1] == 0})
def detected(name: str, data: bytes) -> bool:
if (name.startswith("libopenxr_loader") or not elf.is_elf(data)
or b"RuntimeManifestFile" not in data or b"xrNegotiateLoaderRuntimeInterface" not in data):
return False
try:
e = ELFFile(io.BytesIO(data))
return e["e_machine"] == "EM_AARCH64" and (bool(paths(data)) or composed(data))
except (ELFError, ValueError):
return False
def composed(data: bytes) -> bool:
"""Khronos platform_utils.hpp builds prefix + subdir + major + suffix.
Require the complete known table before touching these potentially shared literals.
"""
sec = ELFFile(io.BytesIO(data)).get_section_by_name(".rodata")
strings = set(sec.data().split(b"\0")) if sec else set()
return (all(s.encode() in strings for s in (*PARTITIONS, SUBDIR, "/active_runtime.json"))
and any(s.startswith(b"/active_runtime.") and s.endswith(b".json") for s in strings))
def redirected(data: bytes) -> bool:
if paths(data) or composed(data) or any(b.encode() + b"\0" in data for b in BROKERS):
return False
if MANIFEST.encode() + b"\0" in data:
return True
sec = ELFFile(io.BytesIO(data)).get_section_by_name(".rodata")
strings = set(sec.data().split(b"\0")) if sec else set()
return (b"card/fp/" in strings and b"/sd" in strings
and not any(p.encode() in strings for p in PARTITIONS))
def rewrite(data: bytes) -> tuple[bytes, int]:
count = 0
if composed(data):
# Keep every candidate identical: /sd + card/fp/ + 1 + /active_runtime[.ABI].json.
# These partition literals may be pooled with app strings; see DEVICE_NOTES.
for old, new in [(p, "/sd") for p in PARTITIONS] + [(SUBDIR, "card/fp/")]:
data, n = elf.replace_rodata_string(data, old, new)
count += n
for old in paths(data):
data, n = elf.replace_rodata_string(data, old, MANIFEST)
count += n
for old in BROKERS:
# Keep URI length unchanged as well as the string's storage; both authorities must miss.
data, n = elf.replace_rodata_string(data, old, old.replace("org.khronos", "org.fp_null"))
count += n
return data, count
+1 -2
View File
@@ -883,8 +883,7 @@ def room_scan(frame: Optional[str] = typer.Option(None, help=FRAME_HELP),
typer.echo(f"{st.get('state')}: {st.get('directions', 0)}/12 directions, "
f"ceiling {'seen' if st.get('looked_up', 0) >= 5 else 'not yet'}", err=True)
result = rooms.scan(_target(frame).frame, seconds, wait, progress=progress, preview=preview)
typer.echo(json.dumps(result, indent=2))
typer.echo(json.dumps(rooms.scan(_target(frame).frame, seconds, wait, progress=progress, preview=preview), indent=2))
@room_app.command("refine")
+1 -2
View File
@@ -342,8 +342,7 @@ def _migrate(data: dict) -> bool:
if (not isinstance(a, dict) or not isinstance(r, dict) or r.get("source") == "user"
or g.get("kind") in ("linux", "rift")):
continue
scene_on = (r.get("patches") or {}).get("adapter.scene_emul", {}).get("value")
if scene_on and uses_scene(analysis_from_dict(a)):
if (r.get("patches") or {}).get("adapter.scene_emul", {}).get("value") and uses_scene(analysis_from_dict(a)):
r["patches"].setdefault("frame.meta_permissions", {})
r.setdefault("reasons", {}).setdefault(
"frame.meta_permissions", "Meta's room and anchor permissions must be granted for the saved room.")
-9
View File
@@ -542,7 +542,6 @@
"Drop your own boxes, doors and windows and use the boxes the scan found.": "",
"Drops Quest-only shared library requirements.": "",
"Each game start used to use up one kernel key; after about 200 starts no game started. FramePort stops that, but used keys only come back after a restart of the Frame.": "",
"Embedded OpenXR loader: use FrameBridge": "",
"Enabled by you.": "",
"End all": "",
"End all {n} {name}?": "",
@@ -944,7 +943,6 @@
"just now": "",
"keep all": "",
"Keep converted copies on this PC (only needed to inspect a build)": "",
"Keep full resolution (no dynamic resolution)": "",
"Keep game saves on the Frame": "",
"Keep menus in place": "",
"Keep past walls": "",
@@ -956,12 +954,10 @@
"Keep the signing keys: without them, updating a game loses its saves.": "",
"Keeps a smoothing setting off that the game turns on itself and that can freeze the Frame.": "",
"Keeps every submitted image rect inside its swapchain. Unity can size the eye area a few pixels past the image on some Frames; SteamVR then rejects every frame (xrEndFrame -25, XR_ERROR_SWAPCHAIN_RECT_INVALID) and the game stops drawing (for example PowerWash Simulator stuck at \"Waiting\"). An empty rect (0x0, e.g. BattleTalent's first frames, GitHub #293) is rejected the same way and becomes the whole swapchain image.": "",
"Keeps extra menus and panels working when the Frame reaches its image count limit. GLES apps only; Vulkan apps are unaffected.": "",
"Keeps games running that use a 360° cube layer the Frame lacks (for example Budget Cuts Ultimate). Leave it on.": "",
"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).": "",
"Keeps one label so the title is stable.": "",
"Keeps one name for the game, so it shows the same everywhere.": "",
"Keeps OpenXR layer recommendations at the full swapchain size. Prevents dynamic resolution from moving Valve foveation off-centre or leaving a coarse white edge line. May increase GPU load.": "",
"Keeps the game from being closed about a minute after it starts (it runs in a separate Android process the Frame doesn't watch).": "",
"Keeps the hand and brush visible over Vermillion's painting canvas (the canvas is drawn in the scene instead of as a layer in front).": "",
"Keeps the resolution the game saves in step with the headset's picture size (else the picture is shifted, with black borders, after a render resolution change).": "",
@@ -1087,7 +1083,6 @@
"Low": "",
"Made in another play area: scan again or use your play area.": "",
"Make Unity text fields work": "",
"Makes an embedded OpenXR loader use FrameBridge and your game settings.": "",
"Makes BlazeRush's menus and controllers work without Meta's avatars, and lets its shader fixes take effect.": "",
"Makes grip, trigger and touch controls reach the game's hands (it picked the empty Gear VR controls on the Frame, for example Vader Immortal).": "",
"Makes Meta's 3D audio library work outside Quest headsets.": "",
@@ -1390,7 +1385,6 @@
"Prepare {title}": "",
"Pretty smooth": "",
"Prevents a start-up crash in some Unreal games.": "",
"Prevents dynamic resolution from moving Valve foveation off-centre or leaving a white edge line. May increase GPU load. Used at the next launch.": "",
"Previous": "",
"Private SSH key file (not the .pub file)": "",
"Problems and feedback": "",
@@ -1576,7 +1570,6 @@
"Room setup": "",
"Room width": "",
"Rotate panels 180°": "",
"Routes a game's statically linked Khronos OpenXR loader through FrameBridge by rewriting its runtime manifest paths in place into its own /sdcard (including assembled paths). Disables its Android runtime broker queries so they fall back to that file. Ships a runtime shim and an unmodified inner Khronos loader, which reads the system runtime manifest. Reinstall to write the per-game manifest.": "",
"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.": "",
"Run a launch test after installing on the Frame": "",
"Run one line on the Frame, then click Allow here. You do this once. No password, and nothing changes on the Frame until you allow it.": "",
@@ -1673,8 +1666,6 @@
"shader cache: faster starts; rebuilt if deleted": "",
"Shader-fix Vulkan layer (frame.zink_shader_fix) for OpenGL ES games, whose GLSL the Frame's GL driver (Zink) turns into SPIR-V: modules matching size + SHA-256 get words inserted at a byte offset, for example stores that set loop counters and accumulators a shader reads before writing (Vader Immortal's lightspeed jump hung the GPU; found by Klownicle, GitHub #49). Same format as vk_shader_fix. Comes from a game's recipe; the driver's output can change with a Frame update, then the fix no longer matches (logged).": "",
"Shader-fix Vulkan layer (frame.zink_shader_fix): writes every distinct SPIR-V module the GL driver creates to Android/data/<package>/files/fp_spirv/<size>_<sha256>.spv, to capture a shader for a new zink_shader_fix. No effect on the game.": "",
"Share GLES panel images in runtime atlases to avoid the Frame's swapchain count limit (about 15 per session). Useful when opening extra menus closes them or leaves panels gray. Vulkan apps are unaffected.": "",
"Share panel images (swapchain limit)": "",
"Share working recipe · {title}": "",
"Share working recipe…": "",
"Show 360° backgrounds and videos": "",
@@ -1,69 +0,0 @@
"""Route statically linked Khronos OpenXR loaders through FrameBridge."""
from __future__ import annotations
from ...analysis import static_openxr
from ...tools import overport
from ..base import ApkContext, Patch, Suggestion, register
from . import artifact
from .adapter import GENERIC, ORIGINAL
SHIM = "libfp_xrruntime.so"
class StaticOpenXRLoader(Patch):
id = "frame.static_openxr_loader"
title = "Embedded OpenXR loader: use FrameBridge"
description = ("Routes a game's statically linked Khronos OpenXR loader through FrameBridge by rewriting its "
"runtime manifest paths in place into its own /sdcard (including assembled paths). Disables its "
"Android runtime "
"broker queries so they fall back to that file. Ships a runtime shim and an unmodified inner "
"Khronos loader, which reads the system runtime manifest. Reinstall to write the per-game manifest.")
requires = ("frame.adapter",)
order = 9 # provide the inner loader before frame.adapter wraps it
def applies(self, a):
return bool((a.extra or {}).get("static_openxr_loader"))
def detect(self, a):
if self.applies(a):
return Suggestion(True, "The embedded Khronos OpenXR loader bypasses FrameBridge.")
return None
def apply(self, ctx: ApkContext) -> bool:
ws = ctx.ws
if ws.abi != "arm64-v8a":
raise RuntimeError("Embedded OpenXR loader redirection requires arm64")
libs = (ctx.analysis.extra or {}).get("static_openxr_loader") or []
if not libs:
raise RuntimeError("No embedded Khronos OpenXR loader detected; analyze the APK again")
changed = False
for name in libs:
data = ws.read_lib(name)
fixed, count = static_openxr.rewrite(data)
if not count and not static_openxr.redirected(data):
raise RuntimeError(f"{name}: embedded loader manifest paths could not be redirected")
if count:
ws.put(ws.lib(name), fixed)
changed = True
# Like a32_bridge, also works when the recipe doesn't run OVRPort at all.
if not ws.has(ws.lib(ORIGINAL)) and not ws.has(ws.lib(GENERIC)):
inner = overport.ensure_runtime_lib(GENERIC, ws.abi, ctx.reporter).read_bytes()
ws.put(ws.lib(ORIGINAL), inner)
ws.put(ws.lib(GENERIC), artifact(ws.abi, GENERIC))
changed = True
shim = artifact(ws.abi, SHIM)
if not ws.has(ws.lib(SHIM)) or ws.read_lib(SHIM) != shim:
ws.put(ws.lib(SHIM), shim)
changed = True
return changed
def validate(self, ctx: ApkContext):
ws = ctx.ws
libs = (ctx.analysis.extra or {}).get("static_openxr_loader") or []
ok = bool(libs) and all(ws.has(ws.lib(n)) and static_openxr.redirected(ws.read_lib(n))
for n in libs)
ok = ok and ws.has(ws.lib(SHIM)) and ws.read_lib(SHIM) == artifact(ws.abi, SHIM)
return [("Embedded OpenXR loader uses FrameBridge", ok, ", ".join(libs))]
register(StaticOpenXRLoader)
+4 -20
View File
@@ -16,9 +16,6 @@ BASE_SETTINGS = {"scale": 1.0, "foveation_fix": 1, "controller_fix": 1}
# key, kind, default, title, description
SETTINGS = [
("fixed_resolution", "int", 0, "Keep full resolution (no dynamic resolution)",
"Keeps OpenXR layer recommendations at the full swapchain size. Prevents dynamic resolution from moving "
"Valve foveation off-centre or leaving a coarse white edge line. May increase GPU load."),
("scale", "float", 1.0, "Resolution scale",
"Multiplies the recommended eye-buffer width and height (0.5–2.0). 1.5 ≈ 2.25× pixels."),
("foveation_fix", "int", 1, "Hide Quest foveation", "Hides Quest foveation extensions the Frame runtime lacks."),
@@ -92,9 +89,6 @@ SETTINGS = [
"Monkey's intro video), which the Frame's runtime refuses, get an Android Surface from FrameBridge: each video "
"frame is copied into an ordinary swapchain the panel shows. Without it these panels are missing and a game "
"waiting for its video stays black. 0 = leave them to the runtime."),
("swapchain_pool", "int", 0, "Share panel images (swapchain limit)",
"Share GLES panel images in runtime atlases to avoid the Frame's swapchain count limit (about 15 per session). "
"Useful when opening extra menus closes them or leaves panels gray. Vulkan apps are unaffected."),
("equirect_emul", "int", 0, "Show 360° layers",
"360° (equirect) layers, for example a video player's virtual theater or 360° videos (for example 4XVR), which "
"the Frame's runtime lacks, are drawn by a background thread into a layer behind the game's own picture: the "
@@ -338,9 +332,6 @@ FOVEATION_UI = {
}
UI: dict[str, dict] = {
"fixed_resolution": dict(group="picture", level="advanced", label="Keep full resolution (no dynamic resolution)",
help="Prevents dynamic resolution from moving Valve foveation off-centre or leaving a white edge line. "
"May increase GPU load. Used at the next launch.", control=("switch",)),
"scale": dict(group="picture", level="common", label="Render resolution (supersampling)",
help="Changes the game's render resolution. Higher is sharper but slower; 1.0× is the game's own "
"resolution.",
@@ -429,8 +420,7 @@ UI: dict[str, dict] = {
help="Scene games get your saved room. Without one, they get a simple room from the play area.",
control=("switch",)),
"scene_scan_request": dict(group="room", level="advanced", label="Scan when requested", depends="scene_emul",
help="Scan your room in the headset when the game asks for a room scan.",
control=("switch",)),
help="Scan your room in the headset when the game asks for a room scan.", control=("switch",)),
"scene_hide_bounds": dict(group="room", level="advanced", label="Hide play-area size", depends="scene_emul",
help="For games that reject the room because of the play area.", control=("switch",)),
"scene_height": dict(group="room", level="advanced", label="Room height", depends="scene_emul",
@@ -439,10 +429,6 @@ UI: dict[str, dict] = {
help="0 = the size of your play area.", control=("slider", 0.0, 10.0, 0.5, "{:.1f} m")),
"scene_depth": dict(group="room", level="advanced", label="Room depth", depends="scene_emul",
help="0 = the size of your play area.", control=("slider", 0.0, 10.0, 0.5, "{:.1f} m")),
"swapchain_pool": dict(group="troubleshooting", level="advanced",
label="Share panel images (swapchain limit)",
help="Keeps extra menus and panels working when the Frame reaches its image count limit. "
"GLES apps only; Vulkan apps are unaffected.", control=("switch",)),
"equirect_emul": dict(group="video360", level="common", label="Show 360° backgrounds and videos",
help="Video players' virtual theaters and 360° videos would otherwise stay black.",
control=("switch",)),
@@ -565,8 +551,7 @@ UI: dict[str, dict] = {
# functions OVRPort's dispatcher doesn't know. scene_emul: the dispatcher refuses every scene/anchor function
# ("Unknown proc addr: xrQuerySpacesFB", Pinball FX VR 2026-10-16), so the emulated room only reaches the game through
# the shim (Demeter/NOPE only got it because haptic_fix happened to be on)
XRSHIM_SETTINGS = ("fixed_resolution", "hide_fb_foveation", "controller_models", "haptic_fix", "surface_native",
"scene_emul")
XRSHIM_SETTINGS = ("hide_fb_foveation", "controller_models", "haptic_fix", "surface_native", "scene_emul")
# shown with an "experimental" pill (Game settings, game page, CLI): not proven in the headset on many games yet
EXPERIMENTAL_SETTINGS = ("quest_buttons",)
@@ -649,7 +634,6 @@ class AdapterSetting(Patch):
or "com.oculus.feature.PASSTHROUGH" in (a.extra.get("features") or {})),
"alpha_blend_emul": lambda a: (a.extra.get("mr_only")
or "com.oculus.feature.PASSTHROUGH" in (a.extra.get("features") or {})),
"fixed_resolution": lambda a: "libOVRPlugin.so" in a.libs or a.xr == "OpenXR",
"hide_fb_foveation": ap.is_vulkan,
"flip_emul": ap.is_vulkan,
"flip_quads": ap.is_vulkan,
@@ -680,8 +664,8 @@ class AdapterSetting(Patch):
"proximity_emul": lambda a: "libOVRPlugin.so" in a.libs,
"vk_spec_fixes": lambda a: a.engine == "Unreal" and ap.is_vulkan(a),
"controller_models": ap.may_use_render_models,
**{k: ap.is_gles for k in ("swapchain_pool", "equirect_emul", "equirect_face", "equirect_res",
"equirect_flip", "equirect_fps", "equirect_stereo")},
**{k: ap.is_gles for k in ("equirect_emul", "equirect_face", "equirect_res", "equirect_flip",
"equirect_fps", "equirect_stereo")},
}
rule = rules.get(self.key)
return bool(rule(a)) if rule else True
-1
View File
@@ -4,7 +4,6 @@ Game settings (adapter.*) use their Game settings help line (patches/settings.UI
fallback for settings that dialog doesn't show."""
SUMMARIES = {
"frame.static_openxr_loader": "Makes an embedded OpenXR loader use FrameBridge and your game settings.",
# OVRPort (the Quest → OpenXR conversion)
"patch_copy_libraries": "Adds the parts that let a Quest game run on the Steam Frame. Needed for every Quest "
"game.",
+1 -2
View File
@@ -66,8 +66,7 @@ def suggest(analysis: Analysis, use_catalog: bool = True) -> Recipe:
and "frame.meta_permissions" not in entry.frame_remove):
# the room needs Meta's scene/anchor permissions granted, or the game never asks for it (NOPE)
recipe.patches.setdefault("frame.meta_permissions", {})
recipe.reasons.setdefault("frame.meta_permissions",
"Meta's room and anchor permissions for the saved room.")
recipe.reasons.setdefault("frame.meta_permissions", "Meta's room and anchor permissions for the saved room.")
if entry.device_files:
recipe.patches["device.files"] = {"files": dict(entry.device_files)}
recipe.reasons["device.files"] = why
+8 -46
View File
@@ -112,10 +112,9 @@ class RoomView:
left=0, right=0, bottom=0, bgcolor=T.soft(T.BG, 0.82),
padding=ft.Padding(T.S3, T.S2, T.S3, T.S2),
border_radius=ft.BorderRadius(0, 0, T.RADIUS_SM, T.RADIUS_SM))
self.view = ft.Stack([self.canvas.control, legend, self.placeholder,
ft.Container(self.reset_btn, right=T.S2, top=T.S2)], height=T.px(CANVAS_HEIGHT))
self.canvas_card = C.card(self.view, padding=T.S3, col={"xs": 12, "lg": 8})
self.canvas_card.on_size_change = self._card_sized
view = ft.Stack([self.canvas.control, legend, self.placeholder,
ft.Container(self.reset_btn, right=T.S2, top=T.S2)], height=T.px(CANVAS_HEIGHT))
self.canvas_card = C.card(view, padding=T.S3, col={"xs": 12, "lg": 8})
# status
self.checks = ft.Container(C.checklist([C.Check(None, tr("Checking the room…"))]))
@@ -138,10 +137,8 @@ class RoomView:
self.summary = C.body("", T.TEXT)
self.source = C.meta("")
self.furniture = ft.Row([], spacing=T.S2, run_spacing=T.S2, wrap=True)
# the right column sizes to its content (never clipped: window width, UI scale and the furniture chips change
# its height); level() then makes the top row end level: a short column's last card grows to the 3D view's
# card (self.level_gap), a tall column makes the 3D view taller. Every card with the same padding.
self.level_gap = ft.Container(height=0)
# the right column: as tall as the 3D view's card (its last card takes what's left),
# every card with the same padding
side = ft.Column([
C.card(ft.Column([self.checks, self.error], spacing=T.S2), padding=T.S4),
C.card(ft.Column([C.section(tr("Scan"), self.scan_box,
@@ -149,11 +146,10 @@ class RoomView:
ft.Row([self.boundary_btn, self.delete_btn], spacing=T.S2, wrap=True),
help="mr_room")], spacing=T.S3), padding=T.S4),
C.card(ft.Column([C.meta(tr("In this room")), self.summary, self.source, self.furniture,
self.level_gap, self._tester()], spacing=T.S2),
padding=T.S4),
ft.Container(expand=True), self._tester()], spacing=T.S2),
padding=T.S4, expand=True),
], spacing=T.S4, col={"xs": 12, "lg": 4}, horizontal_alignment=ft.CrossAxisAlignment.STRETCH,
on_size_change=self._side_sized)
self._sizes = {"card": None, "side": None}
height=T.px(CANVAS_HEIGHT) + 2 * T.S3 + 2)
# editing: the scan clean-up, what the label rules decided, furniture boxes + doors / windows
from .room_edit import CleanupCard, FurnitureEditor, LabelsCard
@@ -313,40 +309,6 @@ class RoomView:
C.confirm(self.app.page, tr("Delete room?"), tr("Games will use a simple room until you save another room."),
tr("Delete room"), lambda: RS.operate(self.app, "delete"), danger=True)
# ------------------------------------------------------------------ the top row ends level
def _card_sized(self, e) -> None:
self._sizes["card"] = (e.width, e.height)
self.level()
def _side_sized(self, e) -> None:
self._sizes["side"] = (e.width, e.height)
self.level()
def level(self) -> None:
"""The right column's height (its content's, measured) against the 3D view's card: a shorter column gets a
gap in its last card, a taller one makes the 3D view taller. Stacked (narrow window): neither."""
card, side = self._sizes["card"], self._sizes["side"]
if not card or not side:
return
base = T.px(CANVAS_HEIGHT)
base_card = base + 2 * T.S3 + 2 # (the card's padding and border)
content = side[1] - (self.level_gap.height or 0)
gap, view = 0, base
if side[0] < card[0] * 0.9: # side by side
if content <= base_card:
gap = int(base_card - content)
else:
view = base + int(content - base_card + 0.999)
changed = []
if gap != (self.level_gap.height or 0):
self.level_gap.height = gap
changed.append(self.level_gap)
if view != self.view.height:
self.view.height = self.canvas.control.height = view
changed.append(self.view)
if changed:
C.update(*changed)
async def show_editing(self) -> None:
"""Scroll down to the editing cards (Clean-up, Furniture & screens, Labels)."""
if self.root is not None:
+3 -11
View File
@@ -324,7 +324,8 @@ class LabelsCard:
@staticmethod
def _size(box: dict) -> str:
try:
return size_text(float(box["w"]), float(box["d"]), float(box["h"]))
return tr("{w}×{d}×{h} m").format(w=f"{float(box['w']):.1f}", d=f"{float(box['d']):.1f}",
h=f"{float(box['h']):.1f}")
except (KeyError, TypeError, ValueError):
return ""
@@ -399,15 +400,6 @@ def _panel(rows: list[ft.Control]) -> ft.Container:
bgcolor=T.SURFACE_2, border_radius=T.RADIUS_SM, padding=T.S3, visible=False)
def metres(value: float) -> str:
"""A box size in metres: one decimal, two below 10 cm (a rug's 3 cm isn't "0.0")."""
return f"{value:.2f}" if abs(value) < 0.095 else f"{value:.1f}"
def size_text(w: float, d: float, h: float) -> str:
return tr("{w}×{d}×{h} m").format(w=metres(w), d=metres(d), h=metres(h))
def field_label(name: str) -> str:
return {"Left / right": tr("Left / right"), "Front / back": tr("Front / back"), "Top height": tr("Top height"),
"Turn": tr("Turn"), "Width": tr("Width"), "Depth": tr("Depth"), "Height": tr("Height"),
@@ -537,7 +529,7 @@ class FurnitureEditor:
# ------------------------------------------------------------------ lists
@staticmethod
def _box_size(v) -> str:
return size_text(v.w, v.d, v.h)
return tr("{w}×{d}×{h} m").format(w=f"{v.w:.1f}", d=f"{v.d:.1f}", h=f"{v.h:.1f}")
def _plane_info(self, i: int) -> str:
p = self.room.planes[i]
-34
View File
@@ -1,34 +0,0 @@
#include <assert.h>
#include <stdint.h>
#include <openxr/openxr.h>
static int swapchain_size(XrSwapchain handle, uint32_t *w, uint32_t *h) {
if (!handle) return 0;
*w = handle == (XrSwapchain)(uintptr_t)2 ? 1024 : 2768; *h = *w;
return 1;
}
#include "fixed_resolution.h"
int main(void) {
XrExtent2Di full = {0};
XrCompositionLayerProjectionView views[2] = {{XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW},
{XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW}};
XrCompositionLayerProjection p = {XR_TYPE_COMPOSITION_LAYER_PROJECTION};
p.views = views; p.viewCount = 2;
for (int i=0; i<2; ++i) {
views[i].subImage.swapchain = (XrSwapchain)(uintptr_t)1;
views[i].subImage.imageRect = (XrRect2Di){{0,64},{2704,2704}};
}
assert(fixed_layer_size((void *)&p, &full));
assert(full.width == 2768 && full.height == 2768);
assert(views[0].subImage.imageRect.extent.width == 2704); // never stretch a submitted subrect
views[1].subImage.swapchain = (XrSwapchain)(uintptr_t)2;
assert(!fixed_layer_size((void *)&p, &full));
views[1].subImage.swapchain = XR_NULL_HANDLE;
assert(!fixed_layer_size((void *)&p, &full));
p.viewCount=0; assert(!fixed_layer_size((void *)&p, &full));
assert(!fixed_layer_size(NULL, &full));
XrCompositionLayerQuad q = {XR_TYPE_COMPOSITION_LAYER_QUAD};
q.subImage.swapchain=(XrSwapchain)(uintptr_t)2;
assert(fixed_layer_size((void *)&q, &full) && full.width == 1024);
q.type=XR_TYPE_UNKNOWN; assert(!fixed_layer_size((void *)&q, &full));
return 0;
}
-45
View File
@@ -1,45 +0,0 @@
#include <assert.h>
#include <stdint.h>
#include <stddef.h>
#include <stdarg.h>
#include <openxr/openxr.h>
static int missing, valid_size=1, calls;
static XrInstance active_instance;
static XrResult XRAPI_CALL runtime(XrSession session, const XrRecommendedLayerResolutionGetInfoMETA *info,
XrRecommendedLayerResolutionMETA *out) {
(void)session; (void)info; ++calls;
out->recommendedImageDimensions=(XrExtent2Di){2704,2704};
out->isValid=XR_FALSE;
return XR_SUCCESS;
}
static PFN_xrVoidFunction lookup(XrInstance instance, const char *name) {
(void)instance; (void)name; return missing ? NULL : (PFN_xrVoidFunction)runtime;
}
static int swapchain_size(XrSwapchain handle, uint32_t *w, uint32_t *h) {
(void)handle; *w=2768; *h=2768; return valid_size;
}
static void log_stub(const char *fmt, ...) { (void)fmt; }
#define LOG(...) log_stub(__VA_ARGS__)
#include "fixed_resolution.h"
/* WRAPPER */
int main(void) {
XrCompositionLayerQuad layer={XR_TYPE_COMPOSITION_LAYER_QUAD};
XrRecommendedLayerResolutionGetInfoMETA info={XR_TYPE_RECOMMENDED_LAYER_RESOLUTION_GET_INFO_META};
info.layer=(void *)&layer;
XrRecommendedLayerResolutionMETA out={XR_TYPE_RECOMMENDED_LAYER_RESOLUTION_META};
int tail=42; out.next=&tail;
for (missing=0; missing<2; ++missing) {
assert(fixed_recommended_resolution(XR_NULL_HANDLE,&info,&out)==XR_SUCCESS);
assert(out.isValid==XR_TRUE && out.recommendedImageDimensions.width==2768);
assert(out.recommendedImageDimensions.height==2768 && out.next==&tail && tail==42);
}
assert(calls==1);
valid_size=0; missing=0;
assert(fixed_recommended_resolution(XR_NULL_HANDLE,&info,&out)==XR_SUCCESS);
assert(out.recommendedImageDimensions.width==2704 && !out.isValid);
missing=1;
assert(fixed_recommended_resolution(XR_NULL_HANDLE,&info,&out)==XR_ERROR_FUNCTION_UNSUPPORTED);
assert(fixed_recommended_resolution(XR_NULL_HANDLE,NULL,&out)==XR_ERROR_VALIDATION_FAILURE);
assert(fixed_recommended_resolution(XR_NULL_HANDLE,&info,NULL)==XR_ERROR_VALIDATION_FAILURE);
return 0;
}
-15
View File
@@ -5,15 +5,8 @@
#include <string.h>
#include <openxr/openxr.h>
static int enabled, requested;
static XrResult XRAPI_CALL recommended(XrSession session, const XrRecommendedLayerResolutionGetInfoMETA *info,
XrRecommendedLayerResolutionMETA *out) {
(void)session; (void)info;
out->recommendedImageDimensions = (XrExtent2Di){2768,2768}; out->isValid = XR_TRUE;
return XR_SUCCESS;
}
static int query(void) { return enabled; }
static PFN_xrVoidFunction extension(const char *name) {
if (enabled && !strcmp(name,"xrGetRecommendedLayerResolutionMETA")) return (PFN_xrVoidFunction)recommended;
return !strcmp(name,"framebridge_hide_fb_foveation") ? (PFN_xrVoidFunction)query : NULL;
}
static XrResult XRAPI_CALL enumerate(const char *layer,uint32_t capacity,uint32_t *count,XrExtensionProperties *out) {
@@ -64,13 +57,5 @@ int main(void) {
assert(((PFN_xrCreateInstance)fn)(&ci,NULL)==XR_SUCCESS);
assert(requested==(enabled ? 1 : 2) && ci.enabledExtensionCount==2);
}
enabled=0;
assert(xrGetInstanceProcAddr((XrInstance)(uintptr_t)1,"xrGetRecommendedLayerResolutionMETA",&fn)
== XR_ERROR_FUNCTION_UNSUPPORTED);
enabled=1;
assert(xrGetInstanceProcAddr((XrInstance)(uintptr_t)1,"xrGetRecommendedLayerResolutionMETA",&fn)==XR_SUCCESS);
XrRecommendedLayerResolutionMETA out={XR_TYPE_RECOMMENDED_LAYER_RESOLUTION_META};
assert(((PFN_xrGetRecommendedLayerResolutionMETA)fn)(XR_NULL_HANDLE,NULL,&out)==XR_SUCCESS);
assert(out.isValid && out.recommendedImageDimensions.width==2768);
return 0;
}
-354
View File
@@ -1,354 +0,0 @@
// Host runtime/GL doubles exercise the actual adapter implementation, without a headset.
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <pthread.h>
#include <EGL/egl.h>
#include <GLES3/gl3.h>
#include <openxr/openxr.h>
#define CHECK(c) \
do { \
if (!(c)) { \
fprintf(stderr, "line %d: %s\n", __LINE__, #c); \
return 1; \
} \
} while (0)
#include <stdarg.h>
static void log_ignore(const char *fmt, ...) { (void)fmt; }
#define LOG(...) log_ignore(__VA_ARGS__)
static uintptr_t fake = 100;
#define FAKE_HANDLE(t) ((t)(++fake))
static XrInstance active_instance;
static int swapchain_pool = 1;
static int creates, destroys, acquires, waits, releases, blits, deleted, fail_create, fail_enum, fail_wait,
fail_release;
static int bad_gl, fail_gl, scissor = 1, read_fb = 31, draw_fb = 32, texture = 33;
static GLuint next_texture = 1000;
typedef struct {
XrStructureType type;
void *next;
uint32_t image;
} emul_gles_image;
static XrResult create(XrSession s, const XrSwapchainCreateInfo *ci, XrSwapchain *h) {
(void)s;
(void)ci;
++creates;
if (fail_create) return XR_ERROR_LIMIT_REACHED;
*h = FAKE_HANDLE(XrSwapchain);
return XR_SUCCESS;
}
static XrResult destroy(XrSwapchain h) {
(void)h;
++destroys;
return XR_SUCCESS;
}
static XrResult enumerate(XrSwapchain h, uint32_t cap, uint32_t *n, XrSwapchainImageBaseHeader *images) {
(void)h;
(void)cap;
if (fail_enum) return XR_ERROR_RUNTIME_FAILURE;
*n = 3;
for (int j = 0; j < 3; ++j) ((emul_gles_image *)images)[j].image = 2000 + j;
return XR_SUCCESS;
}
static XrResult acquire(XrSwapchain h, const XrSwapchainImageAcquireInfo *ai, uint32_t *index) {
(void)h;
(void)ai;
*index = acquires++ % 3;
return XR_SUCCESS;
}
static XrResult wait_image(XrSwapchain h, const XrSwapchainImageWaitInfo *wi) {
(void)h;
(void)wi;
++waits;
return fail_wait ? XR_TIMEOUT_EXPIRED : XR_SUCCESS;
}
static XrResult release(XrSwapchain h, const XrSwapchainImageReleaseInfo *ri) {
(void)h;
(void)ri;
++releases;
return fail_release ? XR_ERROR_RUNTIME_FAILURE : XR_SUCCESS;
}
static PFN_xrVoidFunction lookup(XrInstance instance, const char *name) {
(void)instance;
#define PROC(n, f) \
if (!strcmp(name, n)) return (PFN_xrVoidFunction)f
PROC("xrCreateSwapchain", create);
PROC("xrDestroySwapchain", destroy);
PROC("xrEnumerateSwapchainImages", enumerate);
PROC("xrAcquireSwapchainImage", acquire);
PROC("xrWaitSwapchainImage", wait_image);
PROC("xrReleaseSwapchainImage", release);
return NULL;
}
static void note_swapchain_size(XrSwapchain h, uint32_t w, uint32_t he) {
(void)h;
(void)w;
(void)he;
}
static void remember_swapchain(XrSwapchain h) { (void)h; }
static void forget_swapchain(XrSwapchain h) { (void)h; }
static void emul_on_create_swapchain(XrSwapchain h, const XrSwapchainCreateInfo *ci) {
(void)h;
(void)ci;
}
static void emul_on_destroy_swapchain(XrSwapchain h) { (void)h; }
#include "pool_clamp.h"
static EGLContext current(void) { return (EGLContext)1; }
static EGLContext (*p_eglGetCurrentContext)(void) = current;
static void (*p_eglCreateContext)(void);
static int emul_load_gl(void) { return 1; }
static void p_glGetIntegerv(GLenum p, GLint *v) {
*v = p == GL_TEXTURE_BINDING_2D ? texture : p == GL_READ_FRAMEBUFFER_BINDING ? read_fb : draw_fb;
}
static void p_glGenTextures(GLsizei n, GLuint *v) {
while (n--) *v++ = next_texture++;
}
static void p_glBindTexture(GLenum target, GLuint t) {
(void)target;
texture = t;
}
static void p_glTexStorage2D(GLenum t, GLsizei levels, GLenum f, GLsizei w, GLsizei h) {
(void)t;
(void)levels;
(void)f;
(void)w;
(void)h;
}
static GLenum p_glGetError(void) { return fail_gl ? GL_INVALID_OPERATION : GL_NO_ERROR; }
static void p_glDeleteTextures(GLsizei n, const GLuint *v) {
(void)v;
deleted += n;
}
static GLsync p_glFenceSync(GLenum c, GLbitfield f) {
(void)c;
(void)f;
return (GLsync)1;
}
static void p_glDeleteSync(GLsync s) { (void)s; }
static void p_glWaitSync(GLsync s, GLbitfield f, GLuint64 t) {
(void)s;
(void)f;
(void)t;
}
static void p_glFlush(void) {}
static GLboolean p_glIsEnabled(GLenum c) {
(void)c;
return scissor;
}
static void p_glEnable(GLenum c) {
(void)c;
scissor = 1;
}
static void p_glDisable(GLenum c) {
(void)c;
scissor = 0;
}
static void p_glGenFramebuffers(GLsizei n, GLuint *f) {
while (n--) *f++ = next_texture++;
}
static void p_glDeleteFramebuffers(GLsizei n, const GLuint *f) {
(void)n;
(void)f;
}
static void p_glBindFramebuffer(GLenum t, GLuint f) {
if (t == GL_READ_FRAMEBUFFER)
read_fb = f;
else
draw_fb = f;
}
static void p_glFramebufferTexture2D(GLenum t, GLenum a, GLenum tex, GLuint id, GLint level) {
(void)t;
(void)a;
(void)tex;
(void)id;
(void)level;
}
static GLenum p_glCheckFramebufferStatus(GLenum target) {
(void)target;
return bad_gl ? 0 : GL_FRAMEBUFFER_COMPLETE;
}
static void p_glBlitFramebuffer(GLint x, GLint y, GLint w, GLint h, GLint dx, GLint dy, GLint dw, GLint dh,
GLbitfield mask, GLenum filter) {
(void)x;
(void)y;
(void)w;
(void)h;
(void)dx;
(void)dy;
(void)dw;
(void)dh;
(void)mask;
(void)filter;
++blits;
}
#include "swapchain_pool.c"
static int released(XrSwapchain h) {
int i = pool_find(h);
uint32_t index;
CHECK(pool_cycle(i, 0, &index) == XR_SUCCESS && index == 0);
CHECK(pool_cycle(i, 1, NULL) == XR_SUCCESS);
CHECK(pool_cycle(i, 2, NULL) == XR_SUCCESS);
return 0;
}
int main(void) {
pool_session = (XrSession)1;
XrSwapchainCreateInfo ci = {XR_TYPE_SWAPCHAIN_CREATE_INFO};
ci.width = 1024;
ci.height = 128;
ci.format = GL_SRGB8_ALPHA8;
ci.faceCount = ci.arraySize = ci.sampleCount = ci.mipCount = 1;
ci.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT;
XrSwapchain first, second, h;
CHECK(!pool_should_create(pool_session));
for (int k = 0; k < 8; ++k) pool_note_runtime(pool_session, (XrSwapchain)(uintptr_t)(k + 500));
CHECK(pool_should_create(pool_session));
pool_forget_runtime((XrSwapchain)500);
CHECK(!pool_should_create(pool_session));
CHECK(!pool_create((XrSession)2, &ci, &h, 0)); // Vulkan/other session
swapchain_pool = 0;
CHECK(!pool_create(pool_session, &ci, &h, 0));
swapchain_pool = 1;
ci.arraySize = 2;
CHECK(!pool_create(pool_session, &ci, &h, 0));
ci.arraySize = 1;
ci.sampleCount = 4;
CHECK(!pool_create(pool_session, &ci, &h, 0));
ci.sampleCount = 1;
ci.format = GL_DEPTH_COMPONENT24;
CHECK(!pool_create(pool_session, &ci, &h, 0));
ci.format = GL_SRGB8_ALPHA8;
CHECK(pool_create(pool_session, &ci, &first, 0) && texture == 33 && creates == 1);
CHECK(pool_create(pool_session, &ci, &second, 1) && creates == 1);
int i = pool_find(first), j = pool_find(second);
uint32_t n, index;
CHECK(pool_enumerate(i, 0, &n, NULL) == XR_SUCCESS && n == 1);
emul_gles_image image = {XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_ES_KHR};
CHECK(pool_enumerate(i, 1, &n, (void *)&image) == XR_SUCCESS && image.image == pool_images[i].tex);
CHECK(pool_cycle(i, 1, NULL) == XR_ERROR_CALL_ORDER_INVALID);
CHECK(pool_cycle(i, 0, NULL) == XR_ERROR_VALIDATION_FAILURE);
CHECK(pool_cycle(i, 0, &index) == XR_SUCCESS);
CHECK(pool_cycle(i, 0, &index) == XR_ERROR_CALL_ORDER_INVALID);
CHECK(pool_cycle(i, 2, NULL) == XR_ERROR_CALL_ORDER_INVALID);
CHECK(pool_cycle(i, 1, NULL) == XR_SUCCESS && pool_cycle(i, 2, NULL) == XR_SUCCESS);
CHECK(released(second) == 0);
XrSwapchainSubImage src = {second, {{10, 5}, {100, 50}}, 0}, sub = src;
pool_begin_frame();
CHECK(pool_subimage(&sub));
CHECK(sub.swapchain == pool_atlas[0].handle && sub.imageRect.offset.x == pool_images[j].rect.x + 10);
CHECK(sub.imageRect.offset.y == 5 && sub.imageRect.extent.width == 100);
CHECK(read_fb == 31 && draw_fb == 32 && scissor == 1 && texture == 33);
sub = src;
CHECK(pool_subimage(&sub) && blits == 1); // two eyes/strips copy source once
CHECK(acquires == 1 && waits == 1 && releases == 0 && pool_finish() && releases == 1);
pool_begin_frame();
sub = src;
CHECK(pool_subimage(&sub) && blits == 2 && acquires == 2);
CHECK(pool_finish());
// Failed wait keeps ownership, retries the same acquired image, never releases before wait.
pool_begin_frame();
fail_wait = 1;
sub = src;
CHECK(!pool_subimage(&sub));
CHECK(pool_finish());
CHECK(acquires == 3 && releases == 2);
fail_wait = 0;
CHECK(pool_subimage(&sub) && acquires == 3);
CHECK(pool_finish());
pool_begin_frame();
bad_gl = 1;
sub = src;
CHECK(!pool_subimage(&sub));
CHECK(pool_finish());
bad_gl = 0;
CHECK(read_fb == 31 && draw_fb == 32 && scissor == 1);
pool_begin_frame();
sub = src;
CHECK(pool_subimage(&sub));
fail_release = 1;
CHECK(!pool_finish());
fail_release = 0;
CHECK(pool_finish());
// Final layer submission preserves order, const inputs, strip crops, stereo and cylinder rects.
XrCompositionLayerQuad q = {XR_TYPE_COMPOSITION_LAYER_QUAD};
q.subImage = src;
XrCompositionLayerCylinderKHR cylinder = {XR_TYPE_COMPOSITION_LAYER_CYLINDER_KHR};
cylinder.subImage = src;
XrCompositionLayerProjectionView views[2] = {{XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW},
{XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW}};
views[0].subImage = views[1].subImage = src;
XrCompositionLayerProjection projection = {XR_TYPE_COMPOSITION_LAYER_PROJECTION};
projection.viewCount = 2;
projection.views = views;
const XrCompositionLayerBaseHeader *layers[] = {(void *)&projection, (void *)&q, (void *)&cylinder, (void *)&q};
PoolFrame frame;
int before_blits = blits, before_acquires = acquires, before_releases = releases;
CHECK(pool_rewrite_layers(layers, 4, &frame) == 4);
CHECK(layers[0]->type == XR_TYPE_COMPOSITION_LAYER_PROJECTION &&
layers[2]->type == XR_TYPE_COMPOSITION_LAYER_CYLINDER_KHR);
CHECK(frame.views[0][1].subImage.swapchain == pool_atlas[0].handle &&
frame.cylinders[2].subImage.imageRect.offset.x == pool_images[j].rect.x + 10);
CHECK(q.subImage.swapchain == second && views[1].subImage.swapchain == second);
CHECK(blits == before_blits + 1 && acquires == before_acquires + 1 && releases == before_releases + 1);
sub = src;
sub.imageRect = (XrRect2Di){{0, 0}, {0, 0}};
pool_begin_frame();
CHECK(pool_subimage(&sub) && sub.imageRect.extent.width == 1024);
CHECK(pool_finish());
sub = src;
sub.imageRect.offset.x = 2000;
pool_begin_frame();
CHECK(!pool_subimage(&sub));
CHECK(pool_finish());
sub = src;
sub.imageRect.offset.x = -10;
pool_begin_frame();
CHECK(pool_subimage(&sub) && sub.imageRect.offset.x == pool_images[j].rect.x && sub.imageRect.extent.width == 90);
CHECK(pool_finish());
PoolRect old = pool_images[i].rect;
pool_destroy(i);
CHECK(pool_create(pool_session, &ci, &h, 0) && pool_images[pool_find(h)].rect.x == old.x);
// Reporter-sized submenu images keep sharing one runtime handle even when further creates are refused.
int before_creates = creates;
fail_create = 1;
int dimensions[][2] = {{1024, 128}, {1120, 440}, {2048, 256}, {600, 440}, {1000, 440}, {512, 256}, {400, 60}};
for (int k = 0; k < 7; ++k) {
ci.width = dimensions[k][0];
ci.height = dimensions[k][1];
CHECK(pool_create(pool_session, &ci, &h, 1));
}
CHECK(creates == before_creates);
fail_create = 0;
// Full atlas spills to a second one, then refuses instead of overlapping.
pool_cleanup(pool_session);
pool_session = (XrSession)1;
ci.width = ci.height = 4096;
CHECK(pool_create(pool_session, &ci, &first, 0));
CHECK(pool_create(pool_session, &ci, &second, 0));
CHECK(pool_images[pool_find(first)].atlas != pool_images[pool_find(second)].atlas);
CHECK(!pool_create(pool_session, &ci, &h, 0));
pool_cleanup(pool_session);
CHECK(!pool_session && pool_find(first) < 0);
pool_session = (XrSession)1;
fail_create = 1;
CHECK(!pool_create(pool_session, &ci, &h, 1));
fail_create = 0;
fail_enum = 1;
CHECK(!pool_create(pool_session, &ci, &h, 0));
CHECK(!pool_atlas[0].handle);
fail_enum = 0;
fail_gl = 1;
CHECK(!pool_create(pool_session, &ci, &h, 0));
fail_gl = 0;
pool_cleanup(pool_session);
pool_error();
CHECK(pool_errors == 1 && deleted > 0 && destroys > 0);
PoolRect occupied[4] = {{0, 0, 2, 2}, {2, 0, 2, 2}, {0, 2, 2, 2}, {2, 2, 2, 2}}, out;
CHECK(!pool_place(occupied, 4, 4, 1, 1, &out));
CHECK(!pool_place(occupied, 0, 4, 5, 1, &out));
CHECK(!pool_place(occupied, 0, 4, 0, 1, &out));
occupied[1] = occupied[3];
CHECK(pool_place(occupied, 3, 4, 2, 2, &out) && out.x == 2 && out.y == 0);
puts("ok");
return 0;
}
+1 -2
View File
@@ -94,8 +94,7 @@ def test_launcher_sync_and_upgrade(monkeypatch, tmp_path, capsys):
assert '9>&- & scanwatch=$!' in text
assert text.index("_room_sync") < text.index("\nsetsid ")
assert a.set_room_sync_line(text, pkg, True) == text
old = "\n".join(line for line in text.splitlines()
if "# frameport-room-sync" not in line and "# frameport-scan-watch" not in line) + "\n"
old = "\n".join(line for line in text.splitlines() if "# frameport-room-sync" not in line and "# frameport-scan-watch" not in line) + "\n"
(anchor / "launch.sh").write_text(old)
a.upgrade_launchers()
assert (anchor / "launch.sh").read_text().count("# frameport-room-sync") == 1
-16
View File
@@ -94,19 +94,3 @@ def test_library_load_with_a_cold_catalog_does_not_recurse(monkeypatch):
monkeypatch.setattr(library, "load", counting)
library.load()
assert catalog.remote_enabled() is False and len(loads) <= 2
def test_swapchain_pool_catalog_is_gated_for_older_apps(monkeypatch):
from pathlib import Path
import yaml
from frameport.patches import base
path = Path(__file__).resolve().parents[1] / "catalog/games/cn.vr4p.oculus4xvrplayerov.yaml"
entry = yaml.safe_load(path.read_text())
assert entry["adapter"]["swapchain_pool"] == 1
assert catalog.unusable_reason(entry) is None
# Older installed apps see this catalog from main, but cannot register the new adapter key.
monkeypatch.delitem(base.REGISTRY, "adapter.swapchain_pool")
assert "adapter.swapchain_pool" in catalog.unusable_reason(entry)
-87
View File
@@ -1,87 +0,0 @@
"""Full swapchain recommendations and opt-in OVRPlugin routing."""
import os
import shutil
import subprocess
from pathlib import Path
import pytest
from test_patches import _analysis
from frameport.patches import base
from frameport.patches.frame.adapter import needs_xrshim
from frameport.patches.settings import UI, adapter_settings
ROOT = Path(__file__).resolve().parents[1]
def test_setting_is_opt_in_and_routes_ovrplugin():
base.load_all()
patch = base.get("adapter.fixed_resolution")
assert patch.default == 0
assert patch.detect(_analysis()) is None
assert not adapter_settings({}).get("fixed_resolution")
assert needs_xrshim({patch.id: {"value": 1}})
assert not needs_xrshim({patch.id: {"value": 0}, "adapter.haptic_fix": {"value": 0}})
assert UI[patch.key]["group"] == "picture"
assert patch.applies(_analysis(xr="OpenXR", libs=[]))
assert patch.applies(_analysis(xr="VrApi", libs=["libOVRPlugin.so"]))
assert not patch.applies(_analysis(xr="VrApi", libs=[]))
assert not patch.applies(_analysis(abis=["armeabi-v7a"], xr="OpenXR"))
@pytest.mark.native
def test_layer_dimensions(tmp_path):
inc = next(iter((ROOT / "native/.cache").glob("openxr-*")), None)
cc = shutil.which("cc")
if not os.environ.get("FRAMEPORT_NATIVE_TESTS") or not inc or not cc:
pytest.skip("needs FRAMEPORT_NATIVE_TESTS=1, OpenXR headers and cc")
exe = tmp_path / "fixed-resolution"
subprocess.run([cc, "-std=c11", "-Wall", "-Wextra", "-Werror",
"-Wno-missing-field-initializers", "-I", str(inc),
"-I", str(ROOT / "native/adapter"),
str(ROOT / "tests/fixtures/src/fixed_resolution_test.c"), "-o", str(exe)], check=True)
subprocess.run([str(exe)], check=True)
def test_triage_only_offers_fix_for_offset_rects():
from frameport.validate.triage import triage
log = "projection view 0 rect change: swapchain=0x123 rect=0,32 2736x2736 array=0"
found = [f for f in triage(log).findings if f.id == "dynamic-resolution-foveation"]
assert found and found[0].suggest == ["adapter.fixed_resolution"] and found[0].question
assert not any(f.id == "dynamic-resolution-foveation" for f in
triage(log.replace("rect=0,32", "rect=0,0")).findings)
assert not any(f.id == "dynamic-resolution-foveation" for f in
triage(log + "\nfixed_resolution: recommended 2704x2704 -> 2768x2768").findings)
def test_catalog_requires_app_with_the_fix(monkeypatch):
import yaml
import frameport
from frameport.recommend.catalog import unusable_reason
entry = yaml.safe_load((ROOT / "catalog/games/com.camouflaj.manta.yaml").read_text())
assert entry["adapter"]["fixed_resolution"] == 1
monkeypatch.setattr(frameport, "__version__", "1.2.1")
assert unusable_reason(entry) == "needs FramePort 1.2.2"
monkeypatch.setattr(frameport, "__version__", "1.2.2")
assert unusable_reason(entry) is None
@pytest.mark.native
def test_recommendation_wrapper(tmp_path):
inc = next(iter((ROOT / "native/.cache").glob("openxr-*")), None)
cc = shutil.which("cc")
if not os.environ.get("FRAMEPORT_NATIVE_TESTS") or not inc or not cc:
pytest.skip("needs FRAMEPORT_NATIVE_TESTS=1, OpenXR headers and cc")
# Compile the actual wrapper with a stand-in runtime and swapchain table.
source = (ROOT / "native/adapter/frame_adapter.c").read_text()
wrapper = source[source.index("static XRAPI_ATTR XrResult XRAPI_CALL fixed_recommended_resolution"):
source.index("// Clamp `r` to a w x h image;")]
harness = (ROOT / "tests/fixtures/src/fixed_resolution_wrapper_test.c").read_text()
unit = tmp_path / "wrapper.c"
unit.write_text(harness.replace("/* WRAPPER */", wrapper))
exe = tmp_path / "wrapper"
subprocess.run([cc, "-std=c11", "-Wall", "-Wextra", "-Werror",
"-Wno-missing-field-initializers", "-I", str(inc),
"-I", str(ROOT / "native/adapter"), str(unit), "-o", str(exe)], check=True)
subprocess.run([str(exe)], check=True)
-11
View File
@@ -1065,14 +1065,3 @@ def test_hide_fb_foveation_setting():
assert "hide_fb_foveation" in XRSHIM_SETTINGS
assert needs_xrshim(recipe)
assert UI["hide_fb_foveation"]["level"] == "advanced"
def test_swapchain_pool_is_opt_in_and_gles_only():
patch = base.get("adapter.swapchain_pool")
assert patch.default == 0 and patch.revision == 1
assert "swapchain_pool" not in adapter_settings({})
assert adapter_settings({"adapter.swapchain_pool": {"value": "1"}})["swapchain_pool"] == 1
gles = _analysis(graphics="GLES or unknown (no Vulkan declaration)")
assert patch.applies(gles) and patch.detect(gles) is None
assert not patch.applies(_analysis(graphics="Vulkan"))
assert "Vulkan" in patch.description
-7
View File
@@ -372,10 +372,3 @@ class _local_dir:
def __exit__(self, *a):
return False
def test_box_sizes_keep_thin_sizes_readable():
from frameport.ui.views.room_edit import metres, size_text
assert metres(0.03) == "0.03" and metres(0.07) == "0.07" and metres(0.5) == "0.5" and metres(2.05) == "2.0"
assert size_text(1.6, 1.1, 0.03) == "1.6×1.1×0.03 m"
+8 -241
View File
@@ -54,9 +54,9 @@ def run(scanner, dump, directory, *args, success=True):
return report
def room(directory, name="room.txt"):
def room(directory):
records = {}
lines = (directory / name).read_text().splitlines()
lines = (directory / "room.txt").read_text().splitlines()
assert lines[0] == "frameport-room 1"
for line in lines:
if not line or line.startswith("#"):
@@ -95,8 +95,8 @@ def assert_walls(polygon, expected):
(a[1] + b[1]) / 2 + .04 * dx / length, polygon)
def mesh(directory, name="room-mesh.bin"):
data = (directory / name).read_bytes()
def mesh(directory):
data = (directory / "room-mesh.bin").read_bytes()
magic, version, nv, ni = struct.unpack_from("<4sIII", data)
assert (magic, version) == (b"FPRM", 1)
assert 0 < ni // 3 <= 200_000 and ni % 3 == 0
@@ -115,8 +115,7 @@ def test_box_room_geometry_furniture_and_wire_format(scanner, dumps, tmp_path):
assert report["coverage_percent"] == 100
assert abs(report["floor_y"]) < .05
assert abs(report["ceiling_y"] - 2.6) < .05
# The scanner's own output is kept as the raw files; room.txt + room-mesh.bin are refined (tests further down).
records = room(tmp_path, "room-raw.txt")
records = room(tmp_path)
assert records["universe"] == [["18446744073709551615"]]
assert records["source"] == [["scan"]]
assert_walls(outline(records), synth.BOX_OUTLINE)
@@ -132,12 +131,8 @@ def test_box_room_geometry_furniture_and_wire_format(scanner, dumps, tmp_path):
ids = [row[0] for kind in ("room", "floor", "ceiling", "wall", "volume", "mesh") for row in records[kind]]
assert len(ids) == len(set(ids))
assert all(re.fullmatch("[0-9a-f]{32}", value) for value in ids)
vertices, indices = mesh(tmp_path, "room-mesh-raw.bin")
assert records["mesh"][0][1:] == ["room-mesh-raw.bin", str(len(vertices)), str(len(indices))]
refined = room(tmp_path)
assert [row[0] for row in refined["room"] + refined["floor"] + refined["wall"]] == \
[row[0] for row in records["room"] + records["floor"] + records["wall"]] # same anchors as the raw room
assert refined["mesh"][0][1:3] == ["room-mesh.bin", str(len(mesh(tmp_path)[0]))]
vertices, indices = mesh(tmp_path)
assert records["mesh"][0][1:] == ["room-mesh.bin", str(len(vertices)), str(len(indices))]
# Normals on the structural surfaces must face the room interior.
correct = checked = 0
for i in range(0, len(indices), 3):
@@ -355,14 +350,11 @@ def test_roomview_style_scan_retains_room_and_furniture(scanner, roomview_dump,
assert report["far_vertices"] > 1000
assert report["detected_walls"] == 4
assert abs(report["floor_y"]) < .05 and abs(report["ceiling_y"] - 2.6) < .05
records = room(tmp_path, "room-raw.txt")
records = room(tmp_path)
assert_walls(outline(records), synth.BOX_OUTLINE)
assert any(row[1] == "TABLE" and abs(float(row[4]) - .75) < .05 for row in records["volume"])
assert any(row[1] == "COUCH" for row in records["volume"])
mesh(tmp_path, "room-mesh-raw.bin")
mesh(tmp_path)
assert report["heads"] == 28 # the tracked frames' head path, for the walked-volume pass
assert len(json.loads((tmp_path / "scan.json").read_text())["heads"]) == 28
def test_depth_discontinuity_is_rejected_below_absolute_edge_limit(scanner, dumps, tmp_path):
@@ -396,228 +388,3 @@ def test_small_ceiling_patch_does_not_override_default(scanner, dumps, tmp_path)
report = run(scanner, path, tmp_path / "out", "--ceiling-default", "2.7")
assert "ceiling not seen" in report["warnings"]
assert abs(report["ceiling_y"] - report["floor_y"] - 2.7) < .001
# --- Refinement + label rules (docs/mr/scan.md "Refinement") ---------------------------------------------------------
def refine_only(scanner, directory, *args, success=True):
proc = subprocess.run([str(scanner), "--refine-only", str(directory), "--json", *args],
text=True, capture_output=True, timeout=15)
assert proc.returncode == (0 if success else 2), proc.stdout + proc.stderr
report = json.loads(proc.stdout)
assert report["ok"] is success
return report
def centroids(directory, name="room-mesh.bin"):
vertices, indices = mesh(directory, name)
return [[sum(vertices[indices[i + k]][a] for k in range(3)) / 3 for a in range(3)]
for i in range(0, len(indices), 3)]
def in_box(c, box, pad=.06):
(cx, bottom, cz), (w, h, d) = box
return abs(c[0] - cx) <= w / 2 + pad and bottom - pad <= c[1] <= bottom + h + pad and abs(c[2] - cz) <= d / 2 + pad
def masked(text, mesh_name=None):
text = re.sub(r"(?m)^time \d+", "time T", re.sub(r"[0-9a-f]{32}", "U", text))
return text.replace(f" {mesh_name} ", " MESH ") if mesh_name else text
@pytest.fixture(scope="module")
def clutter_dump(tmp_path_factory):
path = tmp_path_factory.mktemp("clutter") / "clutter.fprs"
synth.render(path, profile="legacy", extra=[synth.TUBE, synth.GHOST])
return path
@pytest.fixture(scope="module")
def box_scan(scanner, dumps, tmp_path_factory):
out = tmp_path_factory.mktemp("box-scan") / "room"
return out, run(scanner, dumps["box"], out)
def test_floating_tube_and_walked_through_ghost_are_removed(scanner, clutter_dump, tmp_path):
report = run(scanner, clutter_dump, tmp_path / "default")["refine"]
raw, refined = centroids(tmp_path / "default", "room-mesh-raw.bin"), centroids(tmp_path / "default")
for box in (synth.TUBE, synth.GHOST):
assert sum(in_box(c, box) for c in raw) > 100 # the scanner meshed both
assert not any(in_box(c, box) for c in refined)
assert report["thin_pieces"] > 100 and report["walked"] > 100
# Each piece goes for its own reason: without the walked volume the ghost stays, without tubes the pole stays.
run(scanner, clutter_dump, tmp_path / "unwalked", "--walked", "0")
assert sum(in_box(c, synth.GHOST) for c in centroids(tmp_path / "unwalked")) > 100
run(scanner, clutter_dump, tmp_path / "untubed", "--tubes", "0")
assert sum(in_box(c, synth.TUBE) for c in centroids(tmp_path / "untubed")) > 100
# Real furniture survives: the table top is flattened onto its box, not removed.
assert any(in_box(c, synth.TABLE) and abs(c[1] - .75) < .06 for c in refined)
def test_panels_wind_into_the_room_on_the_reestimated_planes(box_scan):
out, report = box_scan
refine = report["refine"]
assert refine["panel_triangles"] == 12 # four seen walls + floor and ceiling (two ears each)
vertices, indices = mesh(out)
records = room(out)
ceiling = float(records["ceiling"][0][1])
assert abs(ceiling - refine["ceiling_y"]) < 1e-4 and abs(ceiling - 2.6) < .05
assert all(float(w[6]) == ceiling for w in records["wall"]) # wall tops follow the re-estimated ceiling
panels = len(indices) // 3 - refine["panel_triangles"]
for t in range(panels, len(indices) // 3):
a, b, c = (vertices[indices[3 * t + k]] for k in range(3))
u, v = [b[k] - a[k] for k in range(3)], [c[k] - a[k] for k in range(3)]
normal = (u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0])
centroid = [(a[k] + b[k] + c[k]) / 3 for k in range(3)]
assert sum(n * (o - p) for n, o, p in zip(normal, (0, 1.3, 0), centroid, strict=True)) > 0
ys = {round(p[1], 4) for p in (a, b, c)}
assert ys <= {round(refine["floor_y"], 4), round(ceiling, 4)}
def test_refine_only_reproduces_the_scan_and_refine_zero_is_the_old_output(scanner, dumps, box_scan, tmp_path):
out, report = box_scan
copy = tmp_path / "copy"
shutil.copytree(out, copy)
for name in ("room.txt", "room-mesh.bin"):
(copy / name).unlink()
again = refine_only(scanner, copy)
assert again["refine"] == report["refine"]
for name in ("room.txt", "room-mesh.bin", "scan.json", "room-raw.txt", "room-mesh-raw.bin"):
assert (copy / name).read_bytes() == (out / name).read_bytes(), name
# --refine 0 writes what the scanner wrote before refinement existed: its raw output, byte for byte.
plain = tmp_path / "plain"
run(scanner, dumps["box"], plain, "--refine", "0")
assert (plain / "room-mesh.bin").read_bytes() == (out / "room-mesh-raw.bin").read_bytes()
assert masked((plain / "room.txt").read_text(), "room-mesh.bin") == \
masked((out / "room-raw.txt").read_text(), "room-mesh-raw.bin")
# ... and switching refinement back on later gives the default output again (same anchors as its own raw room).
refine_only(scanner, plain)
assert (plain / "room-mesh.bin").read_bytes() == (out / "room-mesh.bin").read_bytes()
assert masked((plain / "room.txt").read_text()) == masked((out / "room.txt").read_text())
def test_scan_json_holds_settings_report_and_heads(box_scan):
out, report = box_scan
scan = json.loads((out / "scan.json").read_text())
assert scan["version"] == 1 and len(scan["heads"]) == report["heads"] > 0
assert all(len(h) == 3 for h in scan["heads"])
assert scan["refine"] == {"refine": 1, "walls": "panels", "band": .35, "facing_deg": 60, "band_in": .12,
"floor_in": .08, "ceil_in": .25, "top_strip": .5, "near": .03, "clip": .05,
"clip_open": .1, "island": .2, "smooth": 8, "tops": .1, "protect": 0, "tubes": 3,
"tube_thick": .15, "walked": .4, "label_rules": 1, "erase": []}
summary = dict(report["refine"])
assert summary.pop("output_triangles") == len(mesh(out)[1]) // 3 and summary.pop("output_volumes") == 2
assert summary == scan["report"]
assert scan["report"]["kept"] + scan["report"]["panel_triangles"] == len(mesh(out)[1]) // 3
assert sorted((label["from"], label["to"]) for label in scan["labels"]) == [("COUCH", "TABLE"), ("TABLE", "TABLE")]
assert scan["suggestions"] == []
def test_head_path_is_decimated_to_two_thousand_points(scanner, dumps, tmp_path):
data = dumps["one"].read_bytes()
empty = b"".join(struct.pack("<d7fII", i, i * .001, 1.6, 0, 0, 0, 0, 1, 0, 0) for i in range(2500))
dump = tmp_path / "long.fprs"
dump.write_bytes(data + empty)
report = run(scanner, dump, tmp_path / "out", "--min-views", "1") # one frame with depth, 2500 without
heads = json.loads((tmp_path / "out" / "scan.json").read_text())["heads"]
assert 1000 <= report["heads"] == len(heads) <= 2000
assert heads[-1][0] > 2 # still spans the whole recording
def test_erase_spheres_remove_the_scan_inside_them(scanner, box_scan, tmp_path):
copy = tmp_path / "copy"
shutil.copytree(box_scan[0], copy)
report = refine_only(scanner, copy, "--erase", "-0.6,0.75,-0.25,0.3;1.5,1.0,-1.0,0.2")["refine"]
assert report["erased"] > 0
assert not any((c[0] + .6) ** 2 + (c[1] - .75) ** 2 + (c[2] + .25) ** 2 <= .09 for c in centroids(copy))
assert json.loads((copy / "scan.json").read_text())["refine"]["erase"] == [[-.6, .75, -.25, .3], [1.5, 1, -1, .2]]
ROOM_IDS = iter(f"{i:032x}" for i in range(1, 1000))
def volume(label, cx, cz, top, yaw, w, d, h):
return f"volume {next(ROOM_IDS)} {label} {cx} {cz} {top} {yaw} {w} {d} {h}\n"
def test_label_rules_on_an_older_room_folder(scanner, tmp_path):
"""Merged and nested boxes and boxes behind a wall go, shapes are relabelled, a door slab becomes a suggestion.
The folder has no raw files (an older scan): its room.txt + mesh are taken as the unrefined scan."""
walls = [(-2, -1.5, 2, -1.5), (2, -1.5, 2, 1.5), (2, 1.5, -2, 1.5), (-2, 1.5, -2, -1.5)]
plane = f"plane {next(ROOM_IDS)} DOOR_FRAME 0.0000 1.0000 1.4900 0.0000 0.0000 0.0000 1.0000 0.9000 2.0000\n"
text = (f"frameport-room 1\nuniverse 7\nsource scan\ntime 1\nroom {next(ROOM_IDS)}\nfloor {next(ROOM_IDS)} 0\n"
f"ceiling {next(ROOM_IDS)} 2.6\noutline 4 -2 -1.5 2 -1.5 2 1.5 -2 1.5\n"
+ "".join(f"wall {next(ROOM_IDS)} {a} {b} {c} {d} 0 2.6\n" for a, b, c, d in walls)
+ volume("TABLE", -1.2, -.5, .75, 0, 1.0, .6, .75)
+ volume("COUCH", .6, -.5, .5, 0, 1.6, .8, .5)
+ volume("OTHER", -.3, -.5, .8, 0, 3.0, 1.0, .8) # holds the table and the couch: merged
+ volume("BED", .6, .85, .55, 0, 2.0, 1.1, .55)
+ volume("OTHER", .6, .85, .45, 0, .5, .4, .45) # inside the bed: part of it
+ volume("OTHER", 1.85, -.8, 1.6, -90, 1.0, .06, .6) # a flat panel along the x = 2 wall: a screen
+ volume("SCREEN", -1.0, -1.45, 2.0, 0, .9, .1, 2.0) # a tall slab by the z = -1.5 wall: a door?
+ volume("STORAGE", 2.6, 0, 1.5, 0, .5, .5, 1.5) # behind a wall
+ plane)
vertices, indices = [], []
def quad(a, b, c, d):
base = len(vertices)
vertices.extend((a, b, c, d))
indices.extend((base, base + 1, base + 2, base, base + 2, base + 3))
for i in range(18): # the screen: a flat 0.9 x 0.5 m grid at x = 1.85
for j in range(10):
z, y = -1.25 + i * .05, 1.05 + j * .05
quad((1.85, y, z), (1.85, y, z + .05), (1.85, y + .05, z + .05), (1.85, y + .05, z))
for i in range(38): # some floor
for j in range(28):
x, z = -1.9 + i * .1, -1.4 + j * .1
quad((x, 0, z), (x, 0, z + .1), (x + .1, 0, z + .1), (x + .1, 0, z))
text += f"mesh {next(ROOM_IDS)} room-mesh.bin {len(vertices)} {len(indices)}\n"
mesh_bytes = (struct.pack("<4sIII", b"FPRM", 1, len(vertices), len(indices))
+ b"".join(struct.pack("<3f", *v) for v in vertices) + struct.pack(f"<{len(indices)}I", *indices))
folder = tmp_path / "room"
folder.mkdir()
(folder / "room.txt").write_text(text)
(folder / "room-mesh.bin").write_bytes(mesh_bytes)
report = refine_only(scanner, folder)
assert "no raw files; the current room was taken as the unrefined scan" in report["warnings"]
assert "no head path; walked volume skipped" in report["warnings"]
assert (folder / "room-mesh-raw.bin").read_bytes() == mesh_bytes
scan = json.loads((folder / "scan.json").read_text())
decisions = [(label["from"], label["to"], label["reason"]) for label in scan["labels"]]
assert decisions == [
("TABLE", "TABLE", "top 0.75 m"),
("COUCH", "COUCH", "seat-height top 0.50 m, 1.6 m long"),
("OTHER", None, "holds a table and a couch 1.8 m apart: several things merged"),
("BED", "BED", "2.2 m², 2.0 m long, top 0.55 m"),
("OTHER", None, "100% inside the bigger bed box: part of it"),
("OTHER", "SCREEN", "thin, raised, screen-shaped, flat (0 cm)"),
("SCREEN", None, "tall thin slab from the floor (2.00 m): a door or closet door?"),
("STORAGE", None, "centre 60 cm outside the room (behind a wall)"),
]
assert scan["suggestions"] == [{"kind": "door", "box": {"cx": -1, "cz": -1.45, "top_y": 2, "yaw_deg": 0,
"w": .9, "d": .1, "h": 2}, "wall": 0, "along": .25,
"reason": decisions[6][2]}]
raw = room(folder, "room-raw.txt")
assert raw["mesh"][0][1] == "room-mesh-raw.bin" and len(raw["volume"]) == 8
refined = room(folder)
assert [row[1] for row in refined["volume"]] == ["TABLE", "COUCH", "BED", "SCREEN"]
assert refined["universe"] == [["7"]] and refined["plane"] == [plane.split()[1:]] # unknown records stay
assert report["refine"]["removed_boxes"] == 4 and report["refine"]["relabelled"] == 1
assert report["refine"]["suggestions"] == 1
first = {name: (folder / name).read_bytes() for name in ("room.txt", "room-mesh.bin", "scan.json")}
refine_only(scanner, folder) # now from the raw files: the same result
assert first == {name: (folder / name).read_bytes() for name in first}
refine_only(scanner, folder, "--label-rules", "0")
assert [row[1] for row in room(folder)["volume"]] == [
"TABLE", "COUCH", "OTHER", "BED", "OTHER", "OTHER", "SCREEN", "STORAGE"]
@pytest.mark.parametrize("args", [("--walls", "shell"), ("--smooth", "1.5"), ("--erase", "1,2,3"), ("--refine", "2"),
("--band", "2"), ("--walked", ".01"), ("--erase", "0,0,0,-1")])
def test_invalid_refinement_options_are_json_errors(scanner, dumps, tmp_path, args):
run(scanner, dumps["one"], tmp_path / "out", *args, success=False)
def test_refine_only_needs_a_room(scanner, tmp_path):
report = refine_only(scanner, tmp_path, success=False)
assert "room-raw.txt" in report["error"]
+6 -10
View File
@@ -36,8 +36,7 @@ def harness(tmp_path_factory):
def run(harness, folder, mode, *args, process="org.frameport.test"):
binary, _ = harness
env = {**os.environ, "FRAMEBRIDGE_CONFIG": str(folder / "settings.conf")}
result = subprocess.run([process, mode, *map(str, args)], executable=binary, capture_output=True, text=True,
env=env)
result = subprocess.run([process, mode, *map(str, args)], executable=binary, capture_output=True, text=True, env=env)
assert result.returncode == 0, result.stdout + result.stderr
return result.stderr
@@ -51,7 +50,7 @@ def room(folder, *, generated_walls=False):
f"volume {uuid(40)} TABLE,OTHER 2 1 0.75 90 2 1 0.75",
f"plane {uuid(41)} WINDOW_FRAME 1 1.5 0 0 0 0 1 0.8 1.2"]
if not generated_walls:
for i, ((x, z), (xx, zz)) in enumerate(zip(points, points[1:] + points[:1], strict=True)):
for i, ((x, z), (xx, zz)) in enumerate(zip(points, points[1:] + points[:1])):
lines.append(f"wall {uuid(10 + i)} {x} {z} {xx} {zz} 0 2.5")
lines += [f"mesh {uuid(42)} room-mesh.bin 4 6", "future-data ignored"]
(folder / "room.txt").write_text("\n".join(lines) + "\n")
@@ -131,8 +130,7 @@ def test_rejects_corrupt_mesh_without_losing_room(harness, tmp_path, damage):
assert "bad room line" in log and "0 mesh triangles" in log
@pytest.mark.parametrize("record", [f"mesh {uuid(80)} ../escape.bin 4 6",
f"mesh {uuid(80)} room-mesh.bin 600001 600000",
@pytest.mark.parametrize("record", [f"mesh {uuid(80)} ../escape.bin 4 6", f"mesh {uuid(80)} room-mesh.bin 600001 600000",
f"mesh {uuid(80)} room-mesh.bin 4 7"])
def test_mesh_caps_and_path_confinement(harness, tmp_path, record):
room(tmp_path)
@@ -141,8 +139,7 @@ def test_mesh_caps_and_path_confinement(harness, tmp_path, record):
run(harness, tmp_path, "room", 5, 0)
@pytest.mark.parametrize("text", [None, "", "frameport-room 2\n", "bad 1\n",
"# comment before magic\nframeport-room 1\n",
@pytest.mark.parametrize("text", [None, "", "frameport-room 2\n", "bad 1\n", "# comment before magic\nframeport-room 1\n",
"frameport-room 1\noutline 3 0 0 1 1 2 2\n"])
def test_box_fallback_unchanged(harness, tmp_path, text):
if text is not None:
@@ -194,7 +191,7 @@ def test_failed_anchor_write_is_reported_and_rolled_back(harness, tmp_path):
def test_missing_outline_and_ceiling_are_derived(harness, tmp_path):
points = [(0, 0), (4, 0), (4, 3), (0, 3)]
lines = ["frameport-room 1", f"room {uuid(1)}", f"floor {uuid(2)} 1"]
for i, ((x, z), (xx, zz)) in enumerate(zip(points, points[1:] + points[:1], strict=True)):
for i, ((x, z), (xx, zz)) in enumerate(zip(points, points[1:] + points[:1])):
lines.append(f"wall {uuid(10 + i)} {x} {z} {xx} {zz} 1 4")
(tmp_path / "room.txt").write_text("\n".join(lines) + "\n")
run(harness, tmp_path, "derived")
@@ -224,8 +221,7 @@ def test_real_shim_bypasses_a_dispatcher_that_refuses_scene_calls(harness, tmp_p
subprocess.run([*common, "-shared", "-fPIC", "-DFAKE_DISPATCH", str(native / "scene_shim_test.c"),
"-o", str(tmp_path / "libopenxr_loader.so")], check=True)
shim = tmp_path / "libframe_xrshim.so"
subprocess.run([*common, "-shared", "-fPIC", "-fvisibility=hidden",
"-I", str(ROOT / "tests/fixtures/src/host_include"),
subprocess.run([*common, "-shared", "-fPIC", "-fvisibility=hidden", "-I", str(ROOT / "tests/fixtures/src/host_include"),
str(ROOT / "native/xrshim/xrshim.c"), "-ldl", "-pthread", "-lm", "-o", str(shim)], check=True)
binary = tmp_path / "test-shim"
subprocess.run([*common, str(native / "scene_shim_test.c"), "-ldl", "-o", str(binary)], check=True)
-188
View File
@@ -1,188 +0,0 @@
"""Embedded loader redirection, independent of any game or device."""
import json
import shutil
import struct
import subprocess
import zipfile
import pytest
from test_agent import load_agent
from test_patches import _analysis
from frameport.analysis import detect
from frameport.analysis import static_openxr as sx
from frameport.apk.workspace import ApkWorkspace
from frameport.core.events import Reporter
from frameport.patches import base
from frameport.patches.frame import static_openxr_loader as patch
@pytest.fixture
def embedded(tmp_path):
# A real ELF layout with synthetic strings; change only e_machine to model an arm64 game.
compiler = shutil.which("cc")
if not compiler:
pytest.skip("needs host C compiler")
strings = [f"/{p}/etc/openxr/1/active_runtime{abi}.json"
for p in ("product", "odm", "oem", "vendor", "system") for abi in ("", ".aarch64", ".arm64-v8a")]
strings += list(sx.BROKERS) + ["RuntimeManifestFile", "xrNegotiateLoaderRuntimeInterface", "unrelated"]
src = tmp_path / "embedded.c"
src.write_text("const char *strings[] = {" + ",".join(json.dumps(s) for s in strings) + "};")
out = tmp_path / "embedded.so"
subprocess.run([compiler, "-shared", "-fPIC", str(src), "-o", str(out)], check=True)
data = bytearray(out.read_bytes())
struct.pack_into("<H", data, 18, 183) # EM_AARCH64
return bytes(data)
def test_detection_and_byte_stable_rewrite(embedded):
assert sx.detected("libengine.so", embedded)
assert not sx.detected("libopenxr_loader.so", embedded)
assert not sx.detected("libengine.so", embedded.replace(b"RuntimeManifestFile", b"X" * 19))
wrong_arch = bytearray(embedded)
struct.pack_into("<H", wrong_arch, 18, 62)
assert not sx.detected("libengine.so", bytes(wrong_arch))
fixed, count = sx.rewrite(embedded)
assert count == 17
assert len(fixed) == len(embedded)
expected = embedded
for old in sx.paths(embedded) + list(sx.BROKERS):
new = sx.MANIFEST if old.startswith("/") else old.replace("org.khronos", "org.fp_null")
expected = expected.replace(old.encode() + b"\0", new.encode().ljust(len(old) + 1, b"\0"))
assert fixed == expected # all other bytes, offsets and ELF metadata are identical
assert sx.rewrite(fixed) == (fixed, 0)
assert not sx.paths(fixed)
assert detect.ANALYSIS_VERSION >= 23
def test_patch_and_adapter_without_overport(tmp_path, monkeypatch, embedded):
inner = tmp_path / "inner.so"
inner.write_bytes(b"unmodified Khronos loader")
monkeypatch.setattr(patch.overport, "ensure_runtime_lib", lambda *args: inner)
apk = tmp_path / "in.apk"
with zipfile.ZipFile(apk, "w") as z:
z.writestr("lib/arm64-v8a/libengine.so", embedded)
a = _analysis(extra={"static_openxr_loader": ["libengine.so"]})
selection = {patch.StaticOpenXRLoader.id: {}, "frame.adapter": {}}
p = patch.StaticOpenXRLoader()
assert p.detect(a).recommended
assert not p.applies(_analysis())
with ApkWorkspace(apk) as ws:
ctx = base.ApkContext(ws, a, {}, Reporter(), selection)
assert p.apply(ctx)
assert base.get("frame.adapter").apply(ctx)
assert ws.read_lib("libopenxr_loader_original.so") == inner.read_bytes()
assert ws.has(ws.lib("libframe_settings.so"))
assert all(ok for _, ok, _ in p.validate(ctx))
assert all(ok for _, ok, _ in base.get("frame.adapter").validate(ctx))
assert not p.apply(ctx)
assert not base.get("frame.adapter").apply(ctx)
out = ws.write(tmp_path / "out.apk")
with ApkWorkspace(out) as ws:
ctx.ws = ws
assert not p.apply(ctx)
ws.put(ws.lib("libengine.so"), embedded)
assert not p.validate(ctx)[0][1]
def test_manifest_repair_and_launcher_upgrade(tmp_path, monkeypatch):
a = load_agent(monkeypatch, tmp_path)
root = tmp_path / "game"
a.write_openxr_runtime_manifest(str(root), False)
assert not root.exists()
a.write_openxr_runtime_manifest(str(root), True)
target = root / "lepton-data/external/fp_openxr_runtime.json"
manifest = json.loads(target.read_text())
assert manifest["runtime"]["library_path"] == patch.SHIM
victim = tmp_path / "victim"
victim.write_text("untouched")
target.unlink()
target.symlink_to(victim)
a.write_openxr_runtime_manifest(str(root), True)
assert victim.read_text() == "untouched"
assert not target.is_symlink()
anchor = tmp_path / "anchors/com.example.game"
anchor.mkdir(parents=True)
a.write_launcher(str(anchor), str(root), "com.example.game", "Test", 123, "/lepton", {}, static_openxr=True)
text = (anchor / "launch.sh").read_text()
assert " _openxr_manifest com.example.game\n" in text
assert a.set_openxr_manifest_line(text, "com.example.game", True) == text
assert "_openxr_manifest" not in a.set_openxr_manifest_line(text, "com.example.game", False)
subprocess.run(["bash", "-n", str(anchor / "launch.sh")], check=True)
def test_runtime_shim_host(tmp_path):
import os
from pathlib import Path
root = Path(__file__).resolve().parents[1]
headers = next((root / "native/.cache").glob("openxr-*/openxr/openxr.h"), None)
cc = shutil.which("cc")
if not headers or not cc:
pytest.skip("needs cached OpenXR headers and host C compiler")
src = root / "native/xrruntime"
inc = str(headers.parent.parent)
subprocess.run([cc, "-shared", "-fPIC", "-I", inc, str(src / "test_bridge.c"),
"-o", str(tmp_path / "libopenxr_loader_generic.so")], check=True)
exe = tmp_path / "shim_test"
subprocess.run([cc, "-Wall", "-Wextra", "-Werror", "-Wno-missing-field-initializers", "-I", inc,
str(src / "xrruntime.c"), str(src / "test.c"), "-ldl", "-lpthread", "-o", str(exe)], check=True)
env = dict(os.environ, LD_LIBRARY_PATH=str(tmp_path))
subprocess.run([str(exe)], env=env, check=True)
(tmp_path / "libopenxr_loader_generic.so").unlink()
subprocess.run([str(exe), "missing"], env=env, check=True)
def test_composed_khronos_paths(tmp_path, embedded):
# Rebuild the fixture with Khronos platform_utils.hpp's actual fragment table.
# This is also the layout in OVRPort's unmodified Khronos loader.
from frameport.analysis import elf
cc = shutil.which("cc")
src = tmp_path / "composed.c"
strings = list(sx.PARTITIONS) + [sx.SUBDIR, "/active_runtime.json", "/active_runtime.aarch64.json"]
strings += list(sx.BROKERS) + ["RuntimeManifestFile", "xrNegotiateLoaderRuntimeInterface"]
src.write_text("const char *strings[] = {" + ",".join(json.dumps(s) for s in strings) + "};")
out = tmp_path / "composed.so"
subprocess.run([cc, "-shared", "-fPIC", str(src), "-o", str(out)], check=True)
buf = bytearray(out.read_bytes())
struct.pack_into("<H", buf, 18, 183)
data = bytes(buf)
assert sx.composed(data) and sx.detected("libgame.so", data)
fixed, count = sx.rewrite(data)
assert count == 8 and len(fixed) == len(data)
assert sx.redirected(fixed) and sx.rewrite(fixed) == (fixed, 0)
expected = data
for old, new in [(p, "/sd") for p in sx.PARTITIONS] + [(sx.SUBDIR, "card/fp/")] + [
(p, p.replace("org.khronos", "org.fp_null")) for p in sx.BROKERS]:
expected, _ = elf.replace_rodata_string(expected, old, new)
assert fixed == expected
assert "/sd" + "card/fp/" + "1" + "/active_runtime.json" == sx.COMPOSED_MANIFEST
def test_analysis_records_embedded_loader(tmp_path, embedded, quest_manifest):
apk = tmp_path / "analyze.apk"
with zipfile.ZipFile(apk, "w") as z:
z.writestr("AndroidManifest.xml", quest_manifest)
z.writestr("lib/arm64-v8a/libengine.so", embedded)
assert detect.analyze(apk).extra["static_openxr_loader"] == ["libengine.so"]
def test_triage_only_when_chain_absent():
from frameport.validate.triage import triage
line = "RuntimeManifestFile::FindManifestFiles - using global runtime file /vendor/etc/openxr/1/active_runtime.json"
assert "static-openxr-loader-bypass" in {f.id for f in triage(line, "RUNNING").findings}
for chain in ("FrameBridge: settings read", "fp_xrruntime: negotiated"):
assert "static-openxr-loader-bypass" not in {f.id for f in triage(line + "\n" + chain, "RUNNING").findings}
def test_heuristic_selects_chain_without_game_specific_rules():
from frameport.recommend.engine import suggest
a = _analysis(package="com.example.othergame", engine="Other", libs=["libengine.so"],
extra={"static_openxr_loader": ["libengine.so"], "vr_kind": "openxr"})
r = suggest(a, use_catalog=False)
assert {"frame.static_openxr_loader", "frame.adapter"} <= r.patches.keys()
assert not r.as_is
-40
View File
@@ -1,40 +0,0 @@
"""Host harness for the GLES swapchain pool, with runtime and GL doubles."""
import os
import shutil
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
INC = ROOT / "native/.cache/openxr-f2448a8797"
if not INC.is_dir():
INC = None
CC = shutil.which("cc") or shutil.which("clang")
@pytest.mark.native
@pytest.mark.skipif(not os.environ.get("FRAMEPORT_NATIVE_TESTS") or not INC or not CC,
reason="set FRAMEPORT_NATIVE_TESTS=1; needs cached OpenXR and host GLES/EGL headers")
def test_pool_runtime_and_gl(tmp_path):
# Copy only graphics headers: adding the NDK sysroot itself shadows host libc headers.
inc = tmp_path / "include"
candidates = [Path("/usr/include"), *(ROOT / "native/.cache").glob(
"ndk-*/toolchains/llvm/prebuilt/*/sysroot/usr/include")]
graphics = next((p for p in candidates if all((p / d).is_dir() for d in ("GLES3", "EGL", "KHR"))), None)
if graphics is None:
pytest.skip("needs GLES3/EGL/KHR headers (host development packages or cached NDK)")
for directory in ("GLES3", "EGL", "KHR"):
shutil.copytree(graphics / directory, inc / directory)
# Exercise production rect_clamp too, rather than a second implementation in the harness.
adapter = (ROOT / "native/adapter/frame_adapter.c").read_text()
start = adapter.index("static int clamp_rect(")
end = adapter.index("\n}", start) + 2
(inc / "pool_clamp.h").write_text(adapter[start:end])
exe = tmp_path / "pool"
subprocess.run([CC, "-std=c11", "-pthread", "-Wall", "-Wextra", "-Werror", "-Wno-missing-field-initializers",
"-I", str(INC), "-I", str(inc), "-I", str(ROOT / "native/adapter"),
str(ROOT / "tests/native/swapchain_pool_harness.c"), "-o", str(exe)], check=True)
out = subprocess.run([str(exe)], capture_output=True, text=True)
assert out.returncode == 0, out.stderr
assert out.stdout.strip() == "ok"
-11
View File
@@ -647,14 +647,3 @@ def test_missing_quest_foveation_image_is_diagnostic():
assert "adapter.hide_fb_foveation" not in result.suggestions()
result = triage(message + "\nFrameBridge: hide_fb_foveation: filtering dispatcher extensions", "EXITED")
assert "quest-foveation-image-missing" not in {f.id for f in result.findings}
def test_swapchain_count_limit_signature():
line = ("10-18 12:00:00 I FrameBridge: xrCreateSwapchain 256x128 format=35907 samples=1 array=1 "
"faces=1 usage=0x1 flags=0x0 result=-10 handle=0x0")
findings = triage(line, "RUNNING").findings
finding = next(f for f in findings if f.id == "swapchain-limit-reached")
assert finding.suggest == [] and "count limit" in finding.diagnosis
for other in (line.replace("result=-10", "result=0"), line.replace("result=-10", "result=-100"),
line.replace("FrameBridge:", "Other:"), "FrameBridge: xrEndFrame result=-10"):
assert "swapchain-limit-reached" not in {f.id for f in triage(other, "RUNNING").findings}