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
2 changes: 1 addition & 1 deletion apps/ios-dev/SplatKitDev/SplatViewHost.swift
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ final class SplatSession: ObservableObject {

var pipelineText: String {
let policy = view.renderPolicy
let sh = min(view.shDegree, view.maxShDegree)
let sh = min(view.shDegree, view.maxShDegree, stats.loadedShDegree)
let limit = policy.lodSplatLimit == 0 ? "all" : String(format: "%.1fM", Double(policy.lodSplatLimit) / 1_000_000)
return "SH\(sh) \(policy.sortDepth)b \(limit)"
}
Expand Down
1 change: 1 addition & 0 deletions packages/splatkit-android/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ The [React Native package](../react-native-splatkit/README.md) maps its `policy`

Unavailable GPU timings/tile counters report zero, not zero-cost execution.
Loaded source splats differ from drawn nodes and GPU-resident hierarchy records.
`loadedShDegree` is the harmonics degree the world holds on the GPU, the ceiling for what `shDegree` draws.
Stats float transport exactly represents integers through 16,777,216.
Optional `com.splatkit.ui.SplatHudView` and `JoystickView` are host conveniences.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ void statsToFloats(const Stats& stats, float* out) {
out[8] = static_cast<float>(stats.computeTileCount);
out[9] = static_cast<float>(stats.nonemptyComputeTileCount);
out[10] = static_cast<float>(stats.hardwareTileCount);
out[11] = static_cast<float>(stats.loadedShDegree);
}

RenderPolicy policyFromDoubles(const double* in) {
Expand Down
6 changes: 3 additions & 3 deletions packages/splatkit-android/src/main/cpp/jni/JniConversions.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ inline constexpr jsize kCameraRequestFields = 8;
inline constexpr jsize kCameraStateFields = 9;
inline constexpr jsize kCameraResolutionFields = kCameraStateFields + 1;
// fps, frame, gpu and sort milliseconds, splat count, walking and motion flags, then the drawn,
// compute tile, non-empty compute tile and hardware tile counts.
inline constexpr jsize kStatsFloats = 11;
// compute tile, non-empty compute tile and hardware tile counts, then the world's SH degree.
inline constexpr jsize kStatsFloats = 12;
inline constexpr jsize kAttitudeFloats = 9;

// Policy and capabilities. Both arrays are double: every field is a count, flag or float.
Expand Down Expand Up @@ -50,7 +50,7 @@ void cameraStateToDoubles(const CameraState& state, double* out);
// Request: mode, anchor xyz, radius, azimuth, elevation, angular rate.
CameraRequest cameraRequestFromDoubles(const double* in);

// Fills out[0..10] in the order kStatsFloats lists.
// Fills out[0..11] in the order kStatsFloats lists.
void statsToFloats(const Stats& stats, float* out);

// 0 raster, 1 tileSize, 2 lodErrorPixels, 3 alphaThreshold, 4 subpixelThreshold,
Expand Down
2 changes: 1 addition & 1 deletion packages/splatkit-android/src/main/cpp/jni/SplatKitJni.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ SPLATKIT_JNI(void, nativeStartBenchmark)(JNIEnv*, jobject, jlong handle, jfloat
if (auto* engine = toEngine(handle)) engine->startBenchmark(seconds);
}

// Fills out[0..10] in the order kStatsFloats lists.
// Fills out[0..11] in the order kStatsFloats lists.
SPLATKIT_JNI(void, nativeStats)(JNIEnv* env, jobject, jlong handle, jfloatArray out) {
auto* engine = toEngine(handle);
if (engine == nullptr || out == nullptr || env->GetArrayLength(out) < kStatsFloats) return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ class SplatStats {
var loadedSplatCount: Int
get() = splatCount
set(value) { splatCount = value }
/**
* Spherical harmonics degree the loaded world holds on the GPU, its file degree capped by
* [com.splatkit.SplatSurfaceView.maxShDegree] at load; zero without a world.
*/
var loadedShDegree = 0
var walking = false
var motion = false
/** Last completed visibility/order result; zero remains zero even when a source is loaded. */
Expand All @@ -29,9 +34,9 @@ class SplatStats {
var hardwareTileCount = 0
}

internal const val SPLAT_STATS_FLOATS = 11
internal const val SPLAT_STATS_FLOATS = 12

/** Mirrors nativeStats: timings, the loaded count and two flags, then four completed counts. */
/** Mirrors nativeStats: timings, the loaded count and two flags, then four completed counts and the world's harmonics degree. */
internal fun decodeSplatStats(values: FloatArray, into: SplatStats): SplatStats {
require(values.size >= SPLAT_STATS_FLOATS) { "Stats payload needs $SPLAT_STATS_FLOATS floats" }
into.fps = values[0]
Expand All @@ -45,5 +50,6 @@ internal fun decodeSplatStats(values: FloatArray, into: SplatStats): SplatStats
into.computeTileCount = values[8].toInt()
into.nonemptyComputeTileCount = values[9].toInt()
into.hardwareTileCount = values[10].toInt()
into.loadedShDegree = values[11].toInt()
return into
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class SplatStatsTest {
@Test fun decodesTimingsFlagsAndCompletedCounts() {
val snapshot = SplatStats()
val result = decodeSplatStats(
floatArrayOf(60f, 16f, 7f, 3f, 500000f, 1f, 0f, 123f, 40f, 12f, 8f), snapshot
floatArrayOf(60f, 16f, 7f, 3f, 500000f, 1f, 0f, 123f, 40f, 12f, 8f, 2f), snapshot
)
assertSame(snapshot, result)
assertEquals(60f, result.fps, 0f)
Expand All @@ -22,6 +22,7 @@ class SplatStatsTest {
assertEquals(40, result.computeTileCount)
assertEquals(12, result.nonemptyComputeTileCount)
assertEquals(8, result.hardwareTileCount)
assertEquals(2, result.loadedShDegree)
}

@Test fun zeroCompletedCountsNeverFallBackToLoadedCount() {
Expand All @@ -30,13 +31,15 @@ class SplatStatsTest {
computeTileCount = 5
nonemptyComputeTileCount = 4
hardwareTileCount = 3
loadedShDegree = 3
}
decodeSplatStats(floatArrayOf(0f, 0f, 0f, 0f, 2000000f, 0f, 1f, 0f, 0f, 0f, 0f), snapshot)
decodeSplatStats(floatArrayOf(0f, 0f, 0f, 0f, 2000000f, 0f, 1f, 0f, 0f, 0f, 0f, 0f), snapshot)
assertEquals(2000000, snapshot.loadedSplatCount)
assertEquals(0, snapshot.drawnSplatCount)
assertEquals(0, snapshot.computeTileCount)
assertEquals(0, snapshot.nonemptyComputeTileCount)
assertEquals(0, snapshot.hardwareTileCount)
assertEquals(0, snapshot.loadedShDegree)
assertFalse(snapshot.walking)
assertTrue(snapshot.motion)
decodeSplatStats(FloatArray(SPLAT_STATS_FLOATS), snapshot)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ struct Stats {
float gpuMillis = 0; // GPU time of the last frame, from timestamp queries
float sortMillis = 0; // last completed sort
uint32_t splatCount = 0;
uint32_t loadedShDegree = 0; // harmonics the loaded world holds on the GPU; zero without a world
bool walking = false;
bool motion = false;
uint32_t drawnSplatCount = 0; // last completed visibility/order result, not source count
Expand Down Expand Up @@ -59,6 +60,7 @@ class StatsPublisher {
std::size_t selected = 0; // nodes the level of detail selection chose
uint32_t sourceSplats = 0; // splats in the file, what hosts count
uint32_t gpuSplats = 0; // records on the GPU, more than the file with a tree
uint32_t shDegree = 0; // harmonics the world holds on the GPU
bool walking = false;
bool motion = false;
uint32_t computeTiles = 0;
Expand Down Expand Up @@ -104,6 +106,7 @@ class StatsPublisher {
std::atomic<float> gpuMillis_{0};
std::atomic<float> sortMillis_{0};
std::atomic<uint32_t> splats_{0};
std::atomic<uint32_t> loadedShDegree_{0};
std::atomic<uint32_t> drawnSplats_{0};
std::atomic<uint32_t> computeTiles_{0};
std::atomic<uint32_t> nonemptyComputeTiles_{0};
Expand Down
2 changes: 2 additions & 0 deletions packages/splatkit-engine/src/diagnostics/StatsPublisher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ void StatsPublisher::publish(const Sample& s) {
gpuMillis_.store(static_cast<float>(s.gpuMillis), kRelaxed);
sortMillis_.store(static_cast<float>(s.sortMillis), kRelaxed);
splats_.store(s.sourceSplats, kRelaxed);
loadedShDegree_.store(s.shDegree, kRelaxed);
drawnSplats_.store(s.drawn, kRelaxed);
computeTiles_.store(s.computeTiles, kRelaxed);
nonemptyComputeTiles_.store(s.nonemptyComputeTiles, kRelaxed);
Expand Down Expand Up @@ -116,6 +117,7 @@ Stats StatsPublisher::stats() const {
s.gpuMillis = gpuMillis_.load(kRelaxed);
s.sortMillis = sortMillis_.load(kRelaxed);
s.splatCount = splats_.load(kRelaxed);
s.loadedShDegree = loadedShDegree_.load(kRelaxed);
s.drawnSplatCount = drawnSplats_.load(kRelaxed);
s.computeTileCount = computeTiles_.load(kRelaxed);
s.nonemptyComputeTileCount = nonemptyComputeTiles_.load(kRelaxed);
Expand Down
1 change: 1 addition & 0 deletions packages/splatkit-engine/src/engine/SplatEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ StatsPublisher::Sample SplatEngine::sample() const {
const std::optional<GpuWorldInfo> world = renderer_->world();
s.sourceSplats = world ? sourceCount_ : 0;
s.gpuSplats = world ? (streamer_ ? streamer_->held() : world->count) : 0;
s.shDegree = world ? static_cast<uint32_t>(world->shDegree) : 0;
s.walking = camera_.hasCollider();
s.motion = camera_.motionEnabled();
return s;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ namespace {

constexpr int64_t kHalfSecond = 500'000'000LL;

StatsPublisher::Sample counts(uint32_t drawn, uint32_t source, double gpuMillis) {
StatsPublisher::Sample counts(uint32_t drawn, uint32_t source, double gpuMillis,
uint32_t shDegree = 0) {
StatsPublisher::Sample s;
s.drawn = drawn;
s.sourceSplats = source;
s.gpuMillis = gpuMillis;
s.shDegree = shDegree;
return s;
}

Expand All @@ -31,10 +33,11 @@ TEST(StatsPublisher, WindowCloseIsTheOnlyRegularRefresh) {
TEST(StatsPublisher, PublishRefreshesCountsWithoutClosingTheWindow) {
StatsPublisher stats;
stats.onFrame(kHalfSecond, true, [] { return counts(1, 1, 1); });
stats.publish(counts(7, 9, 3));
stats.publish(counts(7, 9, 3, 2));
Stats now = stats.stats();
EXPECT_EQ(now.drawnSplatCount, 7u);
EXPECT_EQ(now.splatCount, 9u);
EXPECT_EQ(now.loadedShDegree, 2u);
EXPECT_FLOAT_EQ(now.gpuMillis, 3.0f);
EXPECT_FLOAT_EQ(now.fps, 0.0f);

Expand Down
19 changes: 18 additions & 1 deletion packages/splatkit-engine/tests/rendering/FrameOrderTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class RecordingRenderer final : public StubRenderer {
explicit RecordingRenderer(bool gpu) : gpu_(gpu) {}
Extent drawExtent() const override { return extent; }
bool uploadWorld(const splat::SplatCloud& cloud, int) override {
world_ = GpuWorldInfo{static_cast<uint32_t>(cloud.count()), 0};
world_ = GpuWorldInfo{static_cast<uint32_t>(cloud.count()), heldShDegree};
return true;
}
std::optional<GpuWorldInfo> world() const override { return world_; }
Expand All @@ -38,6 +38,7 @@ class RecordingRenderer final : public StubRenderer {
ScreenTileStats tiles;
bool gpuLod = false;
uint32_t uploadedBudget = 0;
int heldShDegree = 0;

private:
bool gpu_;
Expand Down Expand Up @@ -115,6 +116,22 @@ TEST(FrameOrder, CompletedScreenTileCountsReachPublishedStatsAndClearWhenUnavail
EXPECT_EQ(stats.hardwareTileCount, 0u);
}

TEST(FrameOrder, LoadedWorldShDegreeReachesPublishedStatsAndIsZeroWithoutAWorld) {
auto renderer = std::make_unique<RecordingRenderer>(true);
auto* observed = renderer.get();
observed->heldShDegree = 2;
SplatEngine engine(std::move(renderer));
engine.render(1);
engine.requestRedraw();
engine.render(500000001);
EXPECT_EQ(engine.stats().loadedShDegree, 0u);
const auto bytes = worldBytes();
engine.loadWorld(bytes.data(), bytes.size());
engine.requestRedraw();
engine.render(1000000002);
EXPECT_EQ(engine.stats().loadedShDegree, 2u);
}

TEST(FrameOrder, RendererWithoutGpuSortFailsTheLoadInsteadOfFallingBackToCpu) {
auto renderer = std::make_unique<RecordingRenderer>(false);
auto* observed = renderer.get();
Expand Down
1 change: 1 addition & 0 deletions packages/splatkit-ios/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,5 +130,6 @@ Selection feeds visibility → radix → raster; denied refinements retain paren
The hierarchy stays resident, without Hi-Z or temporal transitions.

Stats: `loadedSplatCount` counts source splats; `drawnSplatCount` counts completed draw candidates.
`loadedShDegree` is the harmonics degree the world holds on the GPU, the ceiling for what `shDegree` draws.
Command timings overlap; HUD sort includes visibility/radix.
[Phone measurements](../../docs/BENCHMARKS.md) record settings, evidence and visual limitations.
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ public final class SplatMetalView: UIView {
stats.gpuMillis = s.gpuMillis
stats.sortMillis = s.sortMillis
stats.splatCount = Int(s.splatCount)
stats.loadedShDegree = Int(s.loadedShDegree)
stats.drawnSplatCount = Int(s.drawnSplatCount)
stats.computeTileCount = Int(s.computeTileCount)
stats.nonemptyComputeTileCount = Int(s.nonemptyComputeTileCount)
Expand Down
3 changes: 3 additions & 0 deletions packages/splatkit-ios/Sources/SplatKit/SplatStats.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ public struct SplatStats {
public var gpuMillis: Float = 0
public var sortMillis: Float = 0
public var splatCount: Int = 0
/// Spherical harmonics degree the loaded world holds on the GPU, its file degree capped by
/// `maxShDegree` at load; zero without a world. `shDegree` above it draws no more.
public var loadedShDegree: Int = 0
/// Splats in the loaded source world; not GPU residency for a streamed world.
public var loadedSplatCount: Int { splatCount }
/// Last completed visibility/order result, refreshed twice a second without a GPU wait.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ SKSplatStats fromCppStats(const Stats& s) {
s.presentTiming,
s.frameMillisP95,
s.lowFps,
s.droppedFrames};
s.droppedFrames,
s.loadedShDegree};
}

SKDeviceCapabilities fromCppCapabilities(const DeviceCapabilities& caps) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ typedef struct {
float lowFps;
/// Submitted frames never shown in the last half second.
uint32_t droppedFrames;
/// Spherical harmonics degree the loaded world holds on the GPU; zero without a world.
uint32_t loadedShDegree;
} SKSplatStats;

/// The renderer-applicable policy. Mirror of the host contract: raster 0 hardware,
Expand Down
2 changes: 2 additions & 0 deletions packages/splatkit-ios/tests/engine/SKTypeConversionsTest.mm
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ void expectSamePolicy(const RenderPolicy& actual, const RenderPolicy& expected)
stats.computeTileCount = 11;
stats.nonemptyComputeTileCount = 7;
stats.hardwareTileCount = 5;
stats.loadedShDegree = 2;

const SKSplatStats out = fromCppStats(stats);
EXPECT_EQ(out.fps, 59.5f);
Expand All @@ -135,6 +136,7 @@ void expectSamePolicy(const RenderPolicy& actual, const RenderPolicy& expected)
EXPECT_EQ(out.frameMillisP95, 21.0f);
EXPECT_EQ(out.lowFps, 41.0f);
EXPECT_EQ(out.droppedFrames, 3u);
EXPECT_EQ(out.loadedShDegree, 2u);
}

TEST(SKTypeConversionsTest, CapabilitiesCarryLimitsFeaturesSupportAndTheFallbackPolicy) {
Expand Down
Loading