diff --git a/include/Inventor/SoRenderManager.h b/include/Inventor/SoRenderManager.h index 2622de9aa1..45c7f1e79b 100644 --- a/include/Inventor/SoRenderManager.h +++ b/include/Inventor/SoRenderManager.h @@ -120,6 +120,7 @@ class COIN_DLL_API SoRenderManager { uint64_t backendResourcePreparationNanoseconds = 0; uint64_t backendCommandExecutionNanoseconds = 0; uint64_t backendSelectionNanoseconds = 0; + uint64_t drawListRebuilds = 0; uint64_t pickPlanConstructionNanoseconds = 0; uint64_t pickBufferUpdateNanoseconds = 0; uint64_t pickQueryNanoseconds = 0; diff --git a/include/Inventor/rendering/SoRenderIR.h b/include/Inventor/rendering/SoRenderIR.h index af3aeb3fbe..4d92f98832 100644 --- a/include/Inventor/rendering/SoRenderIR.h +++ b/include/Inventor/rendering/SoRenderIR.h @@ -212,6 +212,8 @@ enum SoAlphaTestPolicy : uint8_t { // --- Render param flags (SoRenderParams::flags) --- static constexpr uint32_t SO_PARAM_CLEAR_WINDOW = 1u; static constexpr uint32_t SO_PARAM_CLEAR_DEPTH = 4u; //!< Clear depth buffer before rendering +//! The owning caller guarantees that this exact draw list is unchanged. +static constexpr uint32_t SO_PARAM_REUSE_DRAW_LIST = 8u; /*! \struct SoTextureData diff --git a/src/rendering/SoGLRenderBackend.cpp b/src/rendering/SoGLRenderBackend.cpp index cfb8a766ed..d1c640d579 100644 --- a/src/rendering/SoGLRenderBackend.cpp +++ b/src/rendering/SoGLRenderBackend.cpp @@ -1454,10 +1454,24 @@ SoGLRenderBackend::textureDescriptionMatches( entry.textureAnisotropic == texture.anisotropic; } +void +SoGLRenderBackend::prepareGeometryCache(const SoDrawList & drawlist, + const bool allowReuse) +{ + const size_t commandCount = static_cast(drawlist.getNumCommands()); + if (allowReuse && this->haveCacheGeneration && + this->cacheGeneration == drawlist.getGeneration() && + this->cachedCommandCount == commandCount) { + return; + } + this->updateGeometryCache(drawlist); +} + void SoGLRenderBackend::updateGeometryCache(const SoDrawList & drawlist) { const uint32_t generation = drawlist.getGeneration(); + const size_t commandCount = static_cast(drawlist.getNumCommands()); if (!this->haveCacheGeneration || this->cacheGeneration != generation) { for (CachedCommand & entry : this->gpuCache) { if (!entry.persistent) this->destroyCacheEntry(entry); @@ -1476,7 +1490,7 @@ SoGLRenderBackend::updateGeometryCache(const SoDrawList & drawlist) } this->cacheGeneration = generation; this->haveCacheGeneration = true; - this->cachedCommandCount = static_cast(drawlist.getNumCommands()); + this->cachedCommandCount = commandCount; for (int i = 0; i < drawlist.getNumCommands(); ++i) { const SoRenderCommand & command = drawlist.getCommand(i); @@ -2939,7 +2953,8 @@ SoGLRenderBackend::updatePickBuffer(const SoDrawList & drawlist, const SbVec2s size = params.viewport.getViewportSizePixels(); if (!this->ensurePickFramebuffer(size)) return FALSE; - this->updateGeometryCache(drawlist); + this->prepareGeometryCache( + drawlist, (params.flags & SO_PARAM_REUSE_DRAW_LIST) != 0); cc_glglue_glBindFramebuffer(this->glue, GL_FRAMEBUFFER, this->pickTarget.framebuffer); @@ -3409,7 +3424,8 @@ SoGLRenderBackend::renderSelection(const SoDrawList & drawlist, } ScopedGLState state(this->glue); - this->updateGeometryCache(drawlist); + this->prepareGeometryCache( + drawlist, (params.flags & SO_PARAM_REUSE_DRAW_LIST) != 0); applyViewport(params); glEnable(GL_BLEND); cc_glglue_glBlendFuncSeparate(this->glue, GL_SRC_ALPHA, @@ -3504,7 +3520,8 @@ SoGLRenderBackend::render(const SoDrawList & drawlist, } const BackendPhaseClock::time_point resourceStart = measurePhases ? BackendPhaseClock::now() : BackendPhaseClock::time_point(); - this->updateGeometryCache(drawlist); + this->prepareGeometryCache( + drawlist, (params.flags & SO_PARAM_REUSE_DRAW_LIST) != 0); if (measurePhases) { this->phaseStatistics.resourcePreparationNanoseconds = elapsedNanoseconds(resourceStart); diff --git a/src/rendering/SoGLRenderBackend.h b/src/rendering/SoGLRenderBackend.h index 2f766945c5..6bc5a75b83 100644 --- a/src/rendering/SoGLRenderBackend.h +++ b/src/rendering/SoGLRenderBackend.h @@ -341,6 +341,7 @@ class SoGLRenderBackend : public SoRenderBackend { bool selection); void beginFrame(const SoRenderParams & params); void invalidateCache(); + void prepareGeometryCache(const SoDrawList & drawlist, bool allowReuse); void updateGeometryCache(const SoDrawList & drawlist); void updateLineDistances(CachedCommand & entry, const SoRenderCommand & command, diff --git a/src/rendering/SoRenderManager.cpp b/src/rendering/SoRenderManager.cpp index 474ac3d30d..4bb795f31c 100644 --- a/src/rendering/SoRenderManager.cpp +++ b/src/rendering/SoRenderManager.cpp @@ -158,6 +158,9 @@ retainedRenderParams(const SoRenderManagerP * manager) params.devicePixelRatio = manager->devicePixelRatio; params.clearColor = manager->backgroundcolor; params.clearDepth = 1.0f; + if (manager->drawListValid && !manager->drawListDirty) { + params.flags |= SO_PARAM_REUSE_DRAW_LIST; + } return params; } @@ -406,6 +409,12 @@ SoRenderManager::SoRenderManager(void) PRIVATE(this)->renderPhaseStatistics = RenderPhaseStatistics(); PRIVATE(this)->renderBackendContextId = 0; PRIVATE(this)->drawListCallbackScope = FALSE; + PRIVATE(this)->drawListValid = FALSE; + PRIVATE(this)->drawListDirty = TRUE; + PRIVATE(this)->drawListSceneRevision = 0; + PRIVATE(this)->drawListCameraRevision = 0; + PRIVATE(this)->drawListBackgroundRevision = 0; + PRIVATE(this)->drawListForegroundRevision = 0; PRIVATE(this)->pickTargetDirty = TRUE; PRIVATE(this)->pickTargetGeneration = 0; PRIVATE(this)->viewport = SbViewportRegion(SbVec2s(400, 400)); @@ -512,6 +521,7 @@ SoRenderManager::setSceneGraph(SoNode * const sceneroot) PRIVATE(this)->cameraInSceneGraph = FALSE; PRIVATE(this)->scene = sceneroot; + PRIVATE(this)->drawListDirty = TRUE; if (PRIVATE(this)->scene) { PRIVATE(this)->scene->ref(); @@ -544,6 +554,7 @@ SoRenderManager::setCamera(SoCamera * camera) PRIVATE(this)->camera->unref(); } PRIVATE(this)->camera = camera; + PRIVATE(this)->drawListDirty = TRUE; if (camera) camera->ref(); } @@ -559,7 +570,9 @@ SoRenderManager::getCamera(void) const void SoRenderManager::setCameraInSceneGraph(SbBool inSceneGraph) { + if (PRIVATE(this)->cameraInSceneGraph == inSceneGraph) return; PRIVATE(this)->cameraInSceneGraph = inSceneGraph; + PRIVATE(this)->drawListDirty = TRUE; } SbBool @@ -582,7 +595,9 @@ SoRenderManager::nodesensorCB(void * data, SoSensor * /* sensor */) SoDebugError::postInfo("SoRenderManager::nodesensorCB", "detected change in scene graph"); #endif // debug - ((SoRenderManager *)data)->scheduleRedraw(); + SoRenderManager * manager = static_cast(data); + PRIVATE(manager)->drawListDirty = TRUE; + manager->scheduleRedraw(); } /*! @@ -1012,9 +1027,8 @@ SoRenderManager::renderDrawListPipeline(const SbBool clearwindow, phaseStatistics.backendResourcePreparationNanoseconds = 0; phaseStatistics.backendCommandExecutionNanoseconds = 0; phaseStatistics.backendSelectionNanoseconds = 0; + phaseStatistics.drawListRebuilds = 0; const SbBool measurePhases = PRIVATE(this)->renderPhaseTimingEnabled; - const RenderPhaseClock::time_point drawListStart = measurePhases - ? RenderPhaseClock::now() : RenderPhaseClock::time_point(); const SoRenderManager::RenderMode renderMode = PRIVATE(this)->rendermode; const SoRenderManager::StereoMode stereoMode = PRIVATE(this)->stereomode; const SbBool hasSuperimpositions = @@ -1161,7 +1175,29 @@ SoRenderManager::renderDrawListPipeline(const SbBool clearwindow, SoIRRenderAction * action = PRIVATE(this)->irAction; action->setConstructionTimingEnabled(measurePhases); SoState * state = action->getState(); - action->beginFrame(); + const uint64_t sceneRevision = PRIVATE(this)->scene + ? static_cast(PRIVATE(this)->scene->getNodeId()) : 0; + const uint64_t cameraRevision = PRIVATE(this)->camera + ? static_cast(PRIVATE(this)->camera->getNodeId()) : 0; + const uint64_t backgroundRevision = PRIVATE(this)->renderLayerBackgroundRoot + ? static_cast( + PRIVATE(this)->renderLayerBackgroundRoot->getNodeId()) : 0; + const uint64_t foregroundRevision = PRIVATE(this)->renderLayerForegroundRoot + ? static_cast( + PRIVATE(this)->renderLayerForegroundRoot->getNodeId()) : 0; + const SbBool rebuildDrawList = + !PRIVATE(this)->drawListValid || PRIVATE(this)->drawListDirty || + !PRIVATE(this)->afterMainSceneCallbacks.empty() || + PRIVATE(this)->drawListSceneRevision != sceneRevision || + PRIVATE(this)->drawListCameraRevision != cameraRevision || + PRIVATE(this)->drawListBackgroundRevision != backgroundRevision || + PRIVATE(this)->drawListForegroundRevision != foregroundRevision; + phaseStatistics.drawListRebuilds = rebuildDrawList ? 1 : 0; + const RenderPhaseClock::time_point drawListStart = + measurePhases && rebuildDrawList + ? RenderPhaseClock::now() : RenderPhaseClock::time_point(); + + if (rebuildDrawList) action->beginFrame(); const auto applyTraversalState = [this, renderMode](SoState * traversalState) { SoNode * stateNode = PRIVATE(this)->dummynode; @@ -1199,7 +1235,7 @@ SoRenderManager::renderDrawListPipeline(const SbBool clearwindow, } }; - if (PRIVATE(this)->renderLayerBackgroundRoot) { + if (rebuildDrawList && PRIVATE(this)->renderLayerBackgroundRoot) { SoIRRenderStageScope backgroundScope(*action, SoRenderStage::Background); state->push(); SoDevicePixelRatioElement::set(state, @@ -1213,7 +1249,7 @@ SoRenderManager::renderDrawListPipeline(const SbBool clearwindow, } } - if (PRIVATE(this)->scene) { + if (rebuildDrawList && PRIVATE(this)->scene) { state->push(); SoDevicePixelRatioElement::set(state, PRIVATE(this)->dummynode, @@ -1227,7 +1263,7 @@ SoRenderManager::renderDrawListPipeline(const SbBool clearwindow, state->pop(); } - { + if (rebuildDrawList) { SoIRRenderStageScope afterMainScope(*action, SoRenderStage::AfterMain); state->push(); SoDevicePixelRatioElement::set(state, @@ -1238,7 +1274,7 @@ SoRenderManager::renderDrawListPipeline(const SbBool clearwindow, state->pop(); } - if (PRIVATE(this)->renderLayerForegroundRoot) { + if (rebuildDrawList && PRIVATE(this)->renderLayerForegroundRoot) { SoIRRenderStageScope foregroundScope(*action, SoRenderStage::Foreground); state->push(); SoDevicePixelRatioElement::set(state, @@ -1249,8 +1285,8 @@ SoRenderManager::renderDrawListPipeline(const SbBool clearwindow, state->pop(); } - if (action->hasUnsupportedRendering()) { - const char * reason = action->getUnsupportedReason(); + if (rebuildDrawList && PRIVATE(this)->irAction->hasUnsupportedRendering()) { + const char * reason = PRIVATE(this)->irAction->getUnsupportedReason(); #if COIN_BUILD_LEGACY_GL_RENDERER if (currentContextSupportsLegacyRendering()) { SoDebugError::postWarning( @@ -1282,9 +1318,18 @@ SoRenderManager::renderDrawListPipeline(const SbBool clearwindow, return; } + if (rebuildDrawList) { + PRIVATE(this)->drawListValid = TRUE; + PRIVATE(this)->drawListDirty = FALSE; + PRIVATE(this)->drawListSceneRevision = sceneRevision; + PRIVATE(this)->drawListCameraRevision = cameraRevision; + PRIVATE(this)->drawListBackgroundRevision = backgroundRevision; + PRIVATE(this)->drawListForegroundRevision = foregroundRevision; + } + SoDrawList & drawlist = PRIVATE(this)->irAction->getMutableDrawList(); - if (measurePhases) { + if (measurePhases && rebuildDrawList) { const SoIRRenderAction::ConstructionStatistics & construction = action->getConstructionStatistics(); phaseStatistics.drawListConstructionNanoseconds = @@ -1320,7 +1365,8 @@ SoRenderManager::renderDrawListPipeline(const SbBool clearwindow, params.clearColor = PRIVATE(this)->backgroundcolor; params.clearDepth = 1.0f; params.flags = (clearwindow ? SO_PARAM_CLEAR_WINDOW : 0u) | - (clearzbuffer ? SO_PARAM_CLEAR_DEPTH : 0u); + (clearzbuffer ? SO_PARAM_CLEAR_DEPTH : 0u) | + (!rebuildDrawList ? SO_PARAM_REUSE_DRAW_LIST : 0u); SoRenderPlanner planner; SoRenderPlan plan; const RenderPhaseClock::time_point planStart = measurePhases @@ -1350,8 +1396,10 @@ SoRenderManager::renderDrawListPipeline(const SbBool clearwindow, phaseStatistics.backendSelectionNanoseconds = backendPhases.selectionNanoseconds; } - PRIVATE(this)->pickTargetDirty = TRUE; - PRIVATE(this)->pickTargetGeneration = 0; + if (rebuildDrawList) { + PRIVATE(this)->pickTargetDirty = TRUE; + PRIVATE(this)->pickTargetGeneration = 0; + } if (SoRenderManager::isRealTimeUpdateEnabled()) { SoField * realtime = SoDB::getGlobalField("realTime"); @@ -1909,6 +1957,7 @@ SoRenderManager::setWindowSize(const SbVec2s & newsize) SbViewportRegion region = PRIVATE(this)->viewport; region.setWindowSize(newsize[0], newsize[1]); PRIVATE(this)->viewport = region; + PRIVATE(this)->drawListDirty = TRUE; #if COIN_BUILD_LEGACY_GL_RENDERER PRIVATE(this)->glaction->setViewportRegion(region); #endif @@ -1930,6 +1979,7 @@ SoRenderManager::setDevicePixelRatio(float dpr) { if (PRIVATE(this)->devicePixelRatio == dpr) return; PRIVATE(this)->devicePixelRatio = dpr; + PRIVATE(this)->drawListDirty = TRUE; this->scheduleRedraw(); } @@ -1955,6 +2005,7 @@ SoRenderManager::setSize(const SbVec2s & newsize) SbVec2s origin = region.getViewportOriginPixels(); region.setViewportPixels(origin, newsize); PRIVATE(this)->viewport = region; + PRIVATE(this)->drawListDirty = TRUE; #if COIN_BUILD_LEGACY_GL_RENDERER PRIVATE(this)->glaction->setViewportRegion(region); #endif @@ -1987,6 +2038,7 @@ SoRenderManager::setOrigin(const SbVec2s & newOrigin) SbVec2s size = region.getViewportSizePixels(); region.setViewportPixels(newOrigin, size); PRIVATE(this)->viewport = region; + PRIVATE(this)->drawListDirty = TRUE; #if COIN_BUILD_LEGACY_GL_RENDERER PRIVATE(this)->glaction->setViewportRegion(region); #endif @@ -2015,6 +2067,7 @@ void SoRenderManager::setViewportRegion(const SbViewportRegion & newregion) { PRIVATE(this)->viewport = newregion; + PRIVATE(this)->drawListDirty = TRUE; #if COIN_BUILD_LEGACY_GL_RENDERER PRIVATE(this)->glaction->setViewportRegion(newregion); #endif @@ -2202,7 +2255,9 @@ SoRenderManager::isAutoRedraw(void) const void SoRenderManager::setRenderMode(const RenderMode mode) { + if (PRIVATE(this)->rendermode == mode) return; PRIVATE(this)->rendermode = mode; + PRIVATE(this)->drawListDirty = TRUE; this->scheduleRedraw(); PRIVATE(this)->dummynode->touch(); } @@ -2221,6 +2276,7 @@ SoRenderManager::setLightingMode(const LightingMode mode) { if (PRIVATE(this)->lightingmode == mode) return; PRIVATE(this)->lightingmode = mode; + PRIVATE(this)->drawListDirty = TRUE; #if COIN_BUILD_LEGACY_GL_RENDERER if (PRIVATE(this)->glaction) { PRIVATE(this)->glaction->invalidateState(); @@ -2427,6 +2483,7 @@ SoRenderManager::setRenderLayerRoot(RenderLayer layer, SoNode * root) if (*slot) (*slot)->unref(); *slot = root; + PRIVATE(this)->drawListDirty = TRUE; if (root) { root->ref(); if (!*sensorSlot) { @@ -2661,12 +2718,14 @@ SoRenderManager::invalidateDrawList(void) void SoRenderManager::invalidateScene(void) { + PRIVATE(this)->drawListDirty = TRUE; this->scheduleRedraw(); } void SoRenderManager::invalidateForeground(void) { + PRIVATE(this)->drawListDirty = TRUE; this->scheduleRedraw(); } @@ -2720,7 +2779,9 @@ SoRenderManager::getNearPlaneValue(void) const void SoRenderManager::setTexturesEnabled(const SbBool onoff) { + if (PRIVATE(this)->texturesenabled == onoff) return; PRIVATE(this)->texturesenabled = onoff; + PRIVATE(this)->drawListDirty = TRUE; } /*! @@ -2907,6 +2968,7 @@ SoRenderManager::addAfterMainSceneCallback(SoRenderManagerStageCB * cb, void * d { PRIVATE(this)->afterMainSceneCallbacks.push_back( SoRenderManagerP::StageCBTouple(cb, data)); + PRIVATE(this)->drawListDirty = TRUE; } void @@ -2920,6 +2982,7 @@ SoRenderManager::removeAfterMainSceneCallback(SoRenderManagerStageCB * cb, void "Tried to remove an after-main-scene callback which doesn't exist"); if (findit != PRIVATE(this)->afterMainSceneCallbacks.end()) { PRIVATE(this)->afterMainSceneCallbacks.erase(findit); + PRIVATE(this)->drawListDirty = TRUE; } } diff --git a/src/rendering/SoRenderManagerP.h b/src/rendering/SoRenderManagerP.h index f5920f5a29..fd0b9fcc1d 100644 --- a/src/rendering/SoRenderManagerP.h +++ b/src/rendering/SoRenderManagerP.h @@ -131,6 +131,12 @@ class SoRenderManagerP { SoRenderManager::RenderPhaseStatistics renderPhaseStatistics; uint32_t renderBackendContextId; SbBool drawListCallbackScope; + SbBool drawListValid; + SbBool drawListDirty; + uint64_t drawListSceneRevision; + uint64_t drawListCameraRevision; + uint64_t drawListBackgroundRevision; + uint64_t drawListForegroundRevision; SbBool pickTargetDirty; uint32_t pickTargetGeneration; diff --git a/testsuite/CoinRenderGLBenchmarks.cpp b/testsuite/CoinRenderGLBenchmarks.cpp index 4190fce77a..ecfbf036d6 100644 --- a/testsuite/CoinRenderGLBenchmarks.cpp +++ b/testsuite/CoinRenderGLBenchmarks.cpp @@ -40,6 +40,7 @@ using Clock = std::chrono::steady_clock; struct Options { bool smoke = false; int samples = 0; + int rebuildOnly = 0; std::string output; }; @@ -47,6 +48,7 @@ struct Measurement { std::string workload; std::string renderer; std::string profile; + std::string executionMode; int semanticDraws = 0; int samples = 0; double cpuMedianMs = 0.0; @@ -65,6 +67,7 @@ struct Measurement { double backendResourcePreparationMedianMs = 0.0; double backendCommandExecutionMedianMs = 0.0; double backendSelectionMedianMs = 0.0; + uint64_t drawListRebuilds = 0; double coldPickMs = 0.0; double coldPickBufferUpdateMs = 0.0; double coldPickTargetPreparationMs = 0.0; @@ -128,7 +131,8 @@ bool runVariant(GLTestProfile profile, SoRenderManager::RenderPipeline pipeline, const std::string & renderer, WorkloadKind workload, int drawCount, int samples, Measurement & result, - std::string & unavailable) + std::string & unavailable, + bool forceDrawListRebuild = false) { GLTestContextConfig config; config.profile = profile; @@ -199,6 +203,7 @@ bool runVariant(GLTestProfile profile, glGenQueries(1, &query); for (int sample = 0; sample < samples; ++sample) { context.bindFramebuffer(); + if (forceDrawListRebuild) manager.invalidateDrawList(); const Clock::time_point totalStart = Clock::now(); glBeginQuery(GL_TIME_ELAPSED, query); const Clock::time_point cpuStart = Clock::now(); @@ -226,6 +231,7 @@ bool runVariant(GLTestProfile profile, renderPhases.backendCommandExecutionNanoseconds / 1000000.0); backendSelection.push_back( renderPhases.backendSelectionNanoseconds / 1000000.0); + result.drawListRebuilds += renderPhases.drawListRebuilds; glEndQuery(GL_TIME_ELAPSED); GLuint64 nanoseconds = 0; glGetQueryObjectui64v(query, GL_QUERY_RESULT, &nanoseconds); @@ -357,6 +363,9 @@ bool runVariant(GLTestProfile profile, result.workload = workloadName(workload); result.renderer = renderer; result.profile = profile == GLTestProfile::Core ? "core" : "compatibility"; + result.executionMode = forceDrawListRebuild ? "forced_rebuild" : + (pipeline == SoRenderManager::RenderPipeline::LEGACY_GL ? + "per_frame_traversal" : "steady_state"); result.semanticDraws = drawCount; result.samples = samples; result.cpuMedianMs = percentile(cpu, 0.5); @@ -401,6 +410,11 @@ bool runVariant(GLTestProfile profile, } } result.pixelChecksum = pixelChecksum; + if (forceDrawListRebuild && + result.drawListRebuilds != static_cast(samples)) { + unavailable = "forced rebuild did not rebuild every measured frame"; + return false; + } return true; } @@ -411,10 +425,12 @@ Options parseOptions(int argc, char ** argv) const std::string arg(argv[i]); if (arg == "--smoke") options.smoke = true; else if (arg == "--samples" && i + 1 < argc) options.samples = std::atoi(argv[++i]); + else if (arg == "--rebuild-only" && i + 1 < argc) + options.rebuildOnly = std::atoi(argv[++i]); else if (arg == "--output" && i + 1 < argc) options.output = argv[++i]; else { std::cerr << "Usage: CoinRenderGLBenchmarks [--smoke] [--samples N] " - "[--output FILE]\n"; + "[--rebuild-only N] [--output FILE]\n"; std::exit(2); } } @@ -427,7 +443,7 @@ std::string toJson(const std::vector & results, { std::ostringstream out; out << std::fixed << std::setprecision(6); - out << "{\n \"schema_version\": 4,\n \"mode\": \"" + out << "{\n \"schema_version\": 5,\n \"mode\": \"" << (options.smoke ? "smoke" : "benchmark") << "\",\n \"time_unit\": \"ms\",\n \"benchmarks\": [\n"; for (size_t i = 0; i < results.size(); ++i) { @@ -435,6 +451,7 @@ std::string toJson(const std::vector & results, out << " {\"workload\": \"" << r.workload << "\", \"renderer\": \"" << r.renderer << "\", \"profile\": \"" << r.profile + << "\", \"execution_mode\": \"" << r.executionMode << "\", \"semantic_draws\": " << r.semanticDraws << ", \"samples\": " << r.samples << ", \"cpu_render_median_ms\": " << r.cpuMedianMs @@ -463,6 +480,7 @@ std::string toJson(const std::vector & results, << r.backendCommandExecutionMedianMs << ", \"backend_selection_median_ms\": " << r.backendSelectionMedianMs + << ", \"drawlist_rebuilds\": " << r.drawListRebuilds << ", \"cold_pick_ms\": " << r.coldPickMs << ", \"cold_pick_buffer_update_ms\": " << r.coldPickBufferUpdateMs @@ -520,6 +538,49 @@ int main(int argc, char ** argv) }; std::vector results; std::vector unavailable; + if (options.rebuildOnly > 0) { + const WorkloadKind workload = WorkloadKind::FeatureRich; + const int drawCount = options.rebuildOnly; + const std::string workloadLabel = "feature_rich_rebuild_" + + std::to_string(drawCount); + const auto run = [&](GLTestProfile profile, + SoRenderManager::RenderPipeline pipeline, + const char * renderer, + bool forceRebuild) { + Measurement measurement; + std::string reason; + if (runVariant(profile, pipeline, renderer, workload, drawCount, samples, + measurement, reason, forceRebuild)) { + measurement.workload = workloadLabel; + results.push_back(measurement); + } + else { + unavailable.push_back(workloadLabel + ":" + renderer + ": " + reason); + } + }; +#if COIN_HAVE_LEGACY_GL_RENDERER + run(GLTestProfile::Compatibility, + SoRenderManager::RenderPipeline::LEGACY_GL, "LegacyGL", false); +#endif + run(GLTestProfile::Compatibility, + SoRenderManager::RenderPipeline::DRAW_LIST, "DrawList", false); + run(GLTestProfile::Compatibility, + SoRenderManager::RenderPipeline::DRAW_LIST, "DrawList", true); + run(GLTestProfile::Core, + SoRenderManager::RenderPipeline::DRAW_LIST, "DrawList", false); + run(GLTestProfile::Core, + SoRenderManager::RenderPipeline::DRAW_LIST, "DrawList", true); + + const std::string document = toJson(results, unavailable, options); + if (options.output.empty()) std::cout << document; + else { + std::ofstream output(options.output.c_str()); + if (!output) return 1; + output << document; + } + SoDB::finish(); + return results.empty() ? 77 : 0; + } for (size_t i = 0; i < sizeof(workloads) / sizeof(workloads[0]); ++i) { #if COIN_HAVE_LEGACY_GL_RENDERER Measurement legacy; diff --git a/testsuite/DrawListManagerTest.cpp b/testsuite/DrawListManagerTest.cpp index 769017041e..7cd29559c0 100644 --- a/testsuite/DrawListManagerTest.cpp +++ b/testsuite/DrawListManagerTest.cpp @@ -299,6 +299,23 @@ runTest() std::cerr << "FAIL: retained render phases were not measured" << std::endl; result = 1; } + manager.render(TRUE, TRUE); + const SoRenderManager::RenderPhaseStatistics reusedRenderPhases = + manager.getRenderPhaseStatistics(); + if (reusedRenderPhases.drawListRebuilds != 0 || + reusedRenderPhases.drawListConstructionNanoseconds != 0) { + std::cerr << "FAIL: unchanged retained frame was rebuilt" << std::endl; + result = 1; + } + cubeRoot->touch(); + manager.render(TRUE, TRUE); + const SoRenderManager::RenderPhaseStatistics changedRenderPhases = + manager.getRenderPhaseStatistics(); + if (changedRenderPhases.drawListRebuilds != 1 || + changedRenderPhases.drawListConstructionNanoseconds == 0) { + std::cerr << "FAIL: changed retained frame was not rebuilt" << std::endl; + result = 1; + } if (countNonBlack(context) == 0) { std::cerr << "FAIL: transformed-camera manager render produced no pixels" << std::endl; result = 1; diff --git a/testsuite/RENDER_BENCHMARKS.md b/testsuite/RENDER_BENCHMARKS.md index a78d9ce1da..44c08a7881 100644 --- a/testsuite/RENDER_BENCHMARKS.md +++ b/testsuite/RENDER_BENCHMARKS.md @@ -17,6 +17,8 @@ p95 timings, workload sizes, sample counts, and sanity checksums: build-bench/bin/CoinRenderBenchmarks --output results.json build-bench/bin/CoinRenderBenchmarks --samples 50 --output results.json build-bench/bin/CoinRenderGLBenchmarks --samples 50 --output gl-results.json +build-bench/bin/CoinRenderGLBenchmarks --rebuild-only 5000 \ + --samples 10 --output rebuild-5000.json ``` ## Viewing generated workloads @@ -85,7 +87,8 @@ Picking is split into one-time cold target creation, target refresh after a changed frame, and warm repeated-hover latency. The GL benchmark explicitly enables renderer phase timing. JSON schema version -4 separates draw-list construction into primitive generation, geometry packing, +5 identifies each result as `per_frame_traversal`, `steady_state`, or +`forced_rebuild`. It separates draw-list construction into primitive generation, geometry packing, and command emission, and also reports render-plan construction and backend submission. Command emission includes command state capture and path retention. Work outside these nested shape phases remains visible as the difference from @@ -99,6 +102,20 @@ not affect ordinary rendering. A zero-valued phase means it did not run; for example, a warm hover pick normally reuses its existing pick buffer, and a frame without selected objects performs no selection-overlay work. +The timed render samples represent steady-state frames. The retained manager +reuses its draw list until a scene, camera, layer, viewport-dependent traversal +setting, or explicit `invalidateDrawList()` call invalidates it. The +`drawlist_rebuilds` field makes that distinction visible in benchmark output; +it is normally zero after warmup. + +Use `--rebuild-only N` to isolate the feature-rich scene at `N` semantic +draws. The focused run compares LegacyGL's normal per-frame traversal with +steady-state and forced-rebuild DrawList rendering in compatibility and core +profiles. Before each forced-rebuild sample, the benchmark invalidates the +retained draw list; `drawlist_rebuilds` must therefore equal the sample count. +This mode reuses the feature-rich workload rather than maintaining a separate +benchmark scene. + The deterministic workloads currently cover traversal/IR construction, render plan construction (including transparent sorting and depth segments), retained pick-table construction and resolution, selection churn, and repeated frame