diff --git a/packages/splatkit-android/src/main/cpp/engine/AndroidEngine.cpp b/packages/splatkit-android/src/main/cpp/engine/AndroidEngine.cpp index 0c334d1d..b1f78b14 100644 --- a/packages/splatkit-android/src/main/cpp/engine/AndroidEngine.cpp +++ b/packages/splatkit-android/src/main/cpp/engine/AndroidEngine.cpp @@ -2,6 +2,7 @@ #include +#include "engine/TraceSection.h" #include "splatkit/Log.h" namespace splatkit { @@ -51,9 +52,11 @@ void AndroidEngine::setEventSink(EventSink sink) { } void AndroidEngine::render(int64_t frameTimeNanos) { + const TraceSection section(trace::kFrame); // Before the engine samples its stats, which a still scene would otherwise leave stale. renderer_->collectCompletedFrames(); engine_->render(frameTimeNanos); + traceBenchmark(); if (awaitingWorldFrame_ && renderer_->hasCompletedWorldFrame()) { awaitingWorldFrame_ = false; // The frame may have finished during this render: read it, so stats read after the @@ -64,4 +67,15 @@ void AndroidEngine::render(int64_t frameTimeNanos) { } } +// One async slice over the benchmark turn, so a trace can be cut to the measured window. +void AndroidEngine::traceBenchmark() { + const bool running = engine_->benchmarkRunning(); + if (running == benchmarkTraced_) return; + benchmarkTraced_ = running; + if (running) + ATrace_beginAsyncSection(trace::kBenchmark, trace::kBenchmarkCookie); + else + ATrace_endAsyncSection(trace::kBenchmark, trace::kBenchmarkCookie); +} + } // namespace splatkit diff --git a/packages/splatkit-android/src/main/cpp/engine/AndroidEngine.h b/packages/splatkit-android/src/main/cpp/engine/AndroidEngine.h index c5e142fe..80c92781 100644 --- a/packages/splatkit-android/src/main/cpp/engine/AndroidEngine.h +++ b/packages/splatkit-android/src/main/cpp/engine/AndroidEngine.h @@ -39,10 +39,12 @@ class AndroidEngine { private: AndroidEngine() = default; + void traceBenchmark(); EventSink eventSink_; bool awaitingWorldFrame_ = false; uint32_t worldFrameSplatCount_ = 0; + bool benchmarkTraced_ = false; std::unique_ptr ctx_; std::unique_ptr frameLoop_; diff --git a/packages/splatkit-android/src/main/cpp/engine/TraceSection.h b/packages/splatkit-android/src/main/cpp/engine/TraceSection.h new file mode 100644 index 00000000..5ba3fbae --- /dev/null +++ b/packages/splatkit-android/src/main/cpp/engine/TraceSection.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +#include + +namespace splatkit { + +// Slice names shown on the calling thread in Perfetto; scripts/perfetto_analysis.py matches them. +namespace trace { +inline constexpr char kFrame[] = "SplatKit frame"; +inline constexpr char kWaitForFrameSlot[] = "SplatKit wait for frame slot"; +inline constexpr char kEncodeCompute[] = "SplatKit encode compute"; +inline constexpr char kSubmitPresent[] = "SplatKit submit and present"; +inline constexpr char kUploadWorld[] = "SplatKit upload world"; +inline constexpr char kUploadTile[] = "SplatKit upload tile"; +inline constexpr char kDecodeWorld[] = "SplatKit decode world"; +inline constexpr char kDecodeCollider[] = "SplatKit decode collider"; +// Async slice on the process track; one benchmark runs at a time, so one cookie. +inline constexpr char kBenchmark[] = "SplatKit benchmark"; +inline constexpr int32_t kBenchmarkCookie = 1; +} // namespace trace + +// Scoped slice; costs one flag check while nothing records the app. +class TraceSection { + public: + explicit TraceSection(const char* name) { ATrace_beginSection(name); } + ~TraceSection() { ATrace_endSection(); } + TraceSection(const TraceSection&) = delete; + TraceSection& operator=(const TraceSection&) = delete; +}; + +} // namespace splatkit diff --git a/packages/splatkit-android/src/main/cpp/jni/SplatKitJni.cpp b/packages/splatkit-android/src/main/cpp/jni/SplatKitJni.cpp index ab3d043c..1a814ceb 100644 --- a/packages/splatkit-android/src/main/cpp/jni/SplatKitJni.cpp +++ b/packages/splatkit-android/src/main/cpp/jni/SplatKitJni.cpp @@ -12,6 +12,7 @@ #include #include "engine/AndroidEngine.h" +#include "engine/TraceSection.h" #include "jni/JniConversions.h" #include "splatkit/Log.h" @@ -22,6 +23,7 @@ namespace { +using splatkit::TraceSection; using splatkit::jni::cameraPoseToFloats; using splatkit::jni::cameraRequestFromDoubles; using splatkit::jni::cameraStateToDoubles; @@ -42,6 +44,7 @@ using splatkit::jni::statsToFloats; using splatkit::jni::toJni; using splatkit::jni::withBytes; using splatkit::jni::withUtf8; +namespace trace = splatkit::trace; // The handle Kotlin holds is the host's address; JNI has no other way to carry it. splatkit::AndroidEngine* toHost(jlong handle) { @@ -164,6 +167,7 @@ SPLATKIT_JNI(void, nativeLoadWorld)(JNIEnv* env, jobject, jlong handle, jbyteArr auto* engine = toEngine(handle); if (engine == nullptr) return; withBytes(env, bytes, [engine](const std::uint8_t* data, std::size_t size) { + const TraceSection section(trace::kDecodeWorld); engine->loadWorld(data, size); }); } @@ -172,6 +176,7 @@ SPLATKIT_JNI(void, nativeLoadCollider)(JNIEnv* env, jobject, jlong handle, jbyte auto* engine = toEngine(handle); if (engine == nullptr) return; withBytes(env, bytes, [engine](const std::uint8_t* data, std::size_t size) { + const TraceSection section(trace::kDecodeCollider); engine->loadCollider(data, size); }); } @@ -179,19 +184,28 @@ SPLATKIT_JNI(void, nativeLoadCollider)(JNIEnv* env, jobject, jlong handle, jbyte SPLATKIT_JNI(void, nativeLoadWorldFile)(JNIEnv* env, jobject, jlong handle, jstring path) { auto* engine = toEngine(handle); if (engine == nullptr) return; - withUtf8(env, path, [engine](const std::string& p) { engine->loadWorldFile(p); }); + withUtf8(env, path, [engine](const std::string& p) { + const TraceSection section(trace::kDecodeWorld); + engine->loadWorldFile(p); + }); } SPLATKIT_JNI(void, nativeLoadTiledWorldFile)(JNIEnv* env, jobject, jlong handle, jstring path) { auto* engine = toEngine(handle); if (engine == nullptr) return; - withUtf8(env, path, [engine](const std::string& p) { engine->loadTiledWorldFile(p); }); + withUtf8(env, path, [engine](const std::string& p) { + const TraceSection section(trace::kDecodeWorld); + engine->loadTiledWorldFile(p); + }); } SPLATKIT_JNI(void, nativeLoadColliderFile)(JNIEnv* env, jobject, jlong handle, jstring path) { auto* engine = toEngine(handle); if (engine == nullptr) return; - withUtf8(env, path, [engine](const std::string& p) { engine->loadColliderFile(p); }); + withUtf8(env, path, [engine](const std::string& p) { + const TraceSection section(trace::kDecodeCollider); + engine->loadColliderFile(p); + }); } // Camera and input. diff --git a/packages/splatkit-android/src/main/cpp/rendering/vulkan/VulkanSplatRenderer.cpp b/packages/splatkit-android/src/main/cpp/rendering/vulkan/VulkanSplatRenderer.cpp index 23f64860..ea2d9484 100644 --- a/packages/splatkit-android/src/main/cpp/rendering/vulkan/VulkanSplatRenderer.cpp +++ b/packages/splatkit-android/src/main/cpp/rendering/vulkan/VulkanSplatRenderer.cpp @@ -3,6 +3,7 @@ #include #include +#include "engine/TraceSection.h" #include "rendering/vulkan/sort/VulkanRadixSort.h" #include "splatkit/Log.h" @@ -118,6 +119,7 @@ std::optional VulkanSplatRenderer::world() const { } bool VulkanSplatRenderer::uploadWorld(const splat::SplatCloud& cloud, int maxShDegree) { + const TraceSection section(trace::kUploadWorld); if (!splats_ || cloud.count() > std::numeric_limits::max()) return false; ctx_.waitIdle(); auto compute = VulkanFrameCompute::create(ctx_, static_cast(cloud.count())); @@ -148,6 +150,7 @@ bool VulkanSplatRenderer::selectsLodOnGpu() const { bool VulkanSplatRenderer::uploadLodWorld(const splat::LodTree& tree, int maxShDegree, uint32_t budget) { + const TraceSection section(trace::kUploadWorld); if (!splats_ || tree.nodeCount() > std::numeric_limits::max()) return false; ctx_.waitIdle(); auto compute = @@ -167,6 +170,7 @@ bool VulkanSplatRenderer::uploadLodWorld(const splat::LodTree& tree, int maxShDe } bool VulkanSplatRenderer::createSlab(uint32_t capacity, int shDegree) { + const TraceSection section(trace::kUploadWorld); if (!splats_) return false; ctx_.waitIdle(); auto compute = VulkanFrameCompute::create(ctx_, capacity); @@ -186,6 +190,7 @@ bool VulkanSplatRenderer::createSlab(uint32_t capacity, int shDegree) { } bool VulkanSplatRenderer::uploadTile(uint32_t offset, const splat::SplatCloud& cloud) { + const TraceSection section(trace::kUploadTile); if (!splats_ || !world_) return false; return world_->uploadTile(offset, cloud); } @@ -194,7 +199,10 @@ bool VulkanSplatRenderer::draw(const Frame& frame) { if (!ready()) return false; uint32_t imageIndex = 0; VkCommandBuffer cmd = VK_NULL_HANDLE; - VulkanFrameLoop::Status status = frameLoop_.beginFrame(surface_.swapchain(), imageIndex, cmd); + VulkanFrameLoop::Status status = [&] { + const TraceSection section(trace::kWaitForFrameSlot); + return frameLoop_.beginFrame(surface_.swapchain(), imageIndex, cmd); + }(); if (status == VulkanFrameLoop::Status::swapchainOutOfDate) { rebuildSurface(status); return false; @@ -209,7 +217,10 @@ bool VulkanSplatRenderer::draw(const Frame& frame) { if (world_ && compute_ && frame.orderSource == OrderSource::gpu) { const VkBuffer camera = camera_->update(slot, frame.view, frame.proj, frame.cameraPosition, extent, outputLinear); - gpuDraw = compute_->encode(cmd, slot, camera, *world_->splats, frame); + { + const TraceSection section(trace::kEncodeCompute); + gpuDraw = compute_->encode(cmd, slot, camera, *world_->splats, frame); + } if (gpuDraw) splats_->bindOrder(slot, gpuDraw->order, gpuDraw->capacity); else @@ -253,7 +264,10 @@ bool VulkanSplatRenderer::draw(const Frame& frame) { surface_.blitIfScaled(cmd, imageIndex); const uint64_t submittedBefore = frameLoop_.lastSubmission(); - status = frameLoop_.endFrame(surface_.swapchain(), imageIndex); + { + const TraceSection section(trace::kSubmitPresent); + status = frameLoop_.endFrame(surface_.swapchain(), imageIndex); + } if (gpuDraw && frameLoop_.lastSubmission() != submittedBefore) compute_->submitted(slot, frameLoop_.lastSubmission()); const bool drewWorld = diff --git a/packages/splatkit-engine/include/splatkit/engine/SplatEngine.h b/packages/splatkit-engine/include/splatkit/engine/SplatEngine.h index 9b2840f4..52b397ad 100644 --- a/packages/splatkit-engine/include/splatkit/engine/SplatEngine.h +++ b/packages/splatkit-engine/include/splatkit/engine/SplatEngine.h @@ -171,6 +171,8 @@ class SplatEngine { // Runs a reproducible capture: gyroscope off, a fixed pose, one full yaw turn over // `seconds`, then logs the frame time distribution. Waits for a world if none is up. void startBenchmark(float seconds); + // Render thread: true from the first recorded frame of the turn until its summary. + bool benchmarkRunning() const { return benchmark_.running(); } const std::string& gpuDescription() const { return renderer_->deviceDescription(); } private: