Skip to content

Host-driven navigation across the three SDKs - #3

Merged
Xget7 merged 10 commits into
mainfrom
navigation-api-and-rn-example
Sep 18, 2026
Merged

Xget7 merged 10 commits into
mainfrom
navigation-api-and-rn-example

Conversation

@Xget7

@Xget7 Xget7 commented Sep 18, 2026

Copy link
Copy Markdown
Owner

The joystick is gone from all three SDKs. A view looks only; the host supplies walk velocity, character settings and a collider, and draws whatever control it wants. The sample joysticks live in the example apps.

What changed

  • Navigation API on iOS, Android and React Native: setWalkVelocity, setLook, a collider prop with load events, character settings and a motion toggle.
  • conservativeCapabilities so a first world request carries limits no adapter rejects. Capabilities only arrive with the engine, so a request rejected before the engine exists left a host stuck for good; a test holds the values inside the ranges the Android adapter validates.
  • Android timings reach the host: the adapter sent every timing as unavailable while the engine measured and logged them, so no Android HUD could show a frame rate.
  • iOS Debug crash fixed: SplatKitViewComponentView never seeded _props, which trips an assert RCTViewComponentView only makes in Debug builds.
  • React Native example with a stats card and a quality picker, plus a Metro pin that keeps one copy of React and React Native without breaking subpath imports.

Releases cut from this branch

  • io.github.xget7:splatkit-android:0.1.0-alpha08
  • splatkit-ios v0.1.0-alpha.4
  • @splatkit/react-native@0.1.0-alpha.2 (npm tag next)

Verification

Physical devices, not emulators. iPhone (A19 Pro): 47-67 fps on a 6M-splat capture, spawn resolves to the collider centre, 0 quality-limited refinements at 0.50 px LOD error. Xiaomi MI 9 (Adreno 640, Android 11): same world and same spawn position, 454,316 collider triangles, walk mode active, GPU-bound at 7 fps on the Max preset.

splat_core 169 pass / 3 skipped, engine 206 pass, React Native package 35 pass, codegen green on both platforms, C++ lint clean.

🤖 Generated with Claude Code

Xget7 and others added 10 commits September 17, 2026 20:17
A host that hands the SDK a camera position has no way to know whether the
collider has a floor under it, and a walker placed where it has none refuses
every step: the collider bounds the world, so the walk is over before it
starts. findStandingSpot searches outward from the middle of the collider for
a surface with room on every side and returns the eye position to stand at.

ColliderBuilder and GlbEncoder produce a collider from a world, and
splat_collider writes one out, so a scene no longer needs a hand-authored mesh
to be walkable.

The LOD validator capped hierarchies at 32 levels. The grid builder floors its
finest level at a 2^20 fraction of the extent, which at the default base of 1.5
spans about 35, so a real capture built a tree its own validator rejected and
the world failed to upload. The cap now bounds a corrupt file only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The frame rate counted frames the renderer submitted, so a scene the display
never caught up with still read as smooth. StatsPublisher now takes display
times when the backend has them and reports presented fps, a p95 interval, a
1% low and dropped frames; without them it falls back to submitted frames and
says so.

WalkCamera resolves a spawn the collider cannot support, so a world captured
about an arbitrary origin starts walkable, and it carries the host's character
settings into the controller it builds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The iOS view owned a two-finger walk gesture, so an app that wanted its own
control fought the SDK for touches and got a walk nobody could aim. The view
now handles looking only, through a gesture recognizer that coexists with a
host's own, and walking arrives as velocity from whatever control the app
draws. Character settings and collider loading are part of the same surface.

Metal reports each drawable's presented time, so the stats a HUD reads count
frames the display showed rather than frames submitted, and the LOD selector
applies lodErrorPixels and lodSplatLimit live, thinning detail evenly when a
cut will not fit rather than stopping wherever traversal ran out of room.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The package drew no navigation of its own and offered none: an app could mount
a view and look at a world, but walking, the collider, the character and the
render policy had no prop or command behind them. They do now, with world,
collider, policy and camera events reported back, and SplatKitBuilder resolves
a quality preset against the limits the engine reports in onCapabilities.

Phases and presets are const objects with derived types rather than bare string
unions, so the adapters, the codegen spec and the tests all compare against the
same names.

Two defects the example turned up: SKRenderPolicy was left indeterminate, and
the lodSplatLimit the props do not carry starved the selector to whatever was
on the stack, so a world rendered or vanished by chance. And the component view
never seeded _props with its own props type, which a debug build asserts on, so
the app aborted at the first commit while release builds ran.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Navigation belongs to the app, so the example apps own it: the React Native
example draws a thumb stick and pushes velocity straight to the view, and both
it and the iOS dev app show a stats card and a quality picker built from the
events the SDK reports.

The React Native example resolves react and react-native to its own copy by
redirecting where resolution starts, rather than rewriting requests to absolute
paths, which skipped the package exports map every subpath import needs. Two
copies of react-native means two view config registries, and a component
registered in one is invisible to the renderer reading the other.

Its world request carries the budgets it was built against, so the world
reloads once when the engine reports limits that move them, and never
otherwise. This example installs the package from the monorepo; point it back
at the published version before release.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Credits the PlayCanvas splat-transform collision passes the collider builder
ports.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Capabilities reach the host with the engine a world load creates, so the first
request is built against a guess. A guess above what the adapter accepts is
rejected before any engine exists, which means no capabilities event and no way
for the host to learn what it got wrong: the example asked for a 4M LOD budget,
Android accepts 2.2M, and the app sat on the error for good.

conservativeCapabilities is the narrowest limits any shipped backend reports,
and a test holds them inside the ranges the Android adapter validates against.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two fingers walked the scene, which left an app no way to place its own control
and no way to aim the walk it produced. The view now looks only, one pointer
leading the whole drag, and takes walk velocity, character settings and a
collider from the host. JoystickView stays as a sample the example app draws,
not as the SDK's answer to navigation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The adapter sent every timing as unavailable, so a host HUD on Android could
never show a frame rate or a GPU time while the engine was measuring both and
logging them. Zero is the SDK's own "not measured" for these fields: the GPU
and sort times are zero without timestamp queries, and the frame time is zero
while nothing redraws.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Android goes to 0.1.0-alpha08 with walking handed to the host, iOS to
0.1.0-alpha.4 and React Native to 0.1.0-alpha.2. The SwiftPM binary target and
the npm package's prepack fetch both pin the new XCFramework by checksum.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Xget7
Xget7 merged commit b530258 into main Sep 18, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant