From 5aa5344658f6d767023ef9bf3bd5fd3b4d650cbe Mon Sep 17 00:00:00 2001 From: Xget7 Date: Tue, 20 Oct 2026 10:47:38 -0300 Subject: [PATCH 1/2] Keep the engine drawing while the renderer waits to rebuild its surface --- .../splatkit/rendering/SplatRenderer.h | 4 ++++ .../src/engine/SplatEngine.cpp | 4 +++- .../tests/rendering/FrameOrderTest.cpp | 23 +++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/splatkit-engine/include/splatkit/rendering/SplatRenderer.h b/packages/splatkit-engine/include/splatkit/rendering/SplatRenderer.h index d0db113..655eb0c 100644 --- a/packages/splatkit-engine/include/splatkit/rendering/SplatRenderer.h +++ b/packages/splatkit-engine/include/splatkit/rendering/SplatRenderer.h @@ -82,6 +82,10 @@ class SplatRenderer { virtual Extent drawExtent() const = 0; // Counts the rebuilds of the surface's images. A frame drawn before one is gone. virtual uint32_t generation() const = 0; + // True while the surface is waiting to be rebuilt at the size the host gave it. The + // platform only reports that size after a frame is presented, so the engine keeps + // drawing until this turns false. Render thread only. + virtual bool surfaceRebuildPending() const { return false; } // Uploads a world and draws it from now on. Fails, keeping the previous world, when // the upload does. diff --git a/packages/splatkit-engine/src/engine/SplatEngine.cpp b/packages/splatkit-engine/src/engine/SplatEngine.cpp index 81d50ee..09e1bdf 100644 --- a/packages/splatkit-engine/src/engine/SplatEngine.cpp +++ b/packages/splatkit-engine/src/engine/SplatEngine.cpp @@ -393,7 +393,9 @@ void SplatEngine::render(int64_t frameTimeNanos) { if (benchmark_.running() || camera->view.m != lastDrawnView_.m) redrawNeeded_ = true; } const uint32_t generation = renderer_->generation(); - if (generation != lastDrawnGeneration_) redrawNeeded_ = true; + if (generation != lastDrawnGeneration_ || renderer_->surfaceRebuildPending()) { + redrawNeeded_ = true; + } if (renderer_->reportsPresentTimes()) { const uint32_t dropped = renderer_->takePresentTimes(&presentTimes_); stats_.onPresented(presentTimes_, dropped); diff --git a/packages/splatkit-engine/tests/rendering/FrameOrderTest.cpp b/packages/splatkit-engine/tests/rendering/FrameOrderTest.cpp index ce29d7c..3c10985 100644 --- a/packages/splatkit-engine/tests/rendering/FrameOrderTest.cpp +++ b/packages/splatkit-engine/tests/rendering/FrameOrderTest.cpp @@ -31,12 +31,14 @@ class RecordingRenderer final : public StubRenderer { return true; } ScreenTileStats lastScreenTileStats() const override { return tiles; } + bool surfaceRebuildPending() const override { return rebuildPending; } Extent extent{1000, 1000}; OrderSource source = OrderSource::cpu; std::vector ranges; uint32_t frames = 0; ScreenTileStats tiles; bool gpuLod = false; + bool rebuildPending = false; uint32_t uploadedBudget = 0; int heldShDegree = 0; @@ -198,6 +200,27 @@ TEST(FrameOrder, OrbitAnimationDrawsUntilItFinishesThenIdles) { EXPECT_EQ(observed->frames, beforeAnimation + 1); } +TEST(FrameOrder, RendererWithAPendingSurfaceRebuildKeepsTheEngineDrawingUntilItSettles) { + auto renderer = std::make_unique(true); + auto* observed = renderer.get(); + SplatEngine engine(std::move(renderer)); + const auto bytes = worldBytes(); + engine.loadWorld(bytes.data(), bytes.size()); + engine.render(1); + engine.render(16666667); + const uint32_t settled = observed->frames; // an unchanged scene idles + + observed->rebuildPending = true; + engine.render(33333333); + engine.render(50000000); + EXPECT_EQ(observed->frames, settled + 2); + + observed->rebuildPending = false; + engine.render(66666667); + engine.render(83333333); + EXPECT_EQ(observed->frames, settled + 2); +} + // A phone that turns to portrait while a world loads must end up where one held in // portrait all along does: the default orbit frames the view's current shape. TEST(FrameOrder, DefaultOrbitFramingFollowsTheViewShape) { From d77ab404db1e9b03d15f4e43d24b4fd987e504ab Mon Sep 17 00:00:00 2001 From: Xget7 Date: Tue, 20 Oct 2026 10:54:03 -0300 Subject: [PATCH 2/2] Rebuild the Android swapchain once the surface reports the window's size --- .../rendering/vulkan/VulkanSplatRenderer.h | 1 + .../vulkan/presentation/VulkanSurface.cpp | 55 ++++++++++++------- .../vulkan/presentation/VulkanSurface.h | 17 ++++-- 3 files changed, 49 insertions(+), 24 deletions(-) diff --git a/packages/splatkit-android/src/main/cpp/rendering/vulkan/VulkanSplatRenderer.h b/packages/splatkit-android/src/main/cpp/rendering/vulkan/VulkanSplatRenderer.h index 14330c1..cee30b8 100644 --- a/packages/splatkit-android/src/main/cpp/rendering/vulkan/VulkanSplatRenderer.h +++ b/packages/splatkit-android/src/main/cpp/rendering/vulkan/VulkanSplatRenderer.h @@ -52,6 +52,7 @@ class VulkanSplatRenderer final : public SplatRenderer { Extent drawExtent() const override; // Counts the rebuilds of the swapchain or the target. uint32_t generation() const override { return surface_.generation(); } + bool surfaceRebuildPending() const override { return surface_.rebuildPending(); } // Uploads once the GPU is done with the previous world. Needs `ready()`. bool uploadWorld(const splat::SplatCloud& cloud, int maxShDegree) override; diff --git a/packages/splatkit-android/src/main/cpp/rendering/vulkan/presentation/VulkanSurface.cpp b/packages/splatkit-android/src/main/cpp/rendering/vulkan/presentation/VulkanSurface.cpp index 2897b27..b0bd0ab 100644 --- a/packages/splatkit-android/src/main/cpp/rendering/vulkan/presentation/VulkanSurface.cpp +++ b/packages/splatkit-android/src/main/cpp/rendering/vulkan/presentation/VulkanSurface.cpp @@ -31,6 +31,9 @@ bool VulkanSurface::attach(ANativeWindow* window) { ANativeWindow_acquire(window); window_ = window; + windowExtent_ = {static_cast(std::max(0, ANativeWindow_getWidth(window))), + static_cast(std::max(0, ANativeWindow_getHeight(window)))}; + staleChecks_ = 0; if (!createSurface() || !recreateSwapchain()) { LOGE("could not attach to the surface"); teardown(); @@ -41,11 +44,13 @@ bool VulkanSurface::attach(ANativeWindow* window) { bool VulkanSurface::resize(uint32_t width, uint32_t height) { if (!swapchain_) return true; + windowExtent_ = {width, height}; + staleChecks_ = 0; const VkExtent2D current = swapchain_->extent(); if (current.width == width && current.height == height) return true; LOGI("surface resized to %ux%u, swapchain was %ux%u", width, height, current.width, current.height); - return rebuildOrDrop(); + return rebuildWhenSurfaceReportsWindowSize(); } bool VulkanSurface::setRenderScale(float scale) { @@ -72,11 +77,14 @@ bool VulkanSurface::setVsync(bool vsync) { } bool VulkanSurface::rebuildIfChanged(VulkanFrameLoop::Status status) { - if (status == VulkanFrameLoop::Status::swapchainOutOfDate || - (status == VulkanFrameLoop::Status::swapchainSuboptimal && surfaceExtentChanged())) { - return rebuildOrDrop(); - } - return true; + if (status == VulkanFrameLoop::Status::swapchainOutOfDate) return rebuildOrDrop(); + return !rebuildPending() || rebuildWhenSurfaceReportsWindowSize(); +} + +bool VulkanSurface::rebuildPending() const { + if (!swapchain_) return false; + const VkExtent2D current = swapchain_->extent(); + return current.width != windowExtent_.width || current.height != windowExtent_.height; } // A rebuild that fails leaves nothing to draw into, so the surface is dropped and stays @@ -166,23 +174,30 @@ bool VulkanSurface::rebuildOrDrop() { return false; } -// True when the surface no longer has the swapchain's size, which is how a rotation shows -// up when the driver only answers SUBOPTIMAL. A bare SUBOPTIMAL with the same size is the -// identity pre-transform being second best, and is not worth a rebuild. -bool VulkanSurface::surfaceExtentChanged() const { +// Until a frame is presented, the surface keeps reporting its old size after the window +// changed, and a swapchain built from that report is the old size again. So the rebuild +// waits for the report to match the window. A report that never matches ends the wait +// after `kMaxStaleChecks` frames and the swapchain takes the size the surface gives. +bool VulkanSurface::rebuildWhenSurfaceReportsWindowSize() { VkSurfaceCapabilitiesKHR caps{}; - if (vkGetPhysicalDeviceSurfaceCapabilitiesKHR(ctx_.physicalDevice(), surface_, &caps) != + if (vkGetPhysicalDeviceSurfaceCapabilitiesKHR(ctx_.physicalDevice(), surface_, &caps) == VK_SUCCESS) { - return false; - } - const VkExtent2D current = swapchain_->extent(); - const bool changed = - caps.currentExtent.width != current.width || caps.currentExtent.height != current.height; - if (changed) { - LOGI("surface is now %ux%u, swapchain was %ux%u", caps.currentExtent.width, - caps.currentExtent.height, current.width, current.height); + const bool reported = caps.currentExtent.width == windowExtent_.width && + caps.currentExtent.height == windowExtent_.height; + if (reported) { + LOGI("surface now reports %ux%u", caps.currentExtent.width, caps.currentExtent.height); + staleChecks_ = 0; + return rebuildOrDrop(); + } } - return changed; + if (++staleChecks_ <= kMaxStaleChecks) return true; + LOGW("surface still reports %ux%u, window is %ux%u; using the surface's size", + caps.currentExtent.width, caps.currentExtent.height, windowExtent_.width, + windowExtent_.height); + staleChecks_ = 0; + if (!rebuildOrDrop()) return false; + windowExtent_ = swapchain_->extent(); + return true; } void VulkanSurface::teardown() { diff --git a/packages/splatkit-android/src/main/cpp/rendering/vulkan/presentation/VulkanSurface.h b/packages/splatkit-android/src/main/cpp/rendering/vulkan/presentation/VulkanSurface.h index 548c4af..1302c05 100644 --- a/packages/splatkit-android/src/main/cpp/rendering/vulkan/presentation/VulkanSurface.h +++ b/packages/splatkit-android/src/main/cpp/rendering/vulkan/presentation/VulkanSurface.h @@ -23,6 +23,9 @@ class VulkanSurface { public: static constexpr float kMinRenderScale = 0.1f; static constexpr float kMaxRenderScale = 2.0f; + // Frames a rebuild waits for the surface to report the window's size. A surface that never + // does (a fixed-size buffer) is built at the size it reports instead of redrawing forever. + static constexpr uint32_t kMaxStaleChecks = 60; // `beforeTeardown` runs while the device is idle, before the target, swapchain and // surface are destroyed, so the owner can release what draws into them. @@ -37,15 +40,16 @@ class VulkanSurface { // A new window (takes a reference) or nullptr when the surface is going away. bool attach(ANativeWindow* window); - // The window changed size while staying attached. Rebuilds the swapchain if needed. + // The window changed size while staying attached. Rebuilds the swapchain at once when the + // surface already reports that size; otherwise waits for it (see `rebuildPending`). bool resize(uint32_t width, uint32_t height); // Clamped to [kMinRenderScale, kMaxRenderScale]; below or above 1 draws offscreen. bool setRenderScale(float scale); // Flips the swapchain format. bool setLinearBlending(bool linear); bool setVsync(bool vsync); - // Rebuilds the swapchain after a frame: always when out of date, and when suboptimal only - // if the surface no longer has the swapchain's size. Other statuses change nothing. + // Rebuilds the swapchain after a frame: always when out of date, and when it is not the + // window's size once the surface reports the window's size. Other statuses change nothing. bool rebuildIfChanged(VulkanFrameLoop::Status status); // For a rebuild the owner failed after the surface succeeded: logs and drops it. void drop(); @@ -54,6 +58,9 @@ class VulkanSurface { bool ready() const { return swapchain_ != nullptr; } float renderScale() const { return renderScale_; } bool linearBlending() const { return linearBlending_; } + // True while the swapchain is not the window's size. Android reports the new size only + // after a frame is presented, so the owner keeps drawing until this turns false. + bool rebuildPending() const; // Counts the rebuilds of the swapchain or the target. uint32_t generation() const { return generation_; } @@ -72,7 +79,7 @@ class VulkanSurface { bool recreateSwapchain(); bool createRenderTarget(); bool rebuildOrDrop(); - bool surfaceExtentChanged() const; + bool rebuildWhenSurfaceReportsWindowSize(); void teardown(); void destroySwapchainAndSurface(); @@ -80,6 +87,8 @@ class VulkanSurface { VulkanFrameLoop& frameLoop_; std::function beforeTeardown_; ANativeWindow* window_ = nullptr; + VkExtent2D windowExtent_{}; // the size the swapchain must reach + uint32_t staleChecks_ = 0; VkSurfaceKHR surface_ = VK_NULL_HANDLE; std::unique_ptr swapchain_; std::unique_ptr target_; // only when renderScale_ != 1