From 6fef45267be883fe74583d6a9dcdd14f88a29037 Mon Sep 17 00:00:00 2001 From: Lars Viklund Date: Wed, 5 Aug 2026 00:28:11 +1200 Subject: [PATCH] fix: accurate frame elision and draw performance We always resolve the command buffer hash before drawing any layers so there is no need for mid-draw bail logic. Replaced MurmurHash64A with xxHash's way faster XXH3 for command buffer hashing. Only consider textures used for layer geometry when determining frame completeness for elision. Command buffer command structs are now packed on 1-byte alignment and the command key has shrunk from int to u8. Getting rid of padding isn't crucial as subsequent identical frames should both touch the padding bytes in similar ways, but it's nice. --- engine/render/r_main.cpp | 129 ++++++++++++++++++--------------------- engine/render/r_main.h | 4 +- vcpkg.json | 1 + 3 files changed, 62 insertions(+), 72 deletions(-) diff --git a/engine/render/r_main.cpp b/engine/render/r_main.cpp index 1d46715..c3acc54 100644 --- a/engine/render/r_main.cpp +++ b/engine/render/r_main.cpp @@ -25,6 +25,8 @@ #include #include +#include + static uint64_t MurmurHash64A(void const* data, int len, uint64_t seed); // ======= @@ -125,8 +127,9 @@ Mat4 OrthoMatrix(double left, double right, double bottom, double top, double ne // Layer queue class // ================= +#pragma pack(push, r_layerCmd, 1) struct r_layerCmd_s { - enum Command { + enum Command : uint8_t { VIEWPORT, BLEND, BIND, @@ -165,6 +168,7 @@ struct r_layerCmdQuad_s { int stackLayer, maskLayer; } quad; }; +#pragma pack(pop, r_layerCmd) r_layer_c::r_layer_c(r_renderer_c* renderer, int layer, int subLayer) : renderer(renderer), layer(layer), subLayer(subLayer) @@ -418,6 +422,7 @@ struct RenderStrategy { virtual void ProcessCommand(r_layerCmd_s* cmd) = 0; virtual void Flush() = 0; virtual void SetShowStats(bool showStats) { showStats_ = showStats; } + virtual bool UsedIncompleteTextures() const { return false; } protected: bool showStats_{}; @@ -572,6 +577,8 @@ struct AdjacentMergeStrategy : RenderStrategy { } } + bool UsedIncompleteTextures() const override { return usedIncompleteTextures; }; + private: void Dispatch() { glBindBuffer(GL_ARRAY_BUFFER, vbo_); @@ -623,7 +630,10 @@ struct AdjacentMergeStrategy : RenderStrategy { auto tex = textures[i]; tex->Bind(); if (showStats_) { - ImGui::Text("New tex %d (%s)", tex->texId, tex->fileName.c_str()); + ImGui::Text("New tex %d (%s) %d", tex->texId, tex->fileName.c_str(), tex->status.load()); + } + if (!usedIncompleteTextures && tex->status != r_tex_c::Status::DONE) { + usedIncompleteTextures = true; } } else { @@ -655,7 +665,7 @@ struct AdjacentMergeStrategy : RenderStrategy { struct TexturedBatch { explicit TexturedBatch(GLuint prog) : batch(prog) { - textures.reserve(1ull << 20); + textures.reserve(128); } BatchKey key{}; @@ -673,9 +683,11 @@ struct AdjacentMergeStrategy : RenderStrategy { size_t totalVertexCount_ = 0; size_t batchIndex = 0; + + bool usedIncompleteTextures = false; }; -void r_layer_c::Render() +bool r_layer_c::Render() { int const optLevel = renderer->r_layerOptimize->intVal; bool const shuffle = renderer->r_layerShuffle->intVal == 1; @@ -713,6 +725,8 @@ void r_layer_c::Render() if (renderer->glPopGroupMarkerEXT) { renderer->glPopGroupMarkerEXT(); } + + return strat->UsedIncompleteTextures(); } void r_layer_c::Discard() @@ -1150,12 +1164,6 @@ void r_renderer_c::Shutdown() void r_renderer_c::PumpShaders() { texMan->ProcessPendingTextureUploads(); - for (size_t idx = 0; idx < numShader; ++idx) - if (auto* sh = shaderList[idx]) - if (auto tex = sh->tex; tex && tex->status != r_tex_c::DONE) { - inhibitElision = true; - break; - } } void r_renderer_c::BeginFrame() @@ -1344,13 +1352,15 @@ void r_renderer_c::EndFrame() ImGui::Text("Total dense footprint: %sB", BinaryUnitPrefix(totalDenseFootprint).c_str()); size_t totalCmd{}; - if (ImGui::BeginTable("Layer stats", 7, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit)) { + if (ImGui::BeginTable("Layer stats", 8, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit)) { ImGui::TableSetupColumn("Index"); ImGui::TableSetupColumn("Layer"); ImGui::TableSetupColumn("Sublayer"); ImGui::TableSetupColumn("Command count"); ImGui::TableSetupColumn("Dense"); ImGui::TableSetupColumn("Debug"); + ImGui::TableSetupColumn("XXH3-64"); + ImGui::TableSetupColumn("MH64A"); ImGui::TableHeadersRow(); for (int l = 0; l < numLayer; ++l) { auto layer = layerSort[l]; @@ -1372,6 +1382,22 @@ void r_renderer_c::EndFrame() if (ImGui::Button("Debug")) { layerBreak = { layer->layer, layer->subLayer }; } + + std::chrono::high_resolution_clock::time_point tic; + std::chrono::microseconds dt; + + ImGui::TableNextColumn(); + tic = std::chrono::high_resolution_clock::now(); + volatile auto xxh_hash = XXH3_64bits(layer->cmdStorage.data(), layer->cmdCursor); + dt = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - tic); + ImGui::Text("%d µs", dt.count()); + + ImGui::TableNextColumn(); + tic = std::chrono::high_resolution_clock::now(); + volatile auto mh_hash = MurmurHash64A(layer->cmdStorage.data(), (int)layer->cmdCursor, 0ull); + dt = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - tic); + ImGui::Text("%d µs", dt.count()); + ImGui::PopID(); ImGui::PopID(); } @@ -1383,83 +1409,46 @@ void r_renderer_c::EndFrame() if (inhibitElision || elideFrames != !!r_elideFrames->intVal) { elideFrames = !!r_elideFrames->intVal; - lastFrameHash.clear(); + lastFrameHash = 0; } - std::future>> elidedFrameHashFut; + auto tic = std::chrono::high_resolution_clock::now(); + + uint64_t commandDigest = 0; if (elideFrames) { - elidedFrameHashFut = std::async([&]() -> std::optional> { - std::vector commandDigest; - - for (auto lIdx = 0; lIdx < numLayer; ++lIdx) { - auto layer = layerSort[lIdx]; - uint64_t subHash = MurmurHash64A(layer->cmdStorage.data(), (int)layer->cmdCursor, 0ull); - uint8_t const* p = (uint8_t const*)&subHash; - commandDigest.insert(commandDigest.end(), p, p + sizeof(subHash)); - } + std::shared_ptr hashState(XXH3_createState(), XXH3_freeState); + XXH3_64bits_reset(hashState.get()); - return commandDigest; - }); - } - else { - std::promise>> p; - elidedFrameHashFut = p.get_future(); - p.set_value({}); - } + for (auto lIdx = 0; lIdx < numLayer; ++lIdx) { + auto layer = layerSort[lIdx]; + uint64_t subHash = XXH3_64bits(layer->cmdStorage.data(), (int)layer->cmdCursor); + XXH3_64bits_update(hashState.get(), &subHash, sizeof(subHash)); + } - elidedFrameHashFut.wait(); + commandDigest = XXH3_64bits_digest(hashState.get()); + } ++totalFrames; - bool decideDraw = false; - bool elideDraw = false; + const bool elideDraw = lastFrameHash != 0 && lastFrameHash == commandDigest; + if (!elideDraw) { glBindFramebuffer(GL_FRAMEBUFFER, GetDrawRenderTarget().framebuffer); glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); - int l{}; - for (l = 0; l < numLayer; l++) { - if (!decideDraw && elidedFrameHashFut.wait_for(std::chrono::milliseconds(0)) == std::future_status::ready) { - decideDraw = true; - auto commandDigest = elidedFrameHashFut.get(); - if (commandDigest) { - if (*commandDigest == lastFrameHash) { - elideDraw = true; - break; - } - else { - lastFrameHash = *commandDigest; - } - } - else { - lastFrameHash.clear(); - } - } + for (int l = 0; l < numLayer; l++) { auto& layer = layerSort[l]; if (layerBreak && layerBreak->first == layer->layer && layerBreak->second == layer->subLayer) { #ifdef _WIN32 DebugBreak(); #endif } - layer->Render(); - } - if (!elideDraw) { - presentRtt = 1 - presentRtt; - ++drawnFrames; - } - } - - if (!decideDraw) { - if (auto commandDigest = elidedFrameHashFut.get()) { - lastFrameHash = *commandDigest; - } - else { - lastFrameHash.clear(); + inhibitElision = layer->Render() || inhibitElision; } + presentRtt = 1 - presentRtt; + ++drawnFrames; } - if (inhibitElision) { - // If we explicitly inhibited elision due to things like incomplete textures, make sure that the next frame is drawn. - lastFrameHash.clear(); - } + // If we explicitly inhibited elision due to things like incomplete textures, make sure that the next frame is drawn. + lastFrameHash = inhibitElision ? 0 : commandDigest; for (int l = 0; l < numLayer; ++l) { layerSort[l]->Discard(); @@ -1500,7 +1489,7 @@ void r_renderer_c::EndFrame() if (ImGui::Begin("Hash")) { char* b64{}; size_t b64Len{}; - Base64UrlEncode((char const*)lastFrameHash.data(), lastFrameHash.size(), &b64, &b64Len); + Base64UrlEncode((char const*)&lastFrameHash, sizeof(lastFrameHash), &b64, &b64Len); ImGui::Text("%s", b64); free(b64); } diff --git a/engine/render/r_main.h b/engine/render/r_main.h index f7072fb..808eeff 100644 --- a/engine/render/r_main.h +++ b/engine/render/r_main.h @@ -46,7 +46,7 @@ class r_layer_c { void Bind(r_tex_c* tex); void Color(col4_t col); void Quad(float s0, float t0, float x0, float y0, float s1, float t1, float x1, float y1, float s2, float t2, float x2, float y2, float s3, float t3, float x3, float y3, int stackLayer = 0, int maskLayer = -1); - void Render(); + bool Render(); void Discard(); struct CmdHandle { @@ -180,7 +180,7 @@ class r_renderer_c: public r_IRenderer, public conCmdHandler_c { RenderTarget rttMain[2]; int presentRtt = 0; - std::vector lastFrameHash{}; + uint64_t lastFrameHash{}; uint64_t totalFrames{}; uint64_t drawnFrames{}; diff --git a/vcpkg.json b/vcpkg.json index 2e4efdd..5bdc5d3 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -13,6 +13,7 @@ "pkgconf", "re2", "sol2", + "xxhash", "zstd", "zlib" ]