A handoff-ready prototyping spec, tracked in this repo, that two devs can execute cold — covering Session 0 (positioning bake-off), Session 1 (pose → splat rendering), and Session 2 (deep-dive on whichever positioning approach wins). The map is done when every decision those specs depend on is settled; running the sessions is not part of it.
Skills every session should consult — /grilling and /domain-modeling for decision tickets, /research for research tickets, /prototype for prototype tickets.
Standing decisions for this effort:
Scope is tech-stack validation in one office room. Deliberately small. It cannot prove ship-scale fidelity, and every gate written must say so in as many words rather than reporting a number that looks meaningful and isn't.
Positioning approach is open. Visual relocalization against the splat vs BLE beacon trilateration, decided by the Session 0 bake-off. The project goal of "as little added sensor as possible" pulls toward visual; BLE's edge is working with the phone in a pocket.
Presentation is a rendered splat view — the photorealistic model drawn from the user's pose, with the route overlaid. Not camera passthrough.
Hardware on hand: iPhone Pro (LiDAR) for capture, 3x MikroTik TG-BT5-IN beacons.
Two devs, parallel tracks. Neither is a dedicated measurer, so ground-truth discipline has to live in a written protocol instead of a person.
Everything is tracked here — issues in Gitea, specs as markdown in the repo.
Decisions so far
Research: MikroTik TG-BT5-IN beacon capabilities and configuration — Usable as-is, no RouterOS needed: config is phone-only via the free MikroTik Beacon Manager app over BLE. Min advertising interval 20ms (default 5s — must be changed), TX −28…+6 dBm, emits iBeacon directly. Practical gotcha: tags ship asleep and need a magnet held 3–10s to wake. Non-replaceable 220mAh cell, est. 2–4 months at 100ms. 3 tags is the degenerate minimum (fat triangle, never collinear) — buy 3 more at ~$10. Expect 1.5–3m RSSI accuracy: quadrant-level, not an AR pose.
Research: on-device visual relocalization against a gaussian splat — Does not exist on mobile in 2026. All 3DGS localization is desktop-GPU pose refinement needing a prior pose. Sparse-feature VPS against your own scan does ship, and is less closed than first concluded: MultiSet publishes ~6cm median, ~3–4s per on-device fix, and a bridgeable native Swift xcframework returning pose + confidence at $0/$49/$249 tiers — though on-device offline is Enterprise-gated and map bundles run 100s of MB per ~50k sq ft. Immersal's offline is the least gated but it publishes no accuracy figure at all; Niantic documents no offline mode (disqualifying at sea); Vuforia caps phone-scanned targets at ~50m² each. The real risk is recall, not accuracy — every vendor beats 1m on paper and none publishes a success rate or chest-height-while-walking behaviour. VPS would give absolute heading — the one thing BLE structurally cannot — but retrieval matching is worst-case in mirrored corridors and no vendor publishes a false-fix rate. Recommendation: ARKit VIO + a manual origin fix (free from RN, ~0.5% drift, 0.79m over 145m), with beacons as periodic re-fixes rather than the positioning system. Reframes the bake-off — see #10.
Research: WebGL2 splat viewer performance inside iOS WKWebView — Spark (@sparkjsdev/spark 2.1.0, MIT, WebGL2-only) + SOG/SPZ assets; .RAD LoD streaming beyond room scale. Budget 1.5M gaussians and ≤300–400MB in the WebContent process — the load-time transient gets jetsam-killed, not steady state. WebGPU is gated out of WKWebView below iOS 26 (first-party), confirming the WebGL2 assumption; rAF is capped at 60Hz. Both gates (≥30fps, <100ms) remain unvalidated — no WKWebView splat frame-rate measurement exists anywhere published. Keep the interpolation loop inside the WebView; measure the pose-bridge latency first (#16). Addendum: prefer SOG over SPZ as the runtime format (WebP-backed, decodes off-thread, GPU-direct, 15–20×; PlayCanvas rejects SPZ for web — needs ZSTD WASM, v4-only), with Streamed SOG for LoD. @mkkellogg/GaussianSplats3D is abandoned and points at Spark, a third independent confirmation. Bridge cost on iOS is ~0.2–1.5ms one-way (evaluateJavaScript 0.39ms round trip on iOS 9) — per-frame injection at 60Hz is fine; the ~10ms floor is the AndroidpostMessage path, not iOS. Budget is instead dominated by ARKit pose age (10–25ms), two forced 60Hz quantizations (rAF is hard-capped in WKWebView, and out-of-process CA commit costs a frame) and scanout — ≈45–100ms best case. No late-warp hook exists, so in-WebView forward prediction (~25ms ahead) is the only latency compensation. Send pose native→web only. No shipping splat format publishes PSNR — measure on our own captures.
Research: native / RN gaussian splat rendering without a WebView — WebView route confirmed for Session 1. Independently picks the same viewer as #3: react-native-webview + Spark, runs in Expo Go with zero native code, ~2–4 days vs 1.5–2.5 weeks for an iOS-only bridge plus 3–6 for Android. Native splat rendering is real but iOS-only — MetalSplatter (MIT, active) takes caller-supplied view/projection matrices, a near-perfect fit for externally-driven pose, but has no RN bridge and no Android counterpart. Correction (addendum 2): Babylon React Native 2.0.5 is a turnkey cross-platform native splat path — Babylon.js reads .ply/compressed/.splat/.spz/.sog and Babylon Native ships a C++ sortSplats (2026-01-14) so no Web Worker is needed. It loses on Expo (disclaimed), RN lag (0.81.6 vs 0.87), a legacy view under new-arch interop, and Hermes gaps (SPZ v4 rejected, SOG crashes) — measured and rejected, not unconsidered. Genuine dead ends: react-native-filament (no VertexBuffer API from JS), expo-gl (sort would run in JS on the JS thread), react-native-gsplat and react-native-gpu (do not exist), expo-three (abandoned 2024). Spark cannot run outside a WebView (Vite-only worker import, mandatory Rust WASM, no Hermes WebAssembly) — which is itself part of why the WebView is the right host. Long-term native path to keep warm is react-native-webgpu + hand-written WGSL, not MetalSplatter. Addendum: Apple ships first-party GaussianSplatComponent in RealityKit on iOS 27 (beta) — takes raw buffers, no file format, so pair it with MetalSplatter's SplatIO parser; ~5–8 days for an RN wrapper. Khronos has a KHR_gaussian_splatting glTF extension in RC. Android still has nothing (~37-star ceiling; Filament closed its 3DGS request unimplemented).
Research: iPhone Pro LiDAR capture to a metric, gravity-aligned splat — Scaniverse Classic (free, on-device, PLY+SPZ) is the only consumer app claiming metric scale; Polycam and KIRI's splat modes don't use LiDAR at all and are arbitrary-scale by construction, and Luma is abandoned. ARKit supplies gravity alignment and metres for free, so the risk is a pipeline stage discarding it — nerfstudio's auto_scale_poses=True normalises to ±1 and ns-export gaussian-splat never inverts it; Brush was verified by source not to touch poses. Keep a .ply master and transcode with PlayCanvas splat-transform. Metric capture reduces Umeyama from 7 DOF to 6 — but fit scale free on the first capture and read s as the accuracy measurement. Caveat: the metric-scale claim rests on a vendor pricing page with no technical doc behind it.
Not yet specified
How Session 2 gets specified at all, when its content depends on which approach wins Session 0 — a branch in the spec, a stub the devs fill in, or a second charting pass after Session 0 runs.
Absolute heading acquisition. Sharpened by #2: a steel hull makes the compass unreliable, and there is no mobile splat relocalizer to supply absolute pose. ARKit VIO gives good relative heading once an origin is known, so the problem collapses into origin acquisition and re-anchoring — now ticketed as #15. Sharpened again: all three absolute-heading options are disqualified — ARGeoTrackingConfiguration is outdoors-only by Apple's own docs, ARCore Geospatial depends on Street-View-built VPS, and .gravityAndHeading is compass-based with 60°+ indoor ferrous error and no runtime quality signal. That points hard at fiducial markers plus worldAlignment: .gravity, with a re-fix every ~20–30 m to hold 1 m against 0.5–8% VIO drift. The mirrored-ship failure mode now has a shape rather than being unsolved: every vendor needs our own coarse prior to narrow the query — deck from a barometer, zone from BLE or Wi-Fi — and Immersal, MultiSet and Vuforia all expose prior-pose or location-prior APIs (Vuforia is the only one that names perceptual aliasing outright). OCR of painted signage is the strongest cheap symmetry-breaker found — worth +17.9 points of Recall@1 on a repetitive building, needing no added hardware, and a ship paints deck and cabin numbers everywhere by regulation. The scale of the problem is now measured: NetVLAD Recall@1 falls to 58.2% on a five-floor repetitive building with failures specifically between identical floors; a symmetric room drops single-pose regression to 5%; ORB-SLAM2 is 3× worse with loop closures on (0.67 m → 2.17 m) in repetitive space. What stays foggy is the design of that prior, keeping multiple hypotheses rather than one estimate, and being willing to show "locating…" instead of a confident wrong pose. Also settled: ignore all geo-referencing — WGS84, ARGeoAnchor, VPS coarse geoposition are all wrong the moment the ship leaves the berth. Map-relative pose only, which as a bonus yields absolute yaw.
Multi-deck / vertical ambiguity. BLE bleeds between decks and splats are captured per-space; deck identity may need to be an explicit part of position rather than a z coordinate. A barometer looks attractive (already on the phone, no ship-side hardware) — but correction: barometric deck classification has never been validated aboard a moving vessel. The 96–100% figures are all land buildings, and ship heave plus weather-driven pressure swings dwarf a ~3 m deck spacing. Treat it as unproven, not as the answer. Out of the office room's reach, but it shapes any later deployment.
Out of scope
Ship-scale capture and deployment economics — capturing a 300m vessel, streaming splats at that size, beacon density and cost. A separate effort with its own destination.
Route computation / pathfinding through a ship graph.
Camera-passthrough AR — ruled out for now in favour of the rendered splat view.
A native (non-WebView) renderer — not until WebView is measured and found wanting (#3, #6). Revisit triggers: Babylon React Native gaining Expo support and a Fabric view; or iOS 27 going GA (~Sept 2026), which drops the iOS native path to days via RealityKit — but Android would still need a renderer written from scratch (20–40 engineer-days), so react-native-webgpu + WGSL remains the only write-once answer.
Executing the prototyping sessions, and the in-session work itself (app scaffolding, taping and measuring reference points).
## Destination
A **handoff-ready prototyping spec**, tracked in this repo, that two devs can execute cold — covering **Session 0** (positioning bake-off), **Session 1** (pose → splat rendering), and **Session 2** (deep-dive on whichever positioning approach wins). The map is done when every decision those specs depend on is settled; running the sessions is not part of it.
## Notes
**Domain** — indoor positioning + gaussian-splat AR navigation for cruise ships, on React Native. Background: [project-overview.md](project-overview.md), [prototyping-session.md](prototyping-session.md).
**Skills every session should consult** — `/grilling` and `/domain-modeling` for decision tickets, `/research` for research tickets, `/prototype` for prototype tickets.
**Standing decisions for this effort:**
- **Scope is tech-stack validation in one office room.** Deliberately small. It cannot prove ship-scale fidelity, and every gate written must say so in as many words rather than reporting a number that looks meaningful and isn't.
- **Positioning approach is open.** Visual relocalization against the splat vs BLE beacon trilateration, decided by the Session 0 bake-off. The project goal of "as little added sensor as possible" pulls toward visual; BLE's edge is working with the phone in a pocket.
- **Presentation is a rendered splat view** — the photorealistic model drawn from the user's pose, with the route overlaid. Not camera passthrough.
- **Hardware on hand:** iPhone Pro (LiDAR) for capture, 3x MikroTik TG-BT5-IN beacons.
- **Two devs, parallel tracks.** Neither is a dedicated measurer, so ground-truth discipline has to live in a written protocol instead of a person.
- **Everything is tracked here** — issues in Gitea, specs as markdown in the repo.
## Decisions so far
<!-- one line per closed ticket: gist + link -->
- [Research: MikroTik TG-BT5-IN beacon capabilities and configuration](https://git.larsnolden.com/lars/splat-indoor-navigation/issues/4) — Usable as-is, no RouterOS needed: config is phone-only via the free MikroTik Beacon Manager app over BLE. Min advertising interval **20ms** (default 5s — must be changed), TX −28…+6 dBm, emits iBeacon directly. Practical gotcha: tags ship asleep and need a **magnet** held 3–10s to wake. Non-replaceable 220mAh cell, est. 2–4 months at 100ms. 3 tags is the degenerate minimum (fat triangle, never collinear) — buy 3 more at ~$10. Expect **1.5–3m** RSSI accuracy: quadrant-level, not an AR pose.
- [Research: on-device visual relocalization against a gaussian splat](https://git.larsnolden.com/lars/splat-indoor-navigation/issues/2) — **Does not exist on mobile in 2026.** All 3DGS localization is desktop-GPU pose *refinement* needing a prior pose. Sparse-feature VPS against your own scan does ship, and is **less closed than first concluded**: **MultiSet** publishes ~6cm median, ~3–4s per on-device fix, and a bridgeable native Swift `xcframework` returning pose **+ confidence** at $0/$49/$249 tiers — though on-device offline is Enterprise-gated and map bundles run 100s of MB per ~50k sq ft. Immersal's offline is the least gated but it publishes **no accuracy figure at all**; Niantic documents **no offline mode** (disqualifying at sea); Vuforia caps phone-scanned targets at **~50m²** each. **The real risk is recall, not accuracy** — every vendor beats 1m on paper and none publishes a success rate or chest-height-while-walking behaviour. VPS would give absolute heading — the one thing BLE structurally cannot — but retrieval matching is worst-case in mirrored corridors and no vendor publishes a false-fix rate. Recommendation: **ARKit VIO + a manual origin fix** (free from RN, ~0.5% drift, 0.79m over 145m), with beacons as periodic re-fixes rather than the positioning system. Reframes the bake-off — see #10.
- [Research: WebGL2 splat viewer performance inside iOS WKWebView](https://git.larsnolden.com/lars/splat-indoor-navigation/issues/3) — **Spark (`@sparkjsdev/spark` 2.1.0, MIT, WebGL2-only)** + SOG/SPZ assets; `.RAD` LoD streaming beyond room scale. Budget **1.5M gaussians** and **≤300–400MB** in the WebContent process — the load-time transient gets jetsam-killed, not steady state. WebGPU is gated out of WKWebView below iOS 26 (first-party), confirming the WebGL2 assumption; rAF is capped at 60Hz. **Both gates (≥30fps, <100ms) remain unvalidated — no WKWebView splat frame-rate measurement exists anywhere published.** Keep the interpolation loop inside the WebView; measure the pose-bridge latency first (#16). **Addendum:** prefer **SOG** over SPZ as the runtime format (WebP-backed, decodes off-thread, GPU-direct, 15–20×; PlayCanvas rejects SPZ for web — needs ZSTD WASM, v4-only), with **Streamed SOG** for LoD. `@mkkellogg/GaussianSplats3D` is **abandoned and points at Spark**, a third independent confirmation. **Bridge cost on iOS is ~0.2–1.5ms one-way** (`evaluateJavaScript` 0.39ms round trip on iOS 9) — per-frame injection at 60Hz is fine; the ~10ms floor is the **Android** `postMessage` path, not iOS. Budget is instead dominated by **ARKit pose age (10–25ms)**, two forced **60Hz** quantizations (rAF is hard-capped in WKWebView, and out-of-process CA commit costs a frame) and scanout — **≈45–100ms best case**. **No late-warp hook exists**, so in-WebView forward prediction (~25ms ahead) is the only latency compensation. Send pose native→web only. No shipping splat format publishes PSNR — measure on our own captures.
- [Research: native / RN gaussian splat rendering without a WebView](https://git.larsnolden.com/lars/splat-indoor-navigation/issues/6) — **WebView route confirmed for Session 1.** Independently picks the same viewer as #3: `react-native-webview` + Spark, runs in Expo Go with zero native code, ~2–4 days vs 1.5–2.5 weeks for an iOS-only bridge plus 3–6 for Android. Native splat rendering is real but iOS-only — MetalSplatter (MIT, active) takes caller-supplied view/projection matrices, a near-perfect fit for externally-driven pose, but has no RN bridge and no Android counterpart. **Correction (addendum 2):** Babylon React Native 2.0.5 *is* a turnkey **cross-platform** native splat path — Babylon.js reads `.ply`/compressed/`.splat`/`.spz`/`.sog` and Babylon Native ships a C++ `sortSplats` (2026-01-14) so no Web Worker is needed. It loses on Expo (disclaimed), RN lag (0.81.6 vs 0.87), a legacy view under new-arch interop, and Hermes gaps (SPZ v4 rejected, SOG crashes) — **measured and rejected, not unconsidered**. Genuine dead ends: `react-native-filament` (no VertexBuffer API from JS), `expo-gl` (sort would run in JS on the JS thread), `react-native-gsplat` and `react-native-gpu` (do not exist), `expo-three` (abandoned 2024). **Spark cannot run outside a WebView** (Vite-only worker import, mandatory Rust WASM, no Hermes WebAssembly) — which is itself part of why the WebView is the right host. Long-term native path to keep warm is **react-native-webgpu + hand-written WGSL**, not MetalSplatter. **Addendum:** Apple ships first-party `GaussianSplatComponent` in RealityKit on **iOS 27 (beta)** — takes raw buffers, no file format, so pair it with MetalSplatter's `SplatIO` parser; ~5–8 days for an RN wrapper. Khronos has a `KHR_gaussian_splatting` glTF extension in RC. Android still has nothing (~37-star ceiling; Filament closed its 3DGS request unimplemented).
- [Research: iPhone Pro LiDAR capture to a metric, gravity-aligned splat](https://git.larsnolden.com/lars/splat-indoor-navigation/issues/5) — **Scaniverse Classic** (free, on-device, PLY+SPZ) is the only consumer app claiming **metric scale**; Polycam and KIRI's splat modes don't use LiDAR at all and are arbitrary-scale by construction, and Luma is abandoned. ARKit supplies gravity alignment and metres for free, so the risk is a **pipeline stage discarding it** — nerfstudio's `auto_scale_poses=True` normalises to ±1 and `ns-export gaussian-splat` never inverts it; Brush was verified by source not to touch poses. Keep a `.ply` master and transcode with PlayCanvas `splat-transform`. Metric capture reduces Umeyama from 7 DOF to 6 — but **fit scale free on the first capture and read `s` as the accuracy measurement**. Caveat: the metric-scale claim rests on a vendor pricing page with no technical doc behind it.
## Not yet specified
- **How Session 2 gets specified at all**, when its content depends on which approach wins Session 0 — a branch in the spec, a stub the devs fill in, or a second charting pass after Session 0 runs.
- **Absolute heading acquisition.** Sharpened by #2: a steel hull makes the compass unreliable, and there is no mobile splat relocalizer to supply absolute pose. ARKit VIO gives good *relative* heading once an origin is known, so the problem collapses into origin acquisition and re-anchoring — now ticketed as #15. Sharpened again: **all three absolute-heading options are disqualified** — `ARGeoTrackingConfiguration` is outdoors-only by Apple's own docs, ARCore Geospatial depends on Street-View-built VPS, and `.gravityAndHeading` is compass-based with 60°+ indoor ferrous error and no runtime quality signal. That points hard at **fiducial markers** plus `worldAlignment: .gravity`, with a re-fix every **~20–30 m** to hold 1 m against 0.5–8% VIO drift. The **mirrored-ship failure mode** now has a shape rather than being unsolved: every vendor needs **our own coarse prior** to narrow the query — deck from a barometer, zone from BLE or Wi-Fi — and Immersal, MultiSet and Vuforia all expose prior-pose or location-prior APIs (Vuforia is the only one that names perceptual aliasing outright). **OCR of painted signage** is the strongest cheap symmetry-breaker found — worth **+17.9 points** of Recall@1 on a repetitive building, needing no added hardware, and a ship paints deck and cabin numbers everywhere by regulation. The scale of the problem is now measured: NetVLAD Recall@1 falls to **58.2%** on a five-floor repetitive building with failures specifically *between identical floors*; a symmetric room drops single-pose regression to **5%**; ORB-SLAM2 is **3× worse with loop closures on** (0.67 m → 2.17 m) in repetitive space. What stays foggy is the design of that prior, keeping **multiple hypotheses** rather than one estimate, and being willing to show "locating…" instead of a confident wrong pose. Also settled: **ignore all geo-referencing** — WGS84, `ARGeoAnchor`, VPS coarse geoposition are all wrong the moment the ship leaves the berth. Map-relative pose only, which as a bonus yields absolute yaw.
- **Multi-deck / vertical ambiguity.** BLE bleeds between decks and splats are captured per-space; deck identity may need to be an explicit part of position rather than a z coordinate. A **barometer** looks attractive (already on the phone, no ship-side hardware) — but **correction: barometric deck classification has never been validated aboard a moving vessel.** The 96–100% figures are all land buildings, and ship heave plus weather-driven pressure swings dwarf a ~3 m deck spacing. Treat it as unproven, not as the answer. Out of the office room's reach, but it shapes any later deployment.
## Out of scope
- **Ship-scale capture and deployment economics** — capturing a 300m vessel, streaming splats at that size, beacon density and cost. A separate effort with its own destination.
- **Route computation / pathfinding** through a ship graph.
- **Camera-passthrough AR** — ruled out for now in favour of the rendered splat view.
- **A native (non-WebView) renderer** — not until WebView is measured and found wanting (#3, #6). **Revisit triggers:** Babylon React Native gaining Expo support and a Fabric view; or iOS 27 going GA (~Sept 2026), which drops the iOS native path to days via RealityKit — but Android would still need a renderer written from scratch (20–40 engineer-days), so react-native-webgpu + WGSL remains the only write-once answer.
- **Executing the prototyping sessions**, and the in-session work itself (app scaffolding, taping and measuring reference points).
lars
added this to the Wayfinder: RN prototyping spec milestone 2026-08-11 15:41:37 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Destination
A handoff-ready prototyping spec, tracked in this repo, that two devs can execute cold — covering Session 0 (positioning bake-off), Session 1 (pose → splat rendering), and Session 2 (deep-dive on whichever positioning approach wins). The map is done when every decision those specs depend on is settled; running the sessions is not part of it.
Notes
Domain — indoor positioning + gaussian-splat AR navigation for cruise ships, on React Native. Background: project-overview.md, prototyping-session.md.
Skills every session should consult —
/grillingand/domain-modelingfor decision tickets,/researchfor research tickets,/prototypefor prototype tickets.Standing decisions for this effort:
Decisions so far
xcframeworkreturning pose + confidence at $0/$49/$249 tiers — though on-device offline is Enterprise-gated and map bundles run 100s of MB per ~50k sq ft. Immersal's offline is the least gated but it publishes no accuracy figure at all; Niantic documents no offline mode (disqualifying at sea); Vuforia caps phone-scanned targets at ~50m² each. The real risk is recall, not accuracy — every vendor beats 1m on paper and none publishes a success rate or chest-height-while-walking behaviour. VPS would give absolute heading — the one thing BLE structurally cannot — but retrieval matching is worst-case in mirrored corridors and no vendor publishes a false-fix rate. Recommendation: ARKit VIO + a manual origin fix (free from RN, ~0.5% drift, 0.79m over 145m), with beacons as periodic re-fixes rather than the positioning system. Reframes the bake-off — see #10.@sparkjsdev/spark2.1.0, MIT, WebGL2-only) + SOG/SPZ assets;.RADLoD streaming beyond room scale. Budget 1.5M gaussians and ≤300–400MB in the WebContent process — the load-time transient gets jetsam-killed, not steady state. WebGPU is gated out of WKWebView below iOS 26 (first-party), confirming the WebGL2 assumption; rAF is capped at 60Hz. Both gates (≥30fps, <100ms) remain unvalidated — no WKWebView splat frame-rate measurement exists anywhere published. Keep the interpolation loop inside the WebView; measure the pose-bridge latency first (#16). Addendum: prefer SOG over SPZ as the runtime format (WebP-backed, decodes off-thread, GPU-direct, 15–20×; PlayCanvas rejects SPZ for web — needs ZSTD WASM, v4-only), with Streamed SOG for LoD.@mkkellogg/GaussianSplats3Dis abandoned and points at Spark, a third independent confirmation. Bridge cost on iOS is ~0.2–1.5ms one-way (evaluateJavaScript0.39ms round trip on iOS 9) — per-frame injection at 60Hz is fine; the ~10ms floor is the AndroidpostMessagepath, not iOS. Budget is instead dominated by ARKit pose age (10–25ms), two forced 60Hz quantizations (rAF is hard-capped in WKWebView, and out-of-process CA commit costs a frame) and scanout — ≈45–100ms best case. No late-warp hook exists, so in-WebView forward prediction (~25ms ahead) is the only latency compensation. Send pose native→web only. No shipping splat format publishes PSNR — measure on our own captures.react-native-webview+ Spark, runs in Expo Go with zero native code, ~2–4 days vs 1.5–2.5 weeks for an iOS-only bridge plus 3–6 for Android. Native splat rendering is real but iOS-only — MetalSplatter (MIT, active) takes caller-supplied view/projection matrices, a near-perfect fit for externally-driven pose, but has no RN bridge and no Android counterpart. Correction (addendum 2): Babylon React Native 2.0.5 is a turnkey cross-platform native splat path — Babylon.js reads.ply/compressed/.splat/.spz/.sogand Babylon Native ships a C++sortSplats(2026-01-14) so no Web Worker is needed. It loses on Expo (disclaimed), RN lag (0.81.6 vs 0.87), a legacy view under new-arch interop, and Hermes gaps (SPZ v4 rejected, SOG crashes) — measured and rejected, not unconsidered. Genuine dead ends:react-native-filament(no VertexBuffer API from JS),expo-gl(sort would run in JS on the JS thread),react-native-gsplatandreact-native-gpu(do not exist),expo-three(abandoned 2024). Spark cannot run outside a WebView (Vite-only worker import, mandatory Rust WASM, no Hermes WebAssembly) — which is itself part of why the WebView is the right host. Long-term native path to keep warm is react-native-webgpu + hand-written WGSL, not MetalSplatter. Addendum: Apple ships first-partyGaussianSplatComponentin RealityKit on iOS 27 (beta) — takes raw buffers, no file format, so pair it with MetalSplatter'sSplatIOparser; ~5–8 days for an RN wrapper. Khronos has aKHR_gaussian_splattingglTF extension in RC. Android still has nothing (~37-star ceiling; Filament closed its 3DGS request unimplemented).auto_scale_poses=Truenormalises to ±1 andns-export gaussian-splatnever inverts it; Brush was verified by source not to touch poses. Keep a.plymaster and transcode with PlayCanvassplat-transform. Metric capture reduces Umeyama from 7 DOF to 6 — but fit scale free on the first capture and readsas the accuracy measurement. Caveat: the metric-scale claim rests on a vendor pricing page with no technical doc behind it.Not yet specified
ARGeoTrackingConfigurationis outdoors-only by Apple's own docs, ARCore Geospatial depends on Street-View-built VPS, and.gravityAndHeadingis compass-based with 60°+ indoor ferrous error and no runtime quality signal. That points hard at fiducial markers plusworldAlignment: .gravity, with a re-fix every ~20–30 m to hold 1 m against 0.5–8% VIO drift. The mirrored-ship failure mode now has a shape rather than being unsolved: every vendor needs our own coarse prior to narrow the query — deck from a barometer, zone from BLE or Wi-Fi — and Immersal, MultiSet and Vuforia all expose prior-pose or location-prior APIs (Vuforia is the only one that names perceptual aliasing outright). OCR of painted signage is the strongest cheap symmetry-breaker found — worth +17.9 points of Recall@1 on a repetitive building, needing no added hardware, and a ship paints deck and cabin numbers everywhere by regulation. The scale of the problem is now measured: NetVLAD Recall@1 falls to 58.2% on a five-floor repetitive building with failures specifically between identical floors; a symmetric room drops single-pose regression to 5%; ORB-SLAM2 is 3× worse with loop closures on (0.67 m → 2.17 m) in repetitive space. What stays foggy is the design of that prior, keeping multiple hypotheses rather than one estimate, and being willing to show "locating…" instead of a confident wrong pose. Also settled: ignore all geo-referencing — WGS84,ARGeoAnchor, VPS coarse geoposition are all wrong the moment the ship leaves the berth. Map-relative pose only, which as a bonus yields absolute yaw.Out of scope