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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ bool VulkanSurface::attach(ANativeWindow* window) {

ANativeWindow_acquire(window);
window_ = window;
windowExtent_ = {static_cast<uint32_t>(std::max(0, ANativeWindow_getWidth(window))),
static_cast<uint32_t>(std::max(0, ANativeWindow_getHeight(window)))};
staleChecks_ = 0;
if (!createSurface() || !recreateSwapchain()) {
LOGE("could not attach to the surface");
teardown();
Expand All @@ -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) {
Expand All @@ -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
Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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();
Expand All @@ -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_; }

Expand All @@ -72,14 +79,16 @@ class VulkanSurface {
bool recreateSwapchain();
bool createRenderTarget();
bool rebuildOrDrop();
bool surfaceExtentChanged() const;
bool rebuildWhenSurfaceReportsWindowSize();
void teardown();
void destroySwapchainAndSurface();

VulkanContext& ctx_;
VulkanFrameLoop& frameLoop_;
std::function<void()> beforeTeardown_;
ANativeWindow* window_ = nullptr;
VkExtent2D windowExtent_{}; // the size the swapchain must reach
uint32_t staleChecks_ = 0;
VkSurfaceKHR surface_ = VK_NULL_HANDLE;
std::unique_ptr<VulkanSwapchain> swapchain_;
std::unique_ptr<VulkanRenderTarget> target_; // only when renderScale_ != 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion packages/splatkit-engine/src/engine/SplatEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
23 changes: 23 additions & 0 deletions packages/splatkit-engine/tests/rendering/FrameOrderTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<Range> ranges;
uint32_t frames = 0;
ScreenTileStats tiles;
bool gpuLod = false;
bool rebuildPending = false;
uint32_t uploadedBudget = 0;
int heldShDegree = 0;

Expand Down Expand Up @@ -198,6 +200,27 @@ TEST(FrameOrder, OrbitAnimationDrawsUntilItFinishesThenIdles) {
EXPECT_EQ(observed->frames, beforeAnimation + 1);
}

TEST(FrameOrder, RendererWithAPendingSurfaceRebuildKeepsTheEngineDrawingUntilItSettles) {
auto renderer = std::make_unique<RecordingRenderer>(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) {
Expand Down
Loading