From b6704a2a123759dbd11925771d736a6a35ff6e02 Mon Sep 17 00:00:00 2001 From: Xget7 Date: Tue, 20 Oct 2026 01:06:15 -0300 Subject: [PATCH] Name the Android renderer's frame, upload and decode work in Perfetto TraceSection wraps ATrace sections on the render thread (frame, wait for a frame slot, compute encode, submit and present, world and tile uploads) and on the loader (world and collider decode). An async "SplatKit benchmark" slice spans the benchmark turn, read through the new SplatEngine::benchmarkRunning(), so an analysis can measure exactly that window. Each section costs a flag check while nothing traces. --- .../src/main/cpp/engine/AndroidEngine.cpp | 14 ++++++++ .../src/main/cpp/engine/AndroidEngine.h | 2 ++ .../src/main/cpp/engine/TraceSection.h | 33 +++++++++++++++++++ .../src/main/cpp/jni/SplatKitJni.cpp | 20 +++++++++-- .../rendering/vulkan/VulkanSplatRenderer.cpp | 20 +++++++++-- .../include/splatkit/engine/SplatEngine.h | 2 ++ 6 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 packages/splatkit-android/src/main/cpp/engine/TraceSection.h 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: