Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,16 @@ iOS changes are in the [iOS changelog](packages/splatkit-ios/distribution/CHANGE

## Unreleased

### Added

- `RenderPolicySupport::rasterMask` in the shared engine, listing the raster strategies a backend builds.
- `RenderPolicy::lodSplatLimit` in the shared engine: a live cap on selected hierarchy splats, 0 for the loaded capacity.
- `buildCollider` in splat-core makes a walk-mode collider from a world's splats, a port of PlayCanvas splat-transform's collision voxel passes, with `encodeGlb` and `tools/splat_collider` to write it as a `.glb`.
- Shared engine stats count frames the display showed when a renderer reports presentation times, with a 95th percentile frame time, a 1% low and dropped frames; Metal reports them, Vulkan still counts submitted frames.

### Changed

- Walk mode refuses steps onto a floor more than 0.35 m higher, looking 0.25 m ahead, so it climbs stairs and steps over door tracks but no longer climbs counters, chairs or tables whose top the hip probe passes over, and slides along them when walked into at an angle.
- The [React Native example](apps/react-native/README.md) replaces the React Native dev app: a template React Native 0.87.1 app that installs `@splatkit/react-native` from npm and runs on Android and iOS.

## [0.1.0-alpha07] - 2026-09-16
Expand Down
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ let package = Package(
targets: [
.binaryTarget(
name: "SplatKitCore",
url: "https://github.com/Xget7/splatkit-ios/releases/download/v0.1.0-alpha.3/SplatKitCore.xcframework.zip",
checksum: "2258de61db98721528a805bdfefbc6b764aada311bda6149b3abcf98d5194906"
url: "https://github.com/Xget7/splatkit-ios/releases/download/v0.1.0-alpha.4/SplatKitCore.xcframework.zip",
checksum: "7b92ec52cbcd1f42bfc6ab31d16befd4b8134ee1c7f0a71b26789a4cbcf4f5c5"
),
.target(
name: "SplatKit",
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ Experimental alpha: APIs and quality/performance tradeoffs are still evolving.

Android: API 29+, Vulkan 1.1, arm64-v8a.
The GPU path additionally checks subgroup and memory limits.
Maven Central has `io.github.xget7:splatkit-android:0.1.0-alpha07`, with the render policy, `onWorldFrameReady` and 16 KB alignment; see the [Android releases](https://github.com/Xget7/splatkit-android/releases).
Maven Central has `io.github.xget7:splatkit-android:0.1.0-alpha08`, with host-driven walking, the render policy and `onWorldFrameReady`; see the [Android releases](https://github.com/Xget7/splatkit-android/releases).
To build current source:

```sh
cd apps/android-dev
./gradlew :splatkit:assembleRelease :splatkit:testDebugUnitTest
```

iOS: add [splatkit-ios](https://github.com/Xget7/splatkit-ios) to Swift Package Manager, version `0.1.0-alpha.3`.
iOS: add [splatkit-ios](https://github.com/Xget7/splatkit-ios) to Swift Package Manager, version `0.1.0-alpha.4`.
Use the native view, forward lifecycle and load worlds asynchronously; see each SDK's README for examples.

## Implemented scope
Expand All @@ -28,7 +28,7 @@ Use the native view, forward lifecycle and load worlds asynchronously; see each
| Offline `.lodsplat` and GPU hierarchical selection | Yes | Yes |
| 16-bit quantized depth / two radix passes | Per-view policy approximation | Per-view policy approximation |
| SH degrees 0–3, walk/fly, touch, motion, loaded/drawn stats | Yes | Yes |
| Hybrid compute screen tiles | Experimental | Not implemented |
| Hybrid compute screen tiles | Experimental per-view opt-in, for dense close-up scenes | Not implemented |
| Per-view render policy and capabilities | Yes | Yes |
| React Native policy prop and events | iPhone 17 Pro validated | Mi 9 validated |

Expand Down
27 changes: 27 additions & 0 deletions THIRD_PARTY_LICENSES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,33 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


============================================================================
splat-transform (PlayCanvas), whose collision voxel passes splat-core's
collider builder ports
============================================================================

MIT License

Copyright (c) 2011-2026 PlayCanvas Ltd.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

============================================================================
zstd (Meta Platforms), taken under BSD-3-Clause
============================================================================
Expand Down
87 changes: 45 additions & 42 deletions apps/ios-dev/SplatKitDev/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,23 @@ struct ContentView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity)
.accessibilityIdentifier("splat.preparing")
}
hud
// VStack {
// Spacer()
// HStack(alignment: .bottom) {
// Joystick { forward, right in
// session.view.setWalkVelocity(forward: forward * walkSpeed, right: right * walkSpeed)
// }
// Spacer()
// controls
// }
// .padding(24)
// }
if session.scenePrepared && (args.bool("hud") ?? true) {
overlay
}
// The SDK ships no walking control; this is the app's own, over the SDK's view.
if session.walking {
VStack {
Spacer()
HStack(alignment: .bottom) {
Joystick { forward, right in
session.view.setWalkVelocity(forward: forward * walkSpeed, right: right * walkSpeed)
}
Spacer()
controls
}
.padding(24)
}
}
}
.onAppear(perform: start)
.onDisappear { UIApplication.shared.isIdleTimerDisabled = false }
Expand All @@ -47,32 +52,26 @@ struct ContentView: View {
}
}

private var hud: some View {
let s = session.stats
return VStack(alignment: .leading, spacing: 2) {
Text(session.gpu)
Text(session.status)
Text(String(format: "%.0f fps frame %.1f ms gpu %.1f ms sort %.0f ms", s.fps, s.frameMillis, s.gpuMillis, s.sortMillis))
Text("\(s.loadedSplatCount) loaded \(s.drawnSplatCount) drawn")
if s.computeTileCount + s.hardwareTileCount > 0 {
Text("tiles \(s.computeTileCount) compute / \(s.hardwareTileCount) hardware")
.accessibilityIdentifier("splat.tiles")
Text("compute: \(s.nonemptyComputeTileCount) with splats")
} else {
Text("tiles: unavailable")
.accessibilityIdentifier("splat.tiles")
private var shotSelection: Binding<CameraShot?> {
Binding(get: { session.shot }, set: { if let shot = $0 { session.shot = shot } })
}

private var overlay: some View {
VStack(alignment: .leading) {
RenderStatsCard(session: session)
Spacer()
VStack(spacing: 6) {
if session.orbit != nil {
CapsulePicker(items: CameraShot.allCases, selection: shotSelection) { $0.title }
}
CapsulePicker(items: QualityTier.allCases, selection: $session.quality) { $0.title }
}
Text("\(s.walking ? "walk" : "fly") \(s.motion ? "gyro" : "touch")")
if !captureMessage.isEmpty { Text(captureMessage) }
.frame(maxWidth: .infinity)
}
.font(.system(size: 12, weight: .medium, design: .monospaced))
.foregroundColor(.white)
.padding(8)
.background(Color.black.opacity(0.4))
.cornerRadius(6)
.padding(.top, 54)
.padding(.leading, 12)
.allowsHitTesting(false)
.padding(.horizontal, 16)
.padding(.top, 4)
.padding(.bottom, 8)
.transition(.opacity)
}

private var controls: some View {
Expand Down Expand Up @@ -103,6 +102,11 @@ struct ContentView: View {
view.motionToggleEnabled = !issMap
view.maxShDegree = args.int("sh") ?? 1
if let v = args.int("shdraw") { view.shDegree = v }
// Explicit renderer switches win; otherwise start at a quality level, High by default.
let customQuality = ["scale", "shdraw", "depth-key-bits", "min-pixel-radius", "lod-error-pixels", "lod-splat-limit"].contains(where: args.has)
if !customQuality {
session.quality = args.string("quality").flatMap(QualityTier.init(rawValue:)) ?? .high
}
if let v = args.int("budget") { view.splatBudget = v }
if let v = args.int("residency") { view.residencyBudget = v }
if let v = args.float("margin") { view.cullMarginDegrees = v }
Expand All @@ -121,11 +125,9 @@ struct ContentView: View {
let url = LaunchArgs.resolve(collider) {
view.loadCollider(file: url)
}
// A 45 m radius gives a closer inspection view without changing the field of view.
// Broadside to the truss, far enough to clear the solar arrays.
if issMap {
view.lookAt(from: SIMD3<Float>(0, -2, 43),
target: SIMD3<Float>(0, -2, -2),
up: (args.bool("orbit-horizontal") ?? true) ? SIMD3<Float>(0, 1, 0) : SIMD3<Float>(1, 0, 0))
view.lookAt(from: SIMD3<Float>(0, 35, 128), target: SIMD3<Float>(0, -2, -2), up: SIMD3<Float>(0, 1, 0))
} else {
view.cameraPose = CameraPose(x: 0, y: 0, z: 0, yaw: 0, pitch: 0)
}
Expand All @@ -143,11 +145,12 @@ struct ContentView: View {
session.motion = view.isMotionEnabled
if orbitEnabled {
var settings = OrbitPath.Settings(pivot: SIMD3<Float>(0, -2, -2))
settings.horizontal = args.bool("orbit-horizontal") ?? true
if let radius = args.float("radius"), radius.isFinite, radius > 0 { settings.radius = radius }
if let speed = args.float("speed"), speed.isFinite { settings.degreesPerSecond = speed }
if let start = args.float("start"), start.isFinite { settings.startDegrees = start }
let orbit = OrbitPath(view: view, settings: settings)
let shot = args.string("shot").flatMap(CameraShot.init(rawValue:)) ?? .orbit
session.shot = shot
let orbit = OrbitPath(view: view, settings: settings, shot: shot)
session.orbit = orbit
}
if let v = args.float("walk") { view.setWalkVelocity(forward: v, right: 0) }
Expand Down
17 changes: 12 additions & 5 deletions apps/ios-dev/SplatKitDev/LaunchArgs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import Foundation
/// `--linear`, `--gyro <0|1>`, `--pose x,y,z,yaw,pitch`, `--walk <m/s>`,
/// `--benchmark [seconds]`, `--capture <seconds>` (writes Documents/capture.png).
/// The default world is kitchen_500k.spz. ISS stays in Documents; use --world iss_10M.spz.
/// ISS orbits at a constant 45 m radius and 4 degrees/s; --radius, --speed and --start override it.
/// Its long X axis stays horizontal in screen space, without changing the orbit plane.
/// --orbit-horizontal 0 restores the previous vertical framing for benchmark comparisons.
/// ISS runs a camera shot (--shot overview|orbit|detail|flyby|tour, default orbit) with +Y up.
/// The orbit shot circles at 135 m and 5 degrees/s from azimuth 20; --radius (at least 100),
/// --speed and --start override it.
/// Its starting pose is prepared during loading; motion starts only after the first
/// successful GPU frame. Loading stays covered while the renderer prepares that frame.
/// --orbit 0, --pose, --walk or --benchmark disable the automatic orbit and enable touch look.
Expand All @@ -21,17 +21,24 @@ import Foundation
/// --depth-key-bits <16|32> selects linear camera-depth quantization and two radix
/// passes (16), or the unchanged four-pass ordering (32, default). Restart to change.
/// Quantization can change transparency ordering within a bin; no splats are removed.
/// --tile-raster 1 selects bounded hybrid 16x16 compute compositing, with T <= 0.0001.
/// --lod-error-pixels <px> sets the hierarchy refinement threshold (default 1; lower draws more).
/// --lod-splat-limit <n> caps the splats a frame selects, 0 for the loaded budget; detail thins evenly.
/// --tile-raster 1 sets the view's raster policy to hybrid: bounded 16x16 compute compositing,
/// with T <= 0.0001. It helps only where many large splats overlap; ISS orbits run slower.
/// Hardware fallback retains its existing 254/255 opacity coverage mask.
/// Dense tiles (>512 candidates) and tiles touched by large footprints (>16 tiles)
/// use hardware completion, never truncated lists. Scratch is capped at 128 MiB.
/// --quality <ultra|high|balanced|fast> picks the starting level (default high) unless --scale,
/// --shdraw, --depth-key-bits or --min-pixel-radius set the renderer directly.
/// --shot <overview|orbit|detail|flyby|tour> picks the ISS camera shot (default orbit).
/// --hud 0 hides the stats card and quality picker, for screen recordings.
/// --keep-awake 1 disables idle screen locking only while this dev view is active.
/// --resource-monitor 1 logs process footprint, remaining process allowance, Metal
/// allocation and thermal state at 2 Hz, and pauses on memory/thermal pressure.
/// --memory-limit-mib <MiB> lowers its conservative 2800 MiB process-footprint guard.
/// --run-seconds <seconds> enables monitoring and pauses that long after the first world frame.
/// These dev-only guards stop future frames, not allocations or GPU work in flight.
/// Default 0 retains hardware rasterization. A GPU error stops submission until renderer recreation.
/// A GPU error stops submission until renderer recreation.
struct LaunchArgs {
let values: [String: String]

Expand Down
Loading
Loading