diff --git a/CMakeLists.txt b/CMakeLists.txt index 257f2bc79cc..0ff91df1da4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -94,6 +94,8 @@ include(CoinCMakeUtilities) option(COIN_BUILD_SHARED_LIBS "Build shared library when ON (default), static when OFF." ON) option(COIN_BUILD_TESTS "Build unit tests when ON (default), skips them when OFF." ON) +option(COIN_BUILD_BENCHMARKS + "Build deterministic renderer benchmarks and stress tests with COIN_BUILD_TESTS." OFF) option(COIN_BUILD_VISUAL_TESTS "Enable visual regression test tooling and CLI helpers." OFF) option(COIN_FETCH_TEST_DEPENDENCIES "Fetch missing test-only dependencies from pinned upstream sources." OFF) @@ -174,6 +176,7 @@ cmake_dependent_option(COIN_DEBUG_CHECK_THREAD "Enable thread check in several c report_prepare( COIN_BUILD_SHARED_LIBS COIN_BUILD_TESTS + COIN_BUILD_BENCHMARKS COIN_FETCH_TEST_DEPENDENCIES COIN_BUILD_VISUAL_TESTS COIN_BUILD_DOCUMENTATION diff --git a/include/Inventor/SoRenderManager.h b/include/Inventor/SoRenderManager.h index 5a45a7fbcff..2622de9aa15 100644 --- a/include/Inventor/SoRenderManager.h +++ b/include/Inventor/SoRenderManager.h @@ -102,6 +102,38 @@ class COIN_DLL_API SoRenderManager { SbBool rendered; }; + /*! Optional CPU timings for the most recent retained render and pick. + + Timing is disabled by default. When enabled, the manager records coarse + orchestration phases without changing backend behavior. A zero duration + means that the phase did not run, for example when a pick reused its + existing pick buffer. + */ + struct RenderPhaseStatistics { + uint64_t drawListConstructionNanoseconds = 0; + uint64_t drawListPrimitiveGenerationNanoseconds = 0; + uint64_t drawListGeometryPackingNanoseconds = 0; + uint64_t drawListCommandEmissionNanoseconds = 0; + uint64_t planConstructionNanoseconds = 0; + uint64_t backendSubmissionNanoseconds = 0; + uint64_t backendFrameSetupNanoseconds = 0; + uint64_t backendResourcePreparationNanoseconds = 0; + uint64_t backendCommandExecutionNanoseconds = 0; + uint64_t backendSelectionNanoseconds = 0; + uint64_t pickPlanConstructionNanoseconds = 0; + uint64_t pickBufferUpdateNanoseconds = 0; + uint64_t pickQueryNanoseconds = 0; + uint64_t pickResultResolutionNanoseconds = 0; + uint64_t backendPickTargetPreparationNanoseconds = 0; + uint64_t backendPickTargetRenderingNanoseconds = 0; + uint64_t backendPickDepthRenderingNanoseconds = 0; + uint64_t backendPickDepthPeelingNanoseconds = 0; + uint64_t backendPickReadbackNanoseconds = 0; + uint64_t backendPickHitProcessingNanoseconds = 0; + uint64_t backendPickTargetRestoreNanoseconds = 0; + uint64_t pickBufferRefreshes = 0; + }; + class COIN_DLL_API Superimposition { public: enum StateFlags { @@ -264,6 +296,11 @@ class COIN_DLL_API SoRenderManager { SbBool isRenderPipelineAvailable(RenderPipeline pipeline) const; const RenderResult & getLastRenderResult(void) const; + //! Enable coarse retained-renderer CPU timing for diagnostics. + void setRenderPhaseTimingEnabled(SbBool enabled); + SbBool isRenderPhaseTimingEnabled(void) const; + RenderPhaseStatistics getRenderPhaseStatistics(void) const; + /*! Return the closest renderer-neutral scene hit. The caller owns result. */ SbBool pickClosest(int x, int y, int radius, SoPickedPoint *& result); /*! Return front-to-back renderer-neutral scene hits around a cursor. */ diff --git a/include/Inventor/actions/SoIRRenderAction.h b/include/Inventor/actions/SoIRRenderAction.h index 3bca71e6c59..b9f1facf782 100644 --- a/include/Inventor/actions/SoIRRenderAction.h +++ b/include/Inventor/actions/SoIRRenderAction.h @@ -44,6 +44,12 @@ class COIN_DLL_API SoIRRenderAction : public SoAction { SO_ACTION_HEADER(SoIRRenderAction); public: + struct ConstructionStatistics { + uint64_t primitiveGenerationNanoseconds = 0; + uint64_t geometryPackingNanoseconds = 0; + uint64_t commandEmissionNanoseconds = 0; + }; + /*! Camera state policy used when starting a root traversal. */ enum class CameraPolicy { //! Initialize the traversal from the camera configured on this action. @@ -160,6 +166,13 @@ class COIN_DLL_API SoIRRenderAction : public SoAction { void popPrimitiveCollector(PrimitiveCollector * collector); //! Return the currently active primitive collector, or NULL. PrimitiveCollector * getActivePrimitiveCollector(void) const; + //! Enable intrusive construction attribution for benchmark diagnostics. + void setConstructionTimingEnabled(SbBool enabled); + SbBool isConstructionTimingEnabled() const; + const ConstructionStatistics & getConstructionStatistics() const; + void recordPrimitiveGenerationNanoseconds(uint64_t nanoseconds); + void recordGeometryPackingNanoseconds(uint64_t nanoseconds); + void recordCommandEmissionNanoseconds(uint64_t nanoseconds); protected: virtual void beginTraversal(SoNode * node) override; diff --git a/include/Inventor/system/gl-fallbacks.h b/include/Inventor/system/gl-fallbacks.h index 03c8dec434d..9505cfcb24e 100644 --- a/include/Inventor/system/gl-fallbacks.h +++ b/include/Inventor/system/gl-fallbacks.h @@ -171,6 +171,9 @@ #ifndef GL_COLOR_ATTACHMENT0_EXT #define GL_COLOR_ATTACHMENT0_EXT 0x8CE0 #endif +#ifndef GL_COLOR_ATTACHMENT0 +#define GL_COLOR_ATTACHMENT0 0x8CE0 +#endif #ifndef GL_COLOR_TABLE_WIDTH #define GL_COLOR_TABLE_WIDTH 0x80D9 @@ -219,6 +222,9 @@ #ifndef GL_DEPTH_ATTACHMENT_EXT #define GL_DEPTH_ATTACHMENT_EXT 0x8D00 #endif +#ifndef GL_DEPTH_ATTACHMENT +#define GL_DEPTH_ATTACHMENT 0x8D00 +#endif #ifndef GL_DEPTH_COMPONENT24 #define GL_DEPTH_COMPONENT24 0x81A6 @@ -413,10 +419,22 @@ #ifndef GL_FRAMEBUFFER_COMPLETE_EXT #define GL_FRAMEBUFFER_COMPLETE_EXT 0x8CD5 #endif +#ifndef GL_FRAMEBUFFER_COMPLETE +#define GL_FRAMEBUFFER_COMPLETE 0x8CD5 +#endif #ifndef GL_FRAMEBUFFER_EXT #define GL_FRAMEBUFFER_EXT 0x8D40 #endif +#ifndef GL_FRAMEBUFFER +#define GL_FRAMEBUFFER 0x8D40 +#endif +#ifndef GL_READ_FRAMEBUFFER +#define GL_READ_FRAMEBUFFER 0x8CA8 +#endif +#ifndef GL_DRAW_FRAMEBUFFER +#define GL_DRAW_FRAMEBUFFER 0x8CA9 +#endif #ifndef GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_EXT #define GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_EXT 0x8CD6 @@ -573,6 +591,9 @@ #ifndef GL_RENDERBUFFER_EXT #define GL_RENDERBUFFER_EXT 0x8D41 #endif +#ifndef GL_RENDERBUFFER +#define GL_RENDERBUFFER 0x8D41 +#endif #ifndef GL_RGB16F_ARB #define GL_RGB16F_ARB 0x881B @@ -709,6 +730,12 @@ #ifndef GL_PACK_IMAGE_HEIGHT #define GL_PACK_IMAGE_HEIGHT 0x806C #endif +#ifndef GL_PIXEL_PACK_BUFFER +#define GL_PIXEL_PACK_BUFFER 0x88EB +#endif +#ifndef GL_PIXEL_PACK_BUFFER_BINDING +#define GL_PIXEL_PACK_BUFFER_BINDING 0x88ED +#endif #ifndef GL_PACK_SKIP_IMAGES #define GL_PACK_SKIP_IMAGES 0x806B diff --git a/src/actions/SoIRRenderAction.cpp b/src/actions/SoIRRenderAction.cpp index 9fbb169c602..e7114a8186f 100644 --- a/src/actions/SoIRRenderAction.cpp +++ b/src/actions/SoIRRenderAction.cpp @@ -89,6 +89,8 @@ class SoIRRenderActionP { SoIRBuffer geometryPool; std::vector textureStorage; SbList collectorStack; + bool constructionTimingEnabled = false; + SoIRRenderAction::ConstructionStatistics constructionStatistics; }; #define PRIVATE(obj) (obj->pimpl) @@ -416,6 +418,45 @@ SoIRRenderAction::getActivePrimitiveCollector(void) const return PRIVATE(this)->collectorStack[count - 1]; } +void +SoIRRenderAction::setConstructionTimingEnabled(const SbBool enabled) +{ + PRIVATE(this)->constructionTimingEnabled = enabled != FALSE; +} + +SbBool +SoIRRenderAction::isConstructionTimingEnabled() const +{ + return PRIVATE(this)->constructionTimingEnabled ? TRUE : FALSE; +} + +const SoIRRenderAction::ConstructionStatistics & +SoIRRenderAction::getConstructionStatistics() const +{ + return PRIVATE(this)->constructionStatistics; +} + +void +SoIRRenderAction::recordPrimitiveGenerationNanoseconds(uint64_t nanoseconds) +{ + PRIVATE(this)->constructionStatistics.primitiveGenerationNanoseconds += + nanoseconds; +} + +void +SoIRRenderAction::recordGeometryPackingNanoseconds(uint64_t nanoseconds) +{ + PRIVATE(this)->constructionStatistics.geometryPackingNanoseconds += + nanoseconds; +} + +void +SoIRRenderAction::recordCommandEmissionNanoseconds(uint64_t nanoseconds) +{ + PRIVATE(this)->constructionStatistics.commandEmissionNanoseconds += + nanoseconds; +} + void * SoIRRenderAction::allocateGeometryStorage(size_t bytes, size_t alignment) { @@ -531,6 +572,7 @@ SoIRRenderAction::resetFrameResources() PRIVATE(this)->geometryPool.clear(); PRIVATE(this)->textureStorage.clear(); PRIVATE(this)->collectorStack.truncate(0); + PRIVATE(this)->constructionStatistics = ConstructionStatistics(); } void diff --git a/src/rendering/SoGLRenderBackend.cpp b/src/rendering/SoGLRenderBackend.cpp index 409d3f4e4e5..cfb8a766edf 100644 --- a/src/rendering/SoGLRenderBackend.cpp +++ b/src/rendering/SoGLRenderBackend.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -50,6 +51,16 @@ static constexpr GLuint COLOR_ATTRIBUTE = 2; static constexpr GLuint TEXCOORD_ATTRIBUTE = 3; static constexpr GLuint LINE_DISTANCE_ATTRIBUTE = 4; +using BackendPhaseClock = std::chrono::steady_clock; + +uint64_t +elapsedNanoseconds(const BackendPhaseClock::time_point & start) +{ + return static_cast( + std::chrono::duration_cast( + BackendPhaseClock::now() - start).count()); +} + GLenum textureWrapToGL(const SoTextureWrap wrap) { @@ -2915,6 +2926,11 @@ SoGLRenderBackend::updatePickBuffer(const SoDrawList & drawlist, this->emitError("updatePickBuffer called before backend initialization"); return FALSE; } + const bool measurePhases = this->isPhaseTimingEnabled(); + this->phaseStatistics.pickTargetPreparationNanoseconds = 0; + this->phaseStatistics.pickTargetRenderingNanoseconds = 0; + const BackendPhaseClock::time_point preparationStart = measurePhases + ? BackendPhaseClock::now() : BackendPhaseClock::time_point(); this->pickTarget.ready = false; this->pickTarget.lookup.clear(); ScopedGLState state(this->glue); @@ -2948,6 +2964,12 @@ SoGLRenderBackend::updatePickBuffer(const SoDrawList & drawlist, SbMat frameProjection; params.viewMatrix.getValue(frameView); params.projMatrix.getValue(frameProjection); + if (measurePhases) { + this->phaseStatistics.pickTargetPreparationNanoseconds = + elapsedNanoseconds(preparationStart); + } + const BackendPhaseClock::time_point renderingStart = measurePhases + ? BackendPhaseClock::now() : BackendPhaseClock::time_point(); auto drawPickCommand = [&](const uint32_t commandIndex) { for (size_t i = 0; i < this->pickTarget.lookup.size(); ++i) { const int lookupCommandIndex = this->pickTarget.lookup[i].commandIndex; @@ -2986,6 +3008,10 @@ SoGLRenderBackend::updatePickBuffer(const SoDrawList & drawlist, this->pickTarget.plan = plan; this->pickTarget.params = params; this->pickTarget.ready = true; + if (measurePhases) { + this->phaseStatistics.pickTargetRenderingNanoseconds = + elapsedNanoseconds(renderingStart); + } return TRUE; } @@ -3143,6 +3169,12 @@ SoGLRenderBackend::pickDepthStack(const int x, const int y, const int radius, maxLayers <= 0 || maxHits <= 0 || !this->pickTarget.drawlist) { return FALSE; } + const bool measurePhases = this->isPhaseTimingEnabled(); + this->phaseStatistics.pickDepthRenderingNanoseconds = 0; + this->phaseStatistics.pickDepthPeelingNanoseconds = 0; + this->phaseStatistics.pickReadbackNanoseconds = 0; + this->phaseStatistics.pickHitProcessingNanoseconds = 0; + this->phaseStatistics.pickTargetRestoreNanoseconds = 0; const int width = this->pickTarget.size[0]; const int height = this->pickTarget.size[1]; @@ -3205,11 +3237,19 @@ SoGLRenderBackend::pickDepthStack(const int x, const int y, const int radius, auto readLayer = [&]() { std::vector ids(pixelCount, 0); std::vector depths(pixelCount, 1.0f); + const BackendPhaseClock::time_point readbackStart = measurePhases + ? BackendPhaseClock::now() : BackendPhaseClock::time_point(); glReadPixels(left, bottom, readWidth, readHeight, GL_RED_INTEGER, GL_UNSIGNED_INT, ids.data()); glReadPixels(left, bottom, readWidth, readHeight, GL_DEPTH_COMPONENT, GL_FLOAT, depths.data()); + if (measurePhases) { + this->phaseStatistics.pickReadbackNanoseconds += + elapsedNanoseconds(readbackStart); + } + const BackendPhaseClock::time_point processingStart = measurePhases + ? BackendPhaseClock::now() : BackendPhaseClock::time_point(); std::unordered_map layerHits; for (int row = 0; row < readHeight; ++row) { for (int column = 0; column < readWidth; ++column) { @@ -3242,12 +3282,18 @@ SoGLRenderBackend::pickDepthStack(const int x, const int y, const int radius, if (lhs.depth != rhs.depth) return lhs.depth < rhs.depth; return lhs.id < rhs.id; }); + if (measurePhases) { + this->phaseStatistics.pickHitProcessingNanoseconds += + elapsedNanoseconds(processingStart); + } return ordered; }; auto renderLayer = [&](const std::vector & commands, const int previousDepth, const int targetDepth, const bool peel) { + const BackendPhaseClock::time_point renderingStart = measurePhases + ? BackendPhaseClock::now() : BackendPhaseClock::time_point(); cc_glglue_glBindFramebuffer(this->glue, GL_FRAMEBUFFER, this->pickTarget.framebuffer); cc_glglue_glFramebufferTexture2D( @@ -3289,6 +3335,12 @@ SoGLRenderBackend::pickDepthStack(const int x, const int y, const int radius, for (const uint32_t commandIndex : commands) { drawPickCommand(commandIndex); } + if (measurePhases) { + uint64_t & destination = peel + ? this->phaseStatistics.pickDepthPeelingNanoseconds + : this->phaseStatistics.pickDepthRenderingNanoseconds; + destination += elapsedNanoseconds(renderingStart); + } return true; }; @@ -3336,7 +3388,13 @@ SoGLRenderBackend::pickDepthStack(const int x, const int y, const int radius, // A depth-stack query mutates the ping-pong attachments. Rebuild the cached // frontmost target once so ordinary hover remains a read-only tiny query. this->pickTarget.peelEnabled = false; + const BackendPhaseClock::time_point restoreStart = measurePhases + ? BackendPhaseClock::now() : BackendPhaseClock::time_point(); this->updatePickBuffer(drawlist, plan, params); + if (measurePhases) { + this->phaseStatistics.pickTargetRestoreNanoseconds = + elapsedNanoseconds(restoreStart); + } return !results.hits.empty(); } @@ -3430,9 +3488,27 @@ SoGLRenderBackend::render(const SoDrawList & drawlist, return FALSE; } + const bool measurePhases = this->isPhaseTimingEnabled(); + this->phaseStatistics.frameSetupNanoseconds = 0; + this->phaseStatistics.resourcePreparationNanoseconds = 0; + this->phaseStatistics.commandExecutionNanoseconds = 0; + this->phaseStatistics.selectionNanoseconds = 0; + this->debugValidateDrawList(drawlist); + const BackendPhaseClock::time_point frameSetupStart = measurePhases + ? BackendPhaseClock::now() : BackendPhaseClock::time_point(); this->beginFrame(params); + if (measurePhases) { + this->phaseStatistics.frameSetupNanoseconds = + elapsedNanoseconds(frameSetupStart); + } + const BackendPhaseClock::time_point resourceStart = measurePhases + ? BackendPhaseClock::now() : BackendPhaseClock::time_point(); this->updateGeometryCache(drawlist); + if (measurePhases) { + this->phaseStatistics.resourcePreparationNanoseconds = + elapsedNanoseconds(resourceStart); + } SbMat view; SbMat projection; @@ -3458,10 +3534,18 @@ SoGLRenderBackend::render(const SoDrawList & drawlist, const auto flushSelection = [&]() { if (!queuedSelection.selected.empty() || !queuedSelection.highlighted.empty()) { + const BackendPhaseClock::time_point selectionStart = measurePhases + ? BackendPhaseClock::now() : BackendPhaseClock::time_point(); this->renderSelection(drawlist, queuedSelection, params); + if (measurePhases) { + this->phaseStatistics.selectionNanoseconds += + elapsedNanoseconds(selectionStart); + } queuedSelection = SoSelectionState(); } }; + const BackendPhaseClock::time_point executionStart = measurePhases + ? BackendPhaseClock::now() : BackendPhaseClock::time_point(); for (int i = 0; i < plan.getNumOperations(); ++i) { const SoRenderOperation & operation = plan.getOperation(i); if (operation.type == SoRenderOperationType::DRAW) { @@ -3488,5 +3572,16 @@ SoGLRenderBackend::render(const SoDrawList & drawlist, } } cc_glglue_glUseProgram(this->glue, 0); + if (measurePhases) { + const uint64_t executionWithSelection = elapsedNanoseconds(executionStart); + this->phaseStatistics.commandExecutionNanoseconds = + executionWithSelection - this->phaseStatistics.selectionNanoseconds; + } return TRUE; } + +SoRenderBackendPhaseStatistics +SoGLRenderBackend::getPhaseStatistics() const +{ + return this->phaseStatistics; +} diff --git a/src/rendering/SoGLRenderBackend.h b/src/rendering/SoGLRenderBackend.h index e9da9bcb1d4..2f766945c58 100644 --- a/src/rendering/SoGLRenderBackend.h +++ b/src/rendering/SoGLRenderBackend.h @@ -43,6 +43,7 @@ class SoGLRenderBackend : public SoRenderBackend { const SoRenderPlan & plan, const SoRenderParams & params, const SoSelectionState * selection = nullptr) override; + SoRenderBackendPhaseStatistics getPhaseStatistics() const override; //! Render the current DrawList into the explicit integer picking buffer. SbBool updatePickBuffer(const SoDrawList & drawlist, @@ -453,6 +454,7 @@ class SoGLRenderBackend : public SoRenderBackend { uint32_t cacheGeneration = 0; size_t cachedCommandCount = 0; bool haveCacheGeneration = false; + SoRenderBackendPhaseStatistics phaseStatistics; }; #endif // COIN_SOGLRENDERBACKEND_H diff --git a/src/rendering/SoRenderBackend.cpp b/src/rendering/SoRenderBackend.cpp index 4b27f58673b..6c631c76032 100644 --- a/src/rendering/SoRenderBackend.cpp +++ b/src/rendering/SoRenderBackend.cpp @@ -9,7 +9,7 @@ #include SoRenderBackend::SoRenderBackend() - : initialized(FALSE), initParams() + : initialized(FALSE), phaseTimingEnabled(FALSE), initParams() { } @@ -68,6 +68,24 @@ SoRenderBackend::isInitialized() const return this->initialized; } +void +SoRenderBackend::setPhaseTimingEnabled(const SbBool enabled) +{ + this->phaseTimingEnabled = enabled; +} + +SbBool +SoRenderBackend::isPhaseTimingEnabled() const +{ + return this->phaseTimingEnabled; +} + +SoRenderBackendPhaseStatistics +SoRenderBackend::getPhaseStatistics() const +{ + return SoRenderBackendPhaseStatistics(); +} + void SoRenderBackend::setInitialized(const SbBool state) { diff --git a/src/rendering/SoRenderBackend.h b/src/rendering/SoRenderBackend.h index bcdd39d96b2..11f8a7f9a96 100644 --- a/src/rendering/SoRenderBackend.h +++ b/src/rendering/SoRenderBackend.h @@ -46,6 +46,21 @@ struct SoRenderBackendInitParams { SoRenderBackendLogFn errorCallback = nullptr; }; +//! Opt-in CPU phase timings reported by retained-rendering backends. +struct SoRenderBackendPhaseStatistics { + uint64_t frameSetupNanoseconds = 0; + uint64_t resourcePreparationNanoseconds = 0; + uint64_t commandExecutionNanoseconds = 0; + uint64_t selectionNanoseconds = 0; + uint64_t pickTargetPreparationNanoseconds = 0; + uint64_t pickTargetRenderingNanoseconds = 0; + uint64_t pickDepthRenderingNanoseconds = 0; + uint64_t pickDepthPeelingNanoseconds = 0; + uint64_t pickReadbackNanoseconds = 0; + uint64_t pickHitProcessingNanoseconds = 0; + uint64_t pickTargetRestoreNanoseconds = 0; +}; + /*! \class SoRenderBackend \brief Backend-neutral lifecycle and DrawList execution interface. @@ -101,6 +116,9 @@ class SoRenderBackend { const SoRenderParams & params); SbBool isInitialized() const; + void setPhaseTimingEnabled(SbBool enabled); + SbBool isPhaseTimingEnabled() const; + virtual SoRenderBackendPhaseStatistics getPhaseStatistics() const; protected: void setInitialized(SbBool state); @@ -114,6 +132,7 @@ class SoRenderBackend { private: SbBool initialized; + SbBool phaseTimingEnabled; SoRenderBackendInitParams initParams; }; diff --git a/src/rendering/SoRenderManager.cpp b/src/rendering/SoRenderManager.cpp index cf04327faa1..474ac3d30db 100644 --- a/src/rendering/SoRenderManager.cpp +++ b/src/rendering/SoRenderManager.cpp @@ -61,6 +61,7 @@ #include #include +#include //FIXME:Need this include early, since including it via SoRenderManagerP.h will cause problems for cygwin. Don't understand the root cause BFG 20090629 #include @@ -401,6 +402,8 @@ SoRenderManager::SoRenderManager(void) PRIVATE(this)->lightingmode = SoRenderManager::LIT; PRIVATE(this)->irAction = NULL; PRIVATE(this)->renderBackend = NULL; + PRIVATE(this)->renderPhaseTimingEnabled = FALSE; + PRIVATE(this)->renderPhaseStatistics = RenderPhaseStatistics(); PRIVATE(this)->renderBackendContextId = 0; PRIVATE(this)->drawListCallbackScope = FALSE; PRIVATE(this)->pickTargetDirty = TRUE; @@ -996,6 +999,22 @@ void SoRenderManager::renderDrawListPipeline(const SbBool clearwindow, const SbBool clearzbuffer) { + using RenderPhaseClock = std::chrono::steady_clock; + RenderPhaseStatistics & phaseStatistics = + PRIVATE(this)->renderPhaseStatistics; + phaseStatistics.drawListConstructionNanoseconds = 0; + phaseStatistics.drawListPrimitiveGenerationNanoseconds = 0; + phaseStatistics.drawListGeometryPackingNanoseconds = 0; + phaseStatistics.drawListCommandEmissionNanoseconds = 0; + phaseStatistics.planConstructionNanoseconds = 0; + phaseStatistics.backendSubmissionNanoseconds = 0; + phaseStatistics.backendFrameSetupNanoseconds = 0; + phaseStatistics.backendResourcePreparationNanoseconds = 0; + phaseStatistics.backendCommandExecutionNanoseconds = 0; + phaseStatistics.backendSelectionNanoseconds = 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 = @@ -1083,6 +1102,7 @@ SoRenderManager::renderDrawListPipeline(const SbBool clearwindow, if (!PRIVATE(this)->renderBackend) { PRIVATE(this)->renderBackend = new SoGLRenderBackend; + PRIVATE(this)->renderBackend->setPhaseTimingEnabled(measurePhases); } if (!PRIVATE(this)->renderBackend->isInitialized()) { SoRenderBackendInitParams initparams = {}; @@ -1139,6 +1159,7 @@ SoRenderManager::renderDrawListPipeline(const SbBool clearwindow, PRIVATE(this)->irAction->setDevicePixelRatio(PRIVATE(this)->devicePixelRatio); SoIRRenderAction * action = PRIVATE(this)->irAction; + action->setConstructionTimingEnabled(measurePhases); SoState * state = action->getState(); action->beginFrame(); @@ -1263,6 +1284,20 @@ SoRenderManager::renderDrawListPipeline(const SbBool clearwindow, SoDrawList & drawlist = PRIVATE(this)->irAction->getMutableDrawList(); + if (measurePhases) { + const SoIRRenderAction::ConstructionStatistics & construction = + action->getConstructionStatistics(); + phaseStatistics.drawListConstructionNanoseconds = + static_cast(std::chrono::duration_cast( + RenderPhaseClock::now() - drawListStart).count()); + phaseStatistics.drawListPrimitiveGenerationNanoseconds = + construction.primitiveGenerationNanoseconds; + phaseStatistics.drawListGeometryPackingNanoseconds = + construction.geometryPackingNanoseconds; + phaseStatistics.drawListCommandEmissionNanoseconds = + construction.commandEmissionNanoseconds; + } + SoRenderParams params = {}; params.viewport = viewport; // The retained traversal scopes its scene roots and restores the action @@ -1288,9 +1323,33 @@ SoRenderManager::renderDrawListPipeline(const SbBool clearwindow, (clearzbuffer ? SO_PARAM_CLEAR_DEPTH : 0u); SoRenderPlanner planner; SoRenderPlan plan; + const RenderPhaseClock::time_point planStart = measurePhases + ? RenderPhaseClock::now() : RenderPhaseClock::time_point(); planner.build(drawlist, plan); + if (measurePhases) { + phaseStatistics.planConstructionNanoseconds = + static_cast(std::chrono::duration_cast( + RenderPhaseClock::now() - planStart).count()); + } + const RenderPhaseClock::time_point submissionStart = measurePhases + ? RenderPhaseClock::now() : RenderPhaseClock::time_point(); PRIVATE(this)->renderBackend->render(drawlist, plan, params); + if (measurePhases) { + phaseStatistics.backendSubmissionNanoseconds = + static_cast(std::chrono::duration_cast( + RenderPhaseClock::now() - submissionStart).count()); + const SoRenderBackendPhaseStatistics backendPhases = + PRIVATE(this)->renderBackend->getPhaseStatistics(); + phaseStatistics.backendFrameSetupNanoseconds = + backendPhases.frameSetupNanoseconds; + phaseStatistics.backendResourcePreparationNanoseconds = + backendPhases.resourcePreparationNanoseconds; + phaseStatistics.backendCommandExecutionNanoseconds = + backendPhases.commandExecutionNanoseconds; + phaseStatistics.backendSelectionNanoseconds = + backendPhases.selectionNanoseconds; + } PRIVATE(this)->pickTargetDirty = TRUE; PRIVATE(this)->pickTargetGeneration = 0; @@ -2318,6 +2377,81 @@ SoRenderManager::getRenderPipeline(void) const return PRIVATE(this)->renderPipeline; } +void +SoRenderManager::releaseRenderBackendResources(void) +{ + if (PRIVATE(this)->renderBackend && + PRIVATE(this)->renderBackend->isInitialized()) { + if (backendContextIsCurrent(PRIVATE(this))) { + PRIVATE(this)->renderBackend->shutdown(); + } + else { + SoDebugError::postWarning( + "SoRenderManager::releaseRenderBackendResources", + "the backend's owning GL context is not current; use discardRenderBackendResources() after context loss"); + } + } +} + +void +SoRenderManager::discardRenderBackendResources(void) +{ + if (PRIVATE(this)->renderBackend && + PRIVATE(this)->renderBackend->isInitialized()) { + PRIVATE(this)->renderBackend->discard(); + } +} + +void +SoRenderManager::setRenderLayerRoot(RenderLayer layer, SoNode * root) +{ + SoNode ** slot = NULL; + SoNodeSensor ** sensorSlot = NULL; + switch (layer) { + case RENDER_LAYER_BACKGROUND: + slot = &PRIVATE(this)->renderLayerBackgroundRoot; + sensorSlot = &PRIVATE(this)->renderLayerBackgroundSensor; + break; + case RENDER_LAYER_FOREGROUND: + slot = &PRIVATE(this)->renderLayerForegroundRoot; + sensorSlot = &PRIVATE(this)->renderLayerForegroundSensor; + break; + default: + assert(0 && "unknown render layer"); + return; + } + + if (*slot == root) return; + + if (*sensorSlot) (*sensorSlot)->detach(); + if (*slot) (*slot)->unref(); + + *slot = root; + if (root) { + root->ref(); + if (!*sensorSlot) { + *sensorSlot = new SoNodeSensor(SoRenderManager::nodesensorCB, this); + } + (*sensorSlot)->attach(root); + } + + this->scheduleRedraw(); +} + +SoNode * +SoRenderManager::getRenderLayerRoot(RenderLayer layer) const +{ + switch (layer) { + case RENDER_LAYER_BACKGROUND: + return PRIVATE(this)->renderLayerBackgroundRoot; + case RENDER_LAYER_FOREGROUND: + return PRIVATE(this)->renderLayerForegroundRoot; + default: + assert(0 && "unknown render layer"); + return NULL; + } +} + SbBool SoRenderManager::isRenderPipelineAvailable(const RenderPipeline pipeline) const { @@ -2337,6 +2471,30 @@ SoRenderManager::getLastRenderResult(void) const return PRIVATE(this)->lastRenderResult; } +void +SoRenderManager::setRenderPhaseTimingEnabled(const SbBool enabled) +{ + PRIVATE(this)->renderPhaseTimingEnabled = enabled; + if (PRIVATE(this)->renderBackend) { + PRIVATE(this)->renderBackend->setPhaseTimingEnabled(enabled); + } + if (!enabled) { + PRIVATE(this)->renderPhaseStatistics = RenderPhaseStatistics(); + } +} + +SbBool +SoRenderManager::isRenderPhaseTimingEnabled(void) const +{ + return PRIVATE(this)->renderPhaseTimingEnabled; +} + +SoRenderManager::RenderPhaseStatistics +SoRenderManager::getRenderPhaseStatistics(void) const +{ + return PRIVATE(this)->renderPhaseStatistics; +} + SbBool SoRenderManager::pickClosest(const int x, const int y, const int radius, SoPickedPoint *& result) @@ -2356,6 +2514,22 @@ SoRenderManager::pickDepthStack(const int x, const int y, const int radius, SoPickedPointList & results, const int maxHits) { + using PickPhaseClock = std::chrono::steady_clock; + RenderPhaseStatistics & phaseStatistics = + PRIVATE(this)->renderPhaseStatistics; + phaseStatistics.pickPlanConstructionNanoseconds = 0; + phaseStatistics.pickBufferUpdateNanoseconds = 0; + phaseStatistics.pickQueryNanoseconds = 0; + phaseStatistics.pickResultResolutionNanoseconds = 0; + phaseStatistics.backendPickTargetPreparationNanoseconds = 0; + phaseStatistics.backendPickTargetRenderingNanoseconds = 0; + phaseStatistics.backendPickDepthRenderingNanoseconds = 0; + phaseStatistics.backendPickDepthPeelingNanoseconds = 0; + phaseStatistics.backendPickReadbackNanoseconds = 0; + phaseStatistics.backendPickHitProcessingNanoseconds = 0; + phaseStatistics.backendPickTargetRestoreNanoseconds = 0; + phaseStatistics.pickBufferRefreshes = 0; + const SbBool measurePhases = PRIVATE(this)->renderPhaseTimingEnabled; results.truncate(0); if (PRIVATE(this)->renderPipeline != RenderPipeline::DRAW_LIST || !PRIVATE(this)->renderBackend || !PRIVATE(this)->irAction || @@ -2367,21 +2541,70 @@ SoRenderManager::pickDepthStack(const int x, const int y, const int radius, PRIVATE(this)->pickTargetGeneration != drawlist.getGeneration()) { SoRenderPlanner planner; SoRenderPlan plan; + const PickPhaseClock::time_point planStart = measurePhases + ? PickPhaseClock::now() : PickPhaseClock::time_point(); planner.build(drawlist, plan); + if (measurePhases) { + phaseStatistics.pickPlanConstructionNanoseconds = + static_cast( + std::chrono::duration_cast( + PickPhaseClock::now() - planStart).count()); + } + const PickPhaseClock::time_point updateStart = measurePhases + ? PickPhaseClock::now() : PickPhaseClock::time_point(); if (!PRIVATE(this)->renderBackend->updatePickBuffer(drawlist, plan, params)) return FALSE; + if (measurePhases) { + phaseStatistics.pickBufferUpdateNanoseconds = + static_cast( + std::chrono::duration_cast( + PickPhaseClock::now() - updateStart).count()); + phaseStatistics.pickBufferRefreshes = 1; + const SoRenderBackendPhaseStatistics backendPhases = + PRIVATE(this)->renderBackend->getPhaseStatistics(); + phaseStatistics.backendPickTargetPreparationNanoseconds = + backendPhases.pickTargetPreparationNanoseconds; + phaseStatistics.backendPickTargetRenderingNanoseconds = + backendPhases.pickTargetRenderingNanoseconds; + } PRIVATE(this)->pickTargetDirty = FALSE; PRIVATE(this)->pickTargetGeneration = drawlist.getGeneration(); } SoPickResultList raw; + const PickPhaseClock::time_point queryStart = measurePhases + ? PickPhaseClock::now() : PickPhaseClock::time_point(); if (!PRIVATE(this)->renderBackend->pickDepthStack( x, y, radius, maxLayers, maxHits, raw)) return FALSE; + if (measurePhases) { + phaseStatistics.pickQueryNanoseconds = + static_cast(std::chrono::duration_cast( + PickPhaseClock::now() - queryStart).count()); + const SoRenderBackendPhaseStatistics backendPhases = + PRIVATE(this)->renderBackend->getPhaseStatistics(); + phaseStatistics.backendPickDepthRenderingNanoseconds = + backendPhases.pickDepthRenderingNanoseconds; + phaseStatistics.backendPickDepthPeelingNanoseconds = + backendPhases.pickDepthPeelingNanoseconds; + phaseStatistics.backendPickReadbackNanoseconds = + backendPhases.pickReadbackNanoseconds; + phaseStatistics.backendPickHitProcessingNanoseconds = + backendPhases.pickHitProcessingNanoseconds; + phaseStatistics.backendPickTargetRestoreNanoseconds = + backendPhases.pickTargetRestoreNanoseconds; + } if (raw.generation != drawlist.getGeneration()) return FALSE; + const PickPhaseClock::time_point resolutionStart = measurePhases + ? PickPhaseClock::now() : PickPhaseClock::time_point(); for (const SoPickResult & hit : raw.hits) { SoPickedPoint * picked = resolvePickResult(PRIVATE(this), hit, params); if (picked) results.append(picked); } + if (measurePhases) { + phaseStatistics.pickResultResolutionNanoseconds = + static_cast(std::chrono::duration_cast( + PickPhaseClock::now() - resolutionStart).count()); + } return results.getLength() != 0; } @@ -2429,81 +2652,6 @@ SoRenderManager::invalidateSharedGLState(void) #endif } -void -SoRenderManager::releaseRenderBackendResources(void) -{ - if (PRIVATE(this)->renderBackend && - PRIVATE(this)->renderBackend->isInitialized()) { - if (backendContextIsCurrent(PRIVATE(this))) { - PRIVATE(this)->renderBackend->shutdown(); - } - else { - SoDebugError::postWarning( - "SoRenderManager::releaseRenderBackendResources", - "the backend's owning GL context is not current; use discardRenderBackendResources() after context loss"); - } - } -} - -void -SoRenderManager::discardRenderBackendResources(void) -{ - if (PRIVATE(this)->renderBackend && - PRIVATE(this)->renderBackend->isInitialized()) { - PRIVATE(this)->renderBackend->discard(); - } -} - -void -SoRenderManager::setRenderLayerRoot(RenderLayer layer, SoNode * root) -{ - SoNode ** slot = NULL; - SoNodeSensor ** sensorSlot = NULL; - switch (layer) { - case RENDER_LAYER_BACKGROUND: - slot = &PRIVATE(this)->renderLayerBackgroundRoot; - sensorSlot = &PRIVATE(this)->renderLayerBackgroundSensor; - break; - case RENDER_LAYER_FOREGROUND: - slot = &PRIVATE(this)->renderLayerForegroundRoot; - sensorSlot = &PRIVATE(this)->renderLayerForegroundSensor; - break; - default: - assert(0 && "unknown render layer"); - return; - } - - if (*slot == root) return; - - if (*sensorSlot) (*sensorSlot)->detach(); - if (*slot) (*slot)->unref(); - - *slot = root; - if (root) { - root->ref(); - if (!*sensorSlot) { - *sensorSlot = new SoNodeSensor(SoRenderManager::nodesensorCB, this); - } - (*sensorSlot)->attach(root); - } - - this->scheduleRedraw(); -} - -SoNode * -SoRenderManager::getRenderLayerRoot(RenderLayer layer) const -{ - switch (layer) { - case RENDER_LAYER_BACKGROUND: - return PRIVATE(this)->renderLayerBackgroundRoot; - case RENDER_LAYER_FOREGROUND: - return PRIVATE(this)->renderLayerForegroundRoot; - default: - assert(0 && "unknown render layer"); - return NULL; - } -} - void SoRenderManager::invalidateDrawList(void) { diff --git a/src/rendering/SoRenderManagerP.h b/src/rendering/SoRenderManagerP.h index 8f36e2316c2..f5920f5a295 100644 --- a/src/rendering/SoRenderManagerP.h +++ b/src/rendering/SoRenderManagerP.h @@ -127,6 +127,8 @@ class SoRenderManagerP { SoRenderManager::LightingMode lightingmode; SoIRRenderAction * irAction; SoRenderBackend * renderBackend; + SbBool renderPhaseTimingEnabled; + SoRenderManager::RenderPhaseStatistics renderPhaseStatistics; uint32_t renderBackendContextId; SbBool drawListCallbackScope; SbBool pickTargetDirty; diff --git a/src/shapenodes/SoShape.cpp b/src/shapenodes/SoShape.cpp index f0374fccd2e..9b04457d7f7 100644 --- a/src/shapenodes/SoShape.cpp +++ b/src/shapenodes/SoShape.cpp @@ -53,6 +53,7 @@ class SoVBO; #include "elements/SoLazyElementP.h" #include "shapenodes/SoShapeGLRenderP.h" +#include #include #include #include @@ -234,8 +235,23 @@ class SoIRPrimitiveAssembler : public SoIRRenderAction::PrimitiveCollector { SoState * state = this->action->getState(); SoGeometryDesc geometry = {}; std::vector batches; + const bool measure = this->action->isConstructionTimingEnabled() != FALSE; + std::chrono::steady_clock::time_point start = measure + ? std::chrono::steady_clock::now() + : std::chrono::steady_clock::time_point(); this->fillGeometry(state, geometry, batches); + if (measure) { + this->action->recordGeometryPackingNanoseconds( + static_cast(std::chrono::duration_cast( + std::chrono::steady_clock::now() - start).count())); + start = std::chrono::steady_clock::now(); + } this->emitCommands(state, geometry, batches); + if (measure) { + this->action->recordCommandEmissionNanoseconds( + static_cast(std::chrono::duration_cast( + std::chrono::steady_clock::now() - start).count())); + } this->vertices.clear(); } @@ -835,7 +851,16 @@ SoShape::IRRender(SoIRRenderAction * action) SoIRPrimitiveAssembler assembler(action, this); action->pushPrimitiveCollector(&assembler); + const bool measure = action->isConstructionTimingEnabled() != FALSE; + const std::chrono::steady_clock::time_point primitiveStart = measure + ? std::chrono::steady_clock::now() + : std::chrono::steady_clock::time_point(); this->generatePrimitives(action); + if (measure) { + action->recordPrimitiveGenerationNanoseconds( + static_cast(std::chrono::duration_cast( + std::chrono::steady_clock::now() - primitiveStart).count())); + } action->popPrimitiveCollector(&assembler); assembler.finalize(); diff --git a/testsuite/CMakeLists.txt b/testsuite/CMakeLists.txt index eb9937348c6..aa92d3e941e 100644 --- a/testsuite/CMakeLists.txt +++ b/testsuite/CMakeLists.txt @@ -1,3 +1,5 @@ +set(COIN_TEST_SKIP_RETURN_CODE 77) + macro(create_testsuite input) get_filename_component(FLNAME "${input}" NAME_WE) get_filename_component(FLPATH "${input}" PATH) @@ -142,6 +144,7 @@ endif() if(COIN_BUILD_GL_TESTS_EFFECTIVE) add_library(CoinGLTestSupport STATIC + support/GLRenderTestSession.cpp support/GLTestContext.cpp support/GLTestFramebuffer.cpp) target_include_directories(CoinGLTestSupport PUBLIC @@ -152,7 +155,8 @@ if(COIN_BUILD_GL_TESTS_EFFECTIVE) ${COIN_TARGET_INCLUDE_DIRECTORIES}) target_link_libraries(CoinGLTestSupport PUBLIC ${_coin_glfw_target} - ${COIN_TARGET_LINK_LIBRARIES}) + ${COIN_TARGET_LINK_LIBRARIES} + PRIVATE Coin) function(coin_add_gl_test) cmake_parse_arguments(ARG "PRIVATE_COIN_API" "NAME;PROFILE" @@ -187,7 +191,7 @@ if(COIN_BUILD_GL_TESTS_EFFECTIVE) list(APPEND test_labels ${ARG_LABELS}) endif() set_tests_properties(${ARG_NAME} PROPERTIES - SKIP_RETURN_CODE 77 LABELS "${test_labels}") + SKIP_RETURN_CODE ${COIN_TEST_SKIP_RETURN_CODE} LABELS "${test_labels}") endfunction() endif() @@ -210,7 +214,7 @@ function(coin_add_egl_test) set(test_labels ${ARG_LABELS}) endif() set_tests_properties(${ARG_NAME} PROPERTIES - SKIP_RETURN_CODE 77 LABELS "${test_labels}") + SKIP_RETURN_CODE ${COIN_TEST_SKIP_RETURN_CODE} LABELS "${test_labels}") endfunction() add_test( @@ -252,7 +256,8 @@ target_include_directories(LegacyBlendingTest PRIVATE add_test(NAME LegacyBlendingTest COMMAND LegacyBlendingTest) add_executable(RetainedIRTest RetainedIRTest.cpp) -target_link_libraries(RetainedIRTest Coin ${COIN_TARGET_LINK_LIBRARIES}) +target_link_libraries(RetainedIRTest Coin + ${COIN_TARGET_LINK_LIBRARIES}) target_include_directories(RetainedIRTest PRIVATE ${PROJECT_SOURCE_DIR}/include ${PROJECT_BINARY_DIR}/include @@ -348,14 +353,17 @@ if(COIN_BUILD_GL_TESTS_EFFECTIVE) coin_add_gl_test( NAME RetainedMaterialLightingGLTest PROFILE core + PRIVATE_COIN_API SOURCES RetainedMaterialLightingGLTest.cpp) coin_add_gl_test( NAME RetainedRasterGLTest PROFILE core + PRIVATE_COIN_API SOURCES RetainedRasterGLTest.cpp) coin_add_gl_test( NAME DrawListGLTest PROFILE core + PRIVATE_COIN_API SOURCES DrawListGLTest.cpp) coin_add_gl_test( NAME DrawListPickingTest @@ -365,10 +373,12 @@ if(COIN_BUILD_GL_TESTS_EFFECTIVE) coin_add_gl_test( NAME DrawListManagerTest PROFILE core + PRIVATE_COIN_API SOURCES DrawListManagerTest.cpp) coin_add_gl_test( NAME RenderLayerLifecycleTest PROFILE core + PRIVATE_COIN_API SOURCES RenderLayerLifecycleTest.cpp) coin_add_gl_test( NAME GLSLRuntimeTest @@ -402,6 +412,66 @@ if(COIN_BUILD_GL_TESTS_EFFECTIVE) SOURCES LegacyShapeGatingOrderTest.cpp LABELS legacygl) endif() + if(COIN_BUILD_BENCHMARKS) + add_library(CoinRenderWorkloadSupport STATIC + support/RenderWorkloads.cpp) + target_link_libraries(CoinRenderWorkloadSupport PRIVATE Coin) + target_include_directories(CoinRenderWorkloadSupport PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/support + ${PROJECT_SOURCE_DIR}/include + ${PROJECT_BINARY_DIR}/include + ${COIN_TARGET_INCLUDE_DIRECTORIES}) + if(NOT WIN32 OR NOT COIN_BUILD_SHARED_LIBS) + add_executable(CoinRenderGLBenchmarks CoinRenderGLBenchmarks.cpp) + target_link_libraries(CoinRenderGLBenchmarks PRIVATE + CoinRenderWorkloadSupport CoinGLTestSupport + Coin + ${COIN_TARGET_LINK_LIBRARIES}) + target_compile_definitions(CoinRenderGLBenchmarks PRIVATE COIN_INTERNAL) + target_include_directories(CoinRenderGLBenchmarks PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/support + ${PROJECT_SOURCE_DIR}/src + ${PROJECT_SOURCE_DIR}/include + ${PROJECT_BINARY_DIR}/include + ${COIN_TARGET_INCLUDE_DIRECTORIES}) + add_test(NAME CoinRenderGLBenchmarksSmoke + COMMAND CoinRenderGLBenchmarks --smoke) + set_tests_properties(CoinRenderGLBenchmarksSmoke PROPERTIES + SKIP_RETURN_CODE ${COIN_TEST_SKIP_RETURN_CODE} + LABELS "benchmark;benchmark-gl;benchmark-render") + endif() + add_executable(RenderWorkloadParityTest RenderWorkloadParityTest.cpp) + target_link_libraries(RenderWorkloadParityTest PRIVATE + CoinRenderWorkloadSupport CoinGLTestSupport + Coin ${COIN_TARGET_LINK_LIBRARIES}) + target_include_directories(RenderWorkloadParityTest PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/support + ${PROJECT_SOURCE_DIR}/src + ${PROJECT_SOURCE_DIR}/include + ${PROJECT_BINARY_DIR}/include + ${COIN_TARGET_INCLUDE_DIRECTORIES}) + add_test(NAME RenderWorkloadParityTest COMMAND RenderWorkloadParityTest) + set_tests_properties(RenderWorkloadParityTest PROPERTIES + SKIP_RETURN_CODE ${COIN_TEST_SKIP_RETURN_CODE} + LABELS "gl-core;benchmark;benchmark-render;workload-parity") + add_executable(CoinRenderWorkloadViewer CoinRenderWorkloadViewer.cpp) + target_sources(CoinRenderWorkloadViewer PRIVATE + support/RenderWorkloadViewerController.cpp) + target_link_libraries(CoinRenderWorkloadViewer PRIVATE + CoinRenderWorkloadSupport CoinGLTestSupport + Coin ${COIN_TARGET_LINK_LIBRARIES}) + target_include_directories(CoinRenderWorkloadViewer PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/support + ${PROJECT_SOURCE_DIR}/src + ${PROJECT_SOURCE_DIR}/include + ${PROJECT_BINARY_DIR}/include + ${COIN_TARGET_INCLUDE_DIRECTORIES}) + add_test(NAME CoinRenderWorkloadViewerSmoke + COMMAND CoinRenderWorkloadViewer --smoke) + set_tests_properties(CoinRenderWorkloadViewerSmoke PROPERTIES + SKIP_RETURN_CODE ${COIN_TEST_SKIP_RETURN_CODE} + LABELS "benchmark;benchmark-gl;benchmark-render;viewer-smoke") + endif() endif() if(HAVE_EGL) @@ -415,6 +485,27 @@ if(HAVE_EGL) LABELS offscreen requires-egl) endif() +if(COIN_BUILD_BENCHMARKS) + add_executable(CoinRenderBenchmarks CoinRenderBenchmarks.cpp) + target_link_libraries(CoinRenderBenchmarks PRIVATE + Coin ${COIN_TARGET_LINK_LIBRARIES}) + target_include_directories(CoinRenderBenchmarks PRIVATE + ${PROJECT_SOURCE_DIR}/src + ${PROJECT_SOURCE_DIR}/include + ${PROJECT_BINARY_DIR}/include + ${COIN_TARGET_INCLUDE_DIRECTORIES}) + + add_test(NAME CoinRenderBenchmarksSmoke + COMMAND CoinRenderBenchmarks --smoke) + set_tests_properties(CoinRenderBenchmarksSmoke PROPERTIES + LABELS "benchmark;benchmark-render;benchmark-picking;benchmark-selection") + + add_test(NAME CoinRenderLifecycleStress + COMMAND CoinRenderBenchmarks --stress --frames 10000) + set_tests_properties(CoinRenderLifecycleStress PROPERTIES + LABELS "stress;stress-render;stress-picking;stress-lifecycle") +endif() + # Keep the ordinary non-GL tests in the unit capability without maintaining a # second test-name manifest. Tests with an explicit rendering/EGL label keep # their more specific capability classification. diff --git a/testsuite/CoinRenderBenchmarks.cpp b/testsuite/CoinRenderBenchmarks.cpp new file mode 100644 index 00000000000..63068471437 --- /dev/null +++ b/testsuite/CoinRenderBenchmarks.cpp @@ -0,0 +1,343 @@ +#include "rendering/SoRenderPlan.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using Clock = std::chrono::steady_clock; + +struct Options { + bool smoke = false; + bool stress = false; + int frames = 0; + int samples = 0; + std::string output; +}; + +struct Result { + std::string name; + std::string category; + std::string phase; + int workload = 0; + int samples = 0; + double medianMs = 0.0; + double p95Ms = 0.0; + uint64_t checksum = 0; +}; + +volatile uint64_t benchmarkSink = 0; + +double elapsedMs(const Clock::time_point & start) +{ + return std::chrono::duration(Clock::now() - start).count(); +} + +Result summarize(const std::string & name, const std::string & category, + const std::string & phase, int workload, + std::vector timings, uint64_t checksum) +{ + std::sort(timings.begin(), timings.end()); + Result result; + result.name = name; + result.category = category; + result.phase = phase; + result.workload = workload; + result.samples = static_cast(timings.size()); + result.medianMs = timings[timings.size() / 2]; + const size_t p95 = static_cast( + std::ceil(static_cast(timings.size()) * 0.95)) - 1; + result.p95Ms = timings[p95]; + result.checksum = checksum; + benchmarkSink ^= checksum; + return result; +} + +SoSeparator * makeManyDrawsScene(int drawCount, bool materialChurn) +{ + SoSeparator * root = new SoSeparator; + root->ref(); + const SbVec3f triangle[] = { + SbVec3f(-0.5f, -0.5f, 0.0f), + SbVec3f(0.5f, -0.5f, 0.0f), + SbVec3f(0.0f, 0.5f, 0.0f) + }; + for (int i = 0; i < drawCount; ++i) { + SoSeparator * draw = new SoSeparator; + SoTransform * transform = new SoTransform; + transform->translation.setValue( + static_cast(i % 100), static_cast(i / 100), 0.0f); + draw->addChild(transform); + if (materialChurn) { + SoMaterial * material = new SoMaterial; + const float hue = static_cast(i % 31) / 30.0f; + material->diffuseColor.setValue(hue, 1.0f - hue, 0.5f); + draw->addChild(material); + } + SoCoordinate3 * coordinates = new SoCoordinate3; + coordinates->point.setValues(0, 3, triangle); + SoFaceSet * face = new SoFaceSet; + face->numVertices.set1Value(0, 3); + draw->addChild(coordinates); + draw->addChild(face); + root->addChild(draw); + } + return root; +} + +Result benchmarkTraversal(const std::string & name, int drawCount, + int samples, bool materialChurn) +{ + SoSeparator * scene = makeManyDrawsScene(drawCount, materialChurn); + SoIRRenderAction action(SbViewportRegion(64, 64)); + action.apply(scene); + std::vector timings; + uint64_t checksum = 0; + for (int sample = 0; sample < samples; ++sample) { + const Clock::time_point start = Clock::now(); + action.apply(scene); + timings.push_back(elapsedMs(start)); + checksum += static_cast(action.getDrawList().getNumCommands()); + } + scene->unref(); + return summarize(name, "render", "traversal_ir", drawCount, + timings, checksum); +} + +SoDrawList makeDrawList(int commandCount, bool transparent, + int depthSegments) +{ + SoDrawList drawlist; + drawlist.reserve(commandCount); + for (int i = 0; i < commandCount; ++i) { + SoRenderCommand command; + command.objectId = static_cast(i + 1); + command.nodeId = static_cast(i + 1); + command.instanceId = static_cast(i + 1); + command.geometry.vertexCount = 3; + command.geometry.hasBounds = TRUE; + command.geometry.boundsCenter = SbVec3f(0.0f, 0.0f, + -static_cast((i * 7919) % std::max(commandCount, 1))); + command.modelMatrix.makeIdentity(); + command.viewMatrix.makeIdentity(); + if (transparent) command.opacityClass = SO_OPACITY_TRANSPARENT; + drawlist.addCommand(command); + if (depthSegments > 1 && i > 0 && + i % std::max(commandCount / depthSegments, 1) == 0) { + SoDepthClearEvent event; + event.sequence = static_cast(i); + drawlist.addDepthClearEvent(event); + } + } + return drawlist; +} + +Result benchmarkPlanning(const std::string & name, const std::string & category, + int commandCount, int samples, bool transparent, + int depthSegments) +{ + const SoDrawList drawlist = makeDrawList(commandCount, transparent, depthSegments); + SoRenderPlanner planner; + SoRenderPlan plan; + std::vector timings; + uint64_t checksum = 0; + for (int sample = 0; sample < samples; ++sample) { + const Clock::time_point start = Clock::now(); + planner.build(drawlist, plan); + timings.push_back(elapsedMs(start)); + checksum += static_cast(plan.getNumOperations()); + } + return summarize(name, category, "plan_construction", commandCount, + timings, checksum); +} + +Result benchmarkPicking(int commandCount, int samples) +{ + SoDrawList drawlist = makeDrawList(commandCount, false, 1); + std::vector timings; + uint64_t checksum = 0; + for (int sample = 0; sample < samples; ++sample) { + drawlist.getCommand(sample % commandCount).objectId += 1; + const Clock::time_point start = Clock::now(); + drawlist.buildPickLUT(); + for (int probe = 0; probe < 32; ++probe) { + const uint32_t id = static_cast( + 1 + ((probe * 7919 + sample) % commandCount)); + const SoPickLUTEntry * entry = drawlist.resolvePickId(id); + if (entry) checksum += static_cast(entry->commandIndex + 1); + } + timings.push_back(elapsedMs(start)); + } + return summarize("PickingDenseSceneBenchmark", "picking", + "pick_lut_and_resolution", commandCount, timings, checksum); +} + +Result benchmarkSelection(int commandCount, int selectedCount, int samples) +{ + SoDrawList drawlist = makeDrawList(commandCount, false, 4); + std::vector timings; + uint64_t checksum = 0; + for (int sample = 0; sample < samples; ++sample) { + SoSelectionState & state = drawlist.getMutableSelectionState(); + const Clock::time_point start = Clock::now(); + state.selected.clear(); + state.selected.reserve(selectedCount); + for (int i = 0; i < selectedCount; ++i) { + SoSelectionTarget target; + target.commandIndex = (i * 7919 + sample) % commandCount; + target.objectId = drawlist.getCommand(target.commandIndex).objectId; + state.selected.push_back(target); + } + timings.push_back(elapsedMs(start)); + checksum += state.selected.size(); + } + return summarize("SelectionChurnBenchmark", "selection", + "selection_update", selectedCount, timings, checksum); +} + +Result benchmarkLifecycle(int frames, int commandCount) +{ + SoDrawList drawlist; + SoRenderPlanner planner; + SoRenderPlan plan; + std::vector timings; + timings.reserve(frames); + uint64_t checksum = 0; + for (int frame = 0; frame < frames; ++frame) { + const Clock::time_point start = Clock::now(); + drawlist.clear(); + drawlist.reserve(commandCount); + for (int i = 0; i < commandCount; ++i) { + SoRenderCommand command; + command.objectId = static_cast(frame + i + 1); + command.nodeId = static_cast(i + 1); + command.instanceId = static_cast(i + 1); + command.geometry.vertexCount = 3; + command.opacityClass = (i % 7 == 0) + ? SO_OPACITY_TRANSPARENT : SO_OPACITY_OPAQUE; + drawlist.addCommand(command); + } + drawlist.buildPickLUT(); + SoSelectionTarget selected; + selected.commandIndex = frame % commandCount; + drawlist.getMutableSelectionState().selected.push_back(selected); + planner.build(drawlist, plan); + timings.push_back(elapsedMs(start)); + checksum += drawlist.getGeneration(); + checksum += static_cast(plan.getNumOperations()); + if (!drawlist.resolvePickId(1)) { + std::cerr << "FAIL: lifecycle stress produced a stale pick table\n"; + std::exit(1); + } + } + return summarize("RenderLifecycleStressTest", "stress", + "frame_rebuild", commandCount, timings, checksum); +} + +std::string json(const std::vector & results, const Options & options) +{ + std::ostringstream out; + out << std::fixed << std::setprecision(6); + out << "{\n \"schema_version\": 1,\n" + << " \"mode\": \"" << (options.stress ? "stress" : + (options.smoke ? "smoke" : "benchmark")) << "\",\n" + << " \"time_unit\": \"ms\",\n \"benchmarks\": [\n"; + for (size_t i = 0; i < results.size(); ++i) { + const Result & result = results[i]; + out << " {\"name\": \"" << result.name + << "\", \"category\": \"" << result.category + << "\", \"phase\": \"" << result.phase + << "\", \"workload\": " << result.workload + << ", \"samples\": " << result.samples + << ", \"median_ms\": " << result.medianMs + << ", \"p95_ms\": " << result.p95Ms + << ", \"checksum\": " << result.checksum << "}"; + if (i + 1 != results.size()) out << ','; + out << '\n'; + } + out << " ]\n}\n"; + return out.str(); +} + +Options parseOptions(int argc, char ** argv) +{ + Options options; + for (int i = 1; i < argc; ++i) { + const std::string arg(argv[i]); + if (arg == "--smoke") options.smoke = true; + else if (arg == "--stress") options.stress = true; + else if (arg == "--frames" && i + 1 < argc) options.frames = std::atoi(argv[++i]); + else if (arg == "--samples" && i + 1 < argc) options.samples = std::atoi(argv[++i]); + else if (arg == "--output" && i + 1 < argc) options.output = argv[++i]; + else { + std::cerr << "Usage: CoinRenderBenchmarks [--smoke] [--stress] " + "[--frames N] [--samples N] [--output FILE]\n"; + std::exit(2); + } + } + if (options.smoke && options.stress) { + std::cerr << "--smoke and --stress are mutually exclusive\n"; + std::exit(2); + } + return options; +} + +} // namespace + +int main(int argc, char ** argv) +{ + const Options options = parseOptions(argc, argv); + SoDB::init(); + std::vector results; + if (options.stress) { + results.push_back(benchmarkLifecycle( + options.frames > 0 ? options.frames : 10000, options.smoke ? 16 : 256)); + } + else { + const int workload = options.smoke ? 32 : 10000; + const int traversalWorkload = options.smoke ? 8 : 1000; + const int samples = options.samples > 0 ? options.samples : (options.smoke ? 2 : 30); + results.push_back(benchmarkTraversal("RenderManyDrawsBenchmark", + traversalWorkload, samples, false)); + results.push_back(benchmarkTraversal("RenderMaterialChurnBenchmark", + traversalWorkload, samples, true)); + results.push_back(benchmarkPlanning("RenderTransparencyBenchmark", "render", + workload, samples, true, 1)); + results.push_back(benchmarkPicking(workload, samples)); + results.push_back(benchmarkPlanning("PickingDepthStackBenchmark", "picking", + workload, samples, false, options.smoke ? 4 : 128)); + results.push_back(benchmarkSelection(workload, + options.smoke ? 8 : 5000, samples)); + } + + const std::string document = json(results, options); + if (options.output.empty()) std::cout << document; + else { + std::ofstream output(options.output.c_str()); + if (!output) { + std::cerr << "Unable to open benchmark output: " << options.output << '\n'; + return 1; + } + output << document; + } + return benchmarkSink == static_cast(-1) ? 1 : 0; +} diff --git a/testsuite/CoinRenderGLBenchmarks.cpp b/testsuite/CoinRenderGLBenchmarks.cpp new file mode 100644 index 00000000000..4190fce77a3 --- /dev/null +++ b/testsuite/CoinRenderGLBenchmarks.cpp @@ -0,0 +1,565 @@ +#include "support/GLTestContext.h" +#include "support/RenderWorkloads.h" + +#include +#include +#include +#include +#include +#include +#if COIN_HAVE_LEGACY_GL_RENDERER +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace coin_test; +using Clock = std::chrono::steady_clock; + +struct Options { + bool smoke = false; + int samples = 0; + std::string output; +}; + +struct Measurement { + std::string workload; + std::string renderer; + std::string profile; + int semanticDraws = 0; + int samples = 0; + double cpuMedianMs = 0.0; + double cpuP95Ms = 0.0; + double gpuMedianMs = 0.0; + double gpuP95Ms = 0.0; + double completionMedianMs = 0.0; + double completionP95Ms = 0.0; + double drawListConstructionMedianMs = 0.0; + double primitiveGenerationMedianMs = 0.0; + double geometryPackingMedianMs = 0.0; + double commandEmissionMedianMs = 0.0; + double planConstructionMedianMs = 0.0; + double backendSubmissionMedianMs = 0.0; + double backendFrameSetupMedianMs = 0.0; + double backendResourcePreparationMedianMs = 0.0; + double backendCommandExecutionMedianMs = 0.0; + double backendSelectionMedianMs = 0.0; + double coldPickMs = 0.0; + double coldPickBufferUpdateMs = 0.0; + double coldPickTargetPreparationMs = 0.0; + double coldPickTargetRenderingMs = 0.0; + double refreshPickMs = 0.0; + double refreshPickBufferUpdateMs = 0.0; + double refreshPickTargetPreparationMs = 0.0; + double refreshPickTargetRenderingMs = 0.0; + double pickMedianMs = 0.0; + double pickP95Ms = 0.0; + double pickQueryMedianMs = 0.0; + double pickResultResolutionMedianMs = 0.0; + double pickDepthRenderingMedianMs = 0.0; + double pickDepthPeelingMedianMs = 0.0; + double pickReadbackMedianMs = 0.0; + double pickHitProcessingMedianMs = 0.0; + double pickTargetRestoreMedianMs = 0.0; + uint64_t pixelChecksum = 0; +}; + +double elapsedMs(const Clock::time_point & start) +{ + return std::chrono::duration(Clock::now() - start).count(); +} + +double percentile(std::vector values, double fraction) +{ + std::sort(values.begin(), values.end()); + size_t index = static_cast( + std::ceil(static_cast(values.size()) * fraction)) - 1; + return values[index]; +} + + +uint64_t checksumPixels(const std::vector & pixels) +{ + uint64_t hash = 1469598103934665603ULL; + bool nonBlack = false; + for (size_t i = 0; i < pixels.size(); ++i) { + hash ^= pixels[i]; + hash *= 1099511628211ULL; + if ((i % 4) != 3 && pixels[i] > 4) nonBlack = true; + } + return nonBlack ? hash : 0; +} + +bool checkTimerQueries() +{ +#ifdef GL_TIME_ELAPSED + GLuint query = 0; + glGenQueries(1, &query); + if (query == 0 || glGetError() != GL_NO_ERROR) return false; + glDeleteQueries(1, &query); + return true; +#else + return false; +#endif +} + +bool runVariant(GLTestProfile profile, + SoRenderManager::RenderPipeline pipeline, + const std::string & renderer, WorkloadKind workload, + int drawCount, int samples, Measurement & result, + std::string & unavailable) +{ + GLTestContextConfig config; + config.profile = profile; + config.major = 3; + config.minor = 3; + config.width = 256; + config.height = 256; + GLTestContext context; + if (!context.initialize(config)) { + unavailable = "requested OpenGL context is unavailable"; + return false; + } + if (!checkTimerQueries()) { + unavailable = "OpenGL timer queries are unavailable"; + return false; + } + + SoOrthographicCamera * camera = NULL; + SoSeparator * scene = makeScene(workload, drawCount, camera); + SbViewportRegion viewport(SbVec2s(256, 256)); + viewport.setViewportPixels(SbVec2s(0, 0), SbVec2s(256, 256)); +#if COIN_HAVE_LEGACY_GL_RENDERER + SoGLRenderAction legacyAction(viewport); + legacyAction.setCacheContext(context.contextId()); + legacyAction.setTransparencyType(SoGLRenderAction::SORTED_OBJECT_BLEND); +#endif + SoRenderManager manager; + manager.setViewportRegion(viewport); + manager.setSceneGraph(scene); + manager.setCamera(camera); + manager.setLightingMode(SoRenderManager::UNLIT); + manager.setRenderPipeline(pipeline); + manager.setRenderPhaseTimingEnabled( + pipeline == SoRenderManager::RenderPipeline::DRAW_LIST); +#if COIN_HAVE_LEGACY_GL_RENDERER + if (pipeline == SoRenderManager::RenderPipeline::LEGACY_GL) { + manager.setGLRenderAction(&legacyAction); + } +#endif + + for (int warmup = 0; warmup < 5; ++warmup) { + context.bindFramebuffer(); + manager.render(TRUE, TRUE); + } + glFinish(); + if (manager.getLastRenderResult().usedPipeline != pipeline || + !manager.getLastRenderResult().rendered) { + unavailable = "renderer manager fell back from the requested pipeline"; + camera->unref(); + scene->unref(); + return false; + } + + std::vector cpu; + std::vector gpu; + std::vector completion; + std::vector drawListConstruction; + std::vector primitiveGeneration; + std::vector geometryPacking; + std::vector commandEmission; + std::vector planConstruction; + std::vector backendSubmission; + std::vector backendFrameSetup; + std::vector backendResourcePreparation; + std::vector backendCommandExecution; + std::vector backendSelection; + GLuint query = 0; + glGenQueries(1, &query); + for (int sample = 0; sample < samples; ++sample) { + context.bindFramebuffer(); + const Clock::time_point totalStart = Clock::now(); + glBeginQuery(GL_TIME_ELAPSED, query); + const Clock::time_point cpuStart = Clock::now(); + manager.render(TRUE, TRUE); + cpu.push_back(elapsedMs(cpuStart)); + const SoRenderManager::RenderPhaseStatistics renderPhases = + manager.getRenderPhaseStatistics(); + drawListConstruction.push_back( + renderPhases.drawListConstructionNanoseconds / 1000000.0); + primitiveGeneration.push_back( + renderPhases.drawListPrimitiveGenerationNanoseconds / 1000000.0); + geometryPacking.push_back( + renderPhases.drawListGeometryPackingNanoseconds / 1000000.0); + commandEmission.push_back( + renderPhases.drawListCommandEmissionNanoseconds / 1000000.0); + planConstruction.push_back( + renderPhases.planConstructionNanoseconds / 1000000.0); + backendSubmission.push_back( + renderPhases.backendSubmissionNanoseconds / 1000000.0); + backendFrameSetup.push_back( + renderPhases.backendFrameSetupNanoseconds / 1000000.0); + backendResourcePreparation.push_back( + renderPhases.backendResourcePreparationNanoseconds / 1000000.0); + backendCommandExecution.push_back( + renderPhases.backendCommandExecutionNanoseconds / 1000000.0); + backendSelection.push_back( + renderPhases.backendSelectionNanoseconds / 1000000.0); + glEndQuery(GL_TIME_ELAPSED); + GLuint64 nanoseconds = 0; + glGetQueryObjectui64v(query, GL_QUERY_RESULT, &nanoseconds); + completion.push_back(elapsedMs(totalStart)); + gpu.push_back(static_cast(nanoseconds) / 1000000.0); + } + glDeleteQueries(1, &query); + + std::vector pick; + double coldPick = 0.0; + double coldPickBufferUpdate = 0.0; + double coldPickTargetPreparation = 0.0; + double coldPickTargetRendering = 0.0; + double refreshPick = 0.0; + double refreshPickBufferUpdate = 0.0; + double refreshPickTargetPreparation = 0.0; + double refreshPickTargetRendering = 0.0; + std::vector pickQuery; + std::vector pickResultResolution; + std::vector pickDepthRendering; + std::vector pickDepthPeeling; + std::vector pickReadback; + std::vector pickHitProcessing; + std::vector pickTargetRestore; + if (workload == WorkloadKind::DensePicking) { + SoSeparator * legacyPickRoot = NULL; +#if COIN_HAVE_LEGACY_GL_RENDERER + if (pipeline == SoRenderManager::RenderPipeline::LEGACY_GL) { + legacyPickRoot = new SoSeparator; + legacyPickRoot->ref(); + legacyPickRoot->addChild(camera); + legacyPickRoot->addChild(scene); + } +#endif + auto performPick = [&]() { + SoPickedPoint * picked = NULL; + SbBool hit = FALSE; +#if COIN_HAVE_LEGACY_GL_RENDERER + if (pipeline == SoRenderManager::RenderPipeline::LEGACY_GL) { + SoRayPickAction action(viewport); + action.setPoint(SbVec2s(128, 128)); + action.setRadius(4.0f); + action.apply(legacyPickRoot); + if (action.getPickedPoint()) { + picked = new SoPickedPoint(*action.getPickedPoint()); + hit = TRUE; + } + } + else +#endif + { + hit = manager.pickClosest(128, 128, 4, picked); + } + if (!hit || !picked) { + std::cerr << "FAIL: " << renderer + << " dense picking did not return a hit\n"; + std::exit(1); + } + delete picked; + }; + const Clock::time_point coldPickStart = Clock::now(); + performPick(); + coldPick = elapsedMs(coldPickStart); + if (pipeline == SoRenderManager::RenderPipeline::DRAW_LIST) { + const SoRenderManager::RenderPhaseStatistics phases = + manager.getRenderPhaseStatistics(); + coldPickBufferUpdate = + phases.pickBufferUpdateNanoseconds / 1000000.0; + coldPickTargetPreparation = + phases.backendPickTargetPreparationNanoseconds / 1000000.0; + coldPickTargetRendering = + phases.backendPickTargetRenderingNanoseconds / 1000000.0; + } + for (int sample = 0; sample < samples; ++sample) { + const Clock::time_point pickStart = Clock::now(); + performPick(); + pick.push_back(elapsedMs(pickStart)); + if (pipeline == SoRenderManager::RenderPipeline::DRAW_LIST) { + const SoRenderManager::RenderPhaseStatistics phases = + manager.getRenderPhaseStatistics(); + pickQuery.push_back(phases.pickQueryNanoseconds / 1000000.0); + pickResultResolution.push_back( + phases.pickResultResolutionNanoseconds / 1000000.0); + pickDepthRendering.push_back( + phases.backendPickDepthRenderingNanoseconds / 1000000.0); + pickDepthPeeling.push_back( + phases.backendPickDepthPeelingNanoseconds / 1000000.0); + pickReadback.push_back( + phases.backendPickReadbackNanoseconds / 1000000.0); + pickHitProcessing.push_back( + phases.backendPickHitProcessingNanoseconds / 1000000.0); + pickTargetRestore.push_back( + phases.backendPickTargetRestoreNanoseconds / 1000000.0); + } + } + scene->touch(); + context.bindFramebuffer(); + manager.render(TRUE, TRUE); + const Clock::time_point refreshPickStart = Clock::now(); + performPick(); + refreshPick = elapsedMs(refreshPickStart); + if (pipeline == SoRenderManager::RenderPipeline::DRAW_LIST) { + const SoRenderManager::RenderPhaseStatistics phases = + manager.getRenderPhaseStatistics(); + refreshPickBufferUpdate = + phases.pickBufferUpdateNanoseconds / 1000000.0; + refreshPickTargetPreparation = + phases.backendPickTargetPreparationNanoseconds / 1000000.0; + refreshPickTargetRendering = + phases.backendPickTargetRenderingNanoseconds / 1000000.0; + } + if (legacyPickRoot) legacyPickRoot->unref(); + } + + const uint64_t pixelChecksum = checksumPixels(context.readPixels()); + if (pixelChecksum == 0) { + std::cerr << "FAIL: " << renderer << ' ' << workloadName(workload) + << " rendered an empty frame\n"; + camera->unref(); + scene->unref(); + std::exit(1); + } + manager.releaseRenderBackendResources(); + manager.setCamera(NULL); + manager.setSceneGraph(NULL); + camera->unref(); + scene->unref(); + + result.workload = workloadName(workload); + result.renderer = renderer; + result.profile = profile == GLTestProfile::Core ? "core" : "compatibility"; + result.semanticDraws = drawCount; + result.samples = samples; + result.cpuMedianMs = percentile(cpu, 0.5); + result.cpuP95Ms = percentile(cpu, 0.95); + result.gpuMedianMs = percentile(gpu, 0.5); + result.gpuP95Ms = percentile(gpu, 0.95); + result.completionMedianMs = percentile(completion, 0.5); + result.completionP95Ms = percentile(completion, 0.95); + result.drawListConstructionMedianMs = + percentile(drawListConstruction, 0.5); + result.primitiveGenerationMedianMs = percentile(primitiveGeneration, 0.5); + result.geometryPackingMedianMs = percentile(geometryPacking, 0.5); + result.commandEmissionMedianMs = percentile(commandEmission, 0.5); + result.planConstructionMedianMs = percentile(planConstruction, 0.5); + result.backendSubmissionMedianMs = percentile(backendSubmission, 0.5); + result.backendFrameSetupMedianMs = percentile(backendFrameSetup, 0.5); + result.backendResourcePreparationMedianMs = + percentile(backendResourcePreparation, 0.5); + result.backendCommandExecutionMedianMs = + percentile(backendCommandExecution, 0.5); + result.backendSelectionMedianMs = percentile(backendSelection, 0.5); + if (!pick.empty()) { + result.coldPickMs = coldPick; + result.coldPickBufferUpdateMs = coldPickBufferUpdate; + result.coldPickTargetPreparationMs = coldPickTargetPreparation; + result.coldPickTargetRenderingMs = coldPickTargetRendering; + result.refreshPickMs = refreshPick; + result.refreshPickBufferUpdateMs = refreshPickBufferUpdate; + result.refreshPickTargetPreparationMs = refreshPickTargetPreparation; + result.refreshPickTargetRenderingMs = refreshPickTargetRendering; + result.pickMedianMs = percentile(pick, 0.5); + result.pickP95Ms = percentile(pick, 0.95); + if (!pickQuery.empty()) { + result.pickQueryMedianMs = percentile(pickQuery, 0.5); + result.pickResultResolutionMedianMs = + percentile(pickResultResolution, 0.5); + result.pickDepthRenderingMedianMs = percentile(pickDepthRendering, 0.5); + result.pickDepthPeelingMedianMs = percentile(pickDepthPeeling, 0.5); + result.pickReadbackMedianMs = percentile(pickReadback, 0.5); + result.pickHitProcessingMedianMs = percentile(pickHitProcessing, 0.5); + result.pickTargetRestoreMedianMs = percentile(pickTargetRestore, 0.5); + } + } + result.pixelChecksum = pixelChecksum; + return true; +} + +Options parseOptions(int argc, char ** argv) +{ + Options options; + for (int i = 1; i < argc; ++i) { + 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 == "--output" && i + 1 < argc) options.output = argv[++i]; + else { + std::cerr << "Usage: CoinRenderGLBenchmarks [--smoke] [--samples N] " + "[--output FILE]\n"; + std::exit(2); + } + } + return options; +} + +std::string toJson(const std::vector & results, + const std::vector & unavailable, + const Options & options) +{ + std::ostringstream out; + out << std::fixed << std::setprecision(6); + out << "{\n \"schema_version\": 4,\n \"mode\": \"" + << (options.smoke ? "smoke" : "benchmark") + << "\",\n \"time_unit\": \"ms\",\n \"benchmarks\": [\n"; + for (size_t i = 0; i < results.size(); ++i) { + const Measurement & r = results[i]; + out << " {\"workload\": \"" << r.workload + << "\", \"renderer\": \"" << r.renderer + << "\", \"profile\": \"" << r.profile + << "\", \"semantic_draws\": " << r.semanticDraws + << ", \"samples\": " << r.samples + << ", \"cpu_render_median_ms\": " << r.cpuMedianMs + << ", \"cpu_render_p95_ms\": " << r.cpuP95Ms + << ", \"gpu_median_ms\": " << r.gpuMedianMs + << ", \"gpu_p95_ms\": " << r.gpuP95Ms + << ", \"completion_median_ms\": " << r.completionMedianMs + << ", \"completion_p95_ms\": " << r.completionP95Ms + << ", \"drawlist_construction_median_ms\": " + << r.drawListConstructionMedianMs + << ", \"drawlist_primitive_generation_median_ms\": " + << r.primitiveGenerationMedianMs + << ", \"drawlist_geometry_packing_median_ms\": " + << r.geometryPackingMedianMs + << ", \"drawlist_command_emission_median_ms\": " + << r.commandEmissionMedianMs + << ", \"plan_construction_median_ms\": " + << r.planConstructionMedianMs + << ", \"backend_submission_median_ms\": " + << r.backendSubmissionMedianMs + << ", \"backend_frame_setup_median_ms\": " + << r.backendFrameSetupMedianMs + << ", \"backend_resource_preparation_median_ms\": " + << r.backendResourcePreparationMedianMs + << ", \"backend_command_execution_median_ms\": " + << r.backendCommandExecutionMedianMs + << ", \"backend_selection_median_ms\": " + << r.backendSelectionMedianMs + << ", \"cold_pick_ms\": " << r.coldPickMs + << ", \"cold_pick_buffer_update_ms\": " + << r.coldPickBufferUpdateMs + << ", \"cold_pick_target_preparation_ms\": " + << r.coldPickTargetPreparationMs + << ", \"cold_pick_target_rendering_ms\": " + << r.coldPickTargetRenderingMs + << ", \"refresh_pick_ms\": " << r.refreshPickMs + << ", \"refresh_pick_buffer_update_ms\": " + << r.refreshPickBufferUpdateMs + << ", \"refresh_pick_target_preparation_ms\": " + << r.refreshPickTargetPreparationMs + << ", \"refresh_pick_target_rendering_ms\": " + << r.refreshPickTargetRenderingMs + << ", \"pick_median_ms\": " << r.pickMedianMs + << ", \"pick_p95_ms\": " << r.pickP95Ms + << ", \"pick_query_median_ms\": " << r.pickQueryMedianMs + << ", \"pick_result_resolution_median_ms\": " + << r.pickResultResolutionMedianMs + << ", \"pick_depth_rendering_median_ms\": " + << r.pickDepthRenderingMedianMs + << ", \"pick_depth_peeling_median_ms\": " + << r.pickDepthPeelingMedianMs + << ", \"pick_readback_median_ms\": " << r.pickReadbackMedianMs + << ", \"pick_hit_processing_median_ms\": " + << r.pickHitProcessingMedianMs + << ", \"pick_target_restore_median_ms\": " + << r.pickTargetRestoreMedianMs + << ", \"pixel_checksum\": " << r.pixelChecksum << "}"; + if (i + 1 != results.size()) out << ','; + out << '\n'; + } + out << " ],\n \"unavailable\": ["; + for (size_t i = 0; i < unavailable.size(); ++i) { + if (i) out << ", "; + out << '\"' << unavailable[i] << '\"'; + } + out << "]\n}\n"; + return out.str(); +} + +} // namespace + +int main(int argc, char ** argv) +{ + const Options options = parseOptions(argc, argv); + SoDB::init(); + const int samples = options.samples > 0 ? options.samples : (options.smoke ? 2 : 30); + const int draws = options.smoke ? 8 : 500; + const WorkloadKind workloads[] = { + WorkloadKind::ManyDraws, + WorkloadKind::MaterialChurn, + WorkloadKind::Transparency, + WorkloadKind::DensePicking + }; + std::vector results; + std::vector unavailable; + for (size_t i = 0; i < sizeof(workloads) / sizeof(workloads[0]); ++i) { +#if COIN_HAVE_LEGACY_GL_RENDERER + Measurement legacy; + std::string reason; + if (runVariant(GLTestProfile::Compatibility, + SoRenderManager::RenderPipeline::LEGACY_GL, + "LegacyGL", workloads[i], draws, samples, legacy, reason)) { + results.push_back(legacy); + } + else unavailable.push_back(std::string(workloadName(workloads[i])) + + ":LegacyGL: " + reason); +#endif + Measurement compatibility; + std::string compatReason; + if (runVariant(GLTestProfile::Compatibility, + SoRenderManager::RenderPipeline::DRAW_LIST, + "DrawList", workloads[i], draws, samples, + compatibility, compatReason)) { + results.push_back(compatibility); + } + else unavailable.push_back(std::string(workloadName(workloads[i])) + + ":DrawList compatibility: " + compatReason); + + Measurement core; + std::string coreReason; + if (runVariant(GLTestProfile::Core, + SoRenderManager::RenderPipeline::DRAW_LIST, + "DrawList", workloads[i], draws, samples, core, coreReason)) { + results.push_back(core); + } + else unavailable.push_back(std::string(workloadName(workloads[i])) + + ":DrawList core: " + coreReason); + } + 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; +} diff --git a/testsuite/CoinRenderWorkloadViewer.cpp b/testsuite/CoinRenderWorkloadViewer.cpp new file mode 100644 index 00000000000..915d4b02016 --- /dev/null +++ b/testsuite/CoinRenderWorkloadViewer.cpp @@ -0,0 +1,209 @@ +#include "support/GLRenderTestSession.h" +#include "support/RenderWorkloadViewerController.h" +#include "support/RenderWorkloads.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace { + +struct Options { + coin_test::WorkloadKind workload = coin_test::WorkloadKind::FeatureRich; + int objects = 1000; + GLTestProfile profile = GLTestProfile::Core; + SoRenderManager::RenderPipeline pipeline = + SoRenderManager::RenderPipeline::DRAW_LIST; + int width = 1024; + int height = 768; + bool smoke = false; +}; + + +void usage() +{ + std::cout + << "Usage: CoinRenderWorkloadViewer [options]\n" + << " --workload NAME Synthetic workload to display\n" + << " --objects N Draws or occurrences (default: 1000)\n" + << " --renderer legacy|drawlist\n" + << " --gl-profile compat|core\n" + << " --size WIDTH HEIGHT\n\n" + << " --smoke Hidden finite-frame integration check\n\n" + << "Controls: wheel zoom, right/middle drag pan, left click select,\n" + << "M mutation playback, Space pause, R rebuild, C clear, Escape exit.\n"; +} + +bool parseOptions(int argc, char ** argv, Options & options) +{ + for (int i = 1; i < argc; ++i) { + const std::string arg(argv[i]); + if (arg == "--help" || arg == "-h") { + usage(); + return false; + } + if (arg == "--smoke") { + options.smoke = true; + options.workload = coin_test::WorkloadKind::SharedAssemblyRecipe; + options.objects = 24; + options.width = 256; + options.height = 256; + } + else if (arg == "--workload" && i + 1 < argc) { + if (!coin_test::parseWorkloadKind(argv[++i], options.workload)) { + std::cerr << "Unknown workload: " << argv[i] << '\n'; + return false; + } + } + else if (arg == "--objects" && i + 1 < argc) { + options.objects = std::atoi(argv[++i]); + } + else if (arg == "--renderer" && i + 1 < argc) { + const std::string renderer(argv[++i]); + if (renderer == "legacy") + options.pipeline = SoRenderManager::RenderPipeline::LEGACY_GL; + else if (renderer == "drawlist") + options.pipeline = SoRenderManager::RenderPipeline::DRAW_LIST; + else { + std::cerr << "Unknown renderer: " << renderer << '\n'; + return false; + } + } + else if (arg == "--gl-profile" && i + 1 < argc) { + const std::string profile(argv[++i]); + if (profile == "core") options.profile = GLTestProfile::Core; + else if (profile == "compat") options.profile = GLTestProfile::Compatibility; + else { + std::cerr << "Unknown GL profile: " << profile << '\n'; + return false; + } + } + else if (arg == "--size" && i + 2 < argc) { + options.width = std::atoi(argv[++i]); + options.height = std::atoi(argv[++i]); + } + else { + std::cerr << "Unknown or incomplete option: " << arg << '\n'; + return false; + } + } + if (options.objects <= 0 || options.width <= 0 || options.height <= 0) { + std::cerr << "Object count and window dimensions must be positive\n"; + return false; + } + if (options.pipeline == SoRenderManager::RenderPipeline::LEGACY_GL && + options.profile != GLTestProfile::Compatibility) { + std::cerr << "LegacyGL requires --gl-profile compat\n"; + return false; + } + return true; +} + +} // namespace + +int main(int argc, char ** argv) +{ + Options options; + if (!parseOptions(argc, argv, options)) + return argc > 1 && (std::string(argv[1]) == "--help" || + std::string(argv[1]) == "-h") ? 0 : 2; + +#if !COIN_HAVE_LEGACY_GL_RENDERER + if (options.pipeline == SoRenderManager::RenderPipeline::LEGACY_GL) { + std::cerr << "This build does not include LegacyGL\n"; + return 2; + } +#endif + + SoDB::init(); + GLRenderTestConfig renderConfig; + renderConfig.profile = options.profile; + renderConfig.pipeline = options.pipeline; + renderConfig.width = options.width; + renderConfig.height = options.height; + renderConfig.visible = !options.smoke; + renderConfig.vsync = !options.smoke; + GLRenderTestSession session; + if (!session.initialize(renderConfig)) return 1; + GLTestContext & context = session.context(); + SoRenderManager & manager = session.manager(); + + SoOrthographicCamera * camera = nullptr; + coin_test::SceneMutationHandles mutations; + SoSeparator * scene = coin_test::makeScene( + options.workload, options.objects, camera, &mutations); + session.setScene(scene, camera); + manager.setLightingMode(options.workload == coin_test::WorkloadKind::FeatureRich + ? SoRenderManager::LIT : SoRenderManager::UNLIT); + manager.setBackgroundColor(SbColor4f(0.055f, 0.065f, 0.08f, 1.0f)); + + coin_test::RenderWorkloadViewerController viewer( + session, *camera, mutations, options.width, options.height); + viewer.attach(); + if (options.smoke) { + viewer.setAnimationEnabled(true); + viewer.setCursorPosition(options.width * 0.5, options.height * 0.5); + } + + std::cout << "Viewing " << coin_test::workloadName(options.workload) + << " with " << options.objects << " objects\n" + << "Controls: wheel zoom, right/middle drag pan, left click select, " + "M mutate, Space pause, R rebuild, C clear\n"; + using ViewerClock = std::chrono::steady_clock; + ViewerClock::time_point statisticsStart = ViewerClock::now(); + int renderedFrames = 0; + int totalFrames = 0; + const int frameLimit = options.smoke ? 12 : 0; + while (!context.shouldClose() && + (frameLimit == 0 || totalFrames < frameLimit)) { + if (!viewer.pollEvents()) continue; + viewer.beforeRender(); + if (options.smoke && totalFrames == 3 && !viewer.resize(320, 240)) { + std::cerr << "Viewer smoke resize failed\n"; + session.setScene(nullptr, nullptr); + camera->unref(); + scene->unref(); + return 1; + } + if (options.smoke && totalFrames == 6) manager.invalidateDrawList(); + + session.render(); + viewer.afterRender( + options.pipeline == SoRenderManager::RenderPipeline::DRAW_LIST); + + context.present(); + ++renderedFrames; + ++totalFrames; + const ViewerClock::time_point now = ViewerClock::now(); + const double reportSeconds = std::chrono::duration( + now - statisticsStart).count(); + if (reportSeconds >= 1.0) { + std::cout << renderedFrames / reportSeconds << " fps\n"; + statisticsStart = now; + renderedFrames = 0; + } + } + + if (options.smoke) { + if (!manager.getLastRenderResult().rendered || + !viewer.hasHoverTarget()) { + std::cerr << "Viewer smoke did not complete render and hover checks\n"; + session.setScene(nullptr, nullptr); + camera->unref(); + scene->unref(); + return 1; + } + } + + session.setScene(nullptr, nullptr); + camera->unref(); + scene->unref(); + return 0; +} diff --git a/testsuite/DrawListManagerTest.cpp b/testsuite/DrawListManagerTest.cpp index f2b3e2fe71a..769017041ea 100644 --- a/testsuite/DrawListManagerTest.cpp +++ b/testsuite/DrawListManagerTest.cpp @@ -269,15 +269,14 @@ runTest() manager.setCamera(camera); manager.setRenderPipeline(SoRenderManager::RenderPipeline::DRAW_LIST); - if (!manager.isRenderPipelineAvailable( - SoRenderManager::RenderPipeline::DRAW_LIST)) { - std::cerr << "FAIL: DrawList was unavailable in a valid core context" - << std::endl; + if (manager.isRenderPhaseTimingEnabled()) { + std::cerr << "FAIL: render phase timing was enabled by default" << std::endl; result = 1; } - if (manager.getLastRenderResult().rendered) { - std::cerr << "FAIL: manager reported a render before rendering" - << std::endl; + manager.setRenderPhaseTimingEnabled(TRUE); + + if (!manager.isRenderPipelineAvailable(SoRenderManager::RenderPipeline::DRAW_LIST)) { + std::cerr << "FAIL: DrawList was unavailable in a valid core context" << std::endl; result = 1; } @@ -289,17 +288,15 @@ runTest() std::cerr << "FAIL: DrawList manager callbacks were not paired exactly once" << std::endl; result = 1; } - const SoRenderManager::RenderResult & renderResult = - manager.getLastRenderResult(); - if (!renderResult.rendered || - renderResult.requestedPipeline != - SoRenderManager::RenderPipeline::DRAW_LIST || - renderResult.usedPipeline != - SoRenderManager::RenderPipeline::DRAW_LIST || - renderResult.fallbackReason != - SoRenderManager::RenderResult::FallbackReason::NONE) { - std::cerr << "FAIL: manager did not report the retained render outcome" - << std::endl; + const SoRenderManager::RenderPhaseStatistics renderPhases = + manager.getRenderPhaseStatistics(); + if (renderPhases.drawListConstructionNanoseconds == 0 || + renderPhases.planConstructionNanoseconds == 0 || + renderPhases.backendSubmissionNanoseconds == 0 || + renderPhases.backendFrameSetupNanoseconds == 0 || + renderPhases.backendResourcePreparationNanoseconds == 0 || + renderPhases.backendCommandExecutionNanoseconds == 0) { + std::cerr << "FAIL: retained render phases were not measured" << std::endl; result = 1; } if (countNonBlack(context) == 0) { @@ -319,6 +316,20 @@ runTest() result = 1; } delete closest; + const SoRenderManager::RenderPhaseStatistics firstPickPhases = + manager.getRenderPhaseStatistics(); + if (firstPickPhases.pickBufferRefreshes != 1 || + firstPickPhases.pickBufferUpdateNanoseconds == 0 || + firstPickPhases.pickQueryNanoseconds == 0 || + firstPickPhases.pickResultResolutionNanoseconds == 0 || + firstPickPhases.backendPickTargetPreparationNanoseconds == 0 || + firstPickPhases.backendPickTargetRenderingNanoseconds == 0 || + firstPickPhases.backendPickDepthRenderingNanoseconds == 0 || + firstPickPhases.backendPickReadbackNanoseconds == 0 || + firstPickPhases.backendPickTargetRestoreNanoseconds == 0) { + std::cerr << "FAIL: retained pick phases were not measured" << std::endl; + result = 1; + } SoPickedPointList stack; if (!manager.pickDepthStack(16, 16, 0, 8, stack) || @@ -329,6 +340,24 @@ runTest() << std::endl; result = 1; } + const SoRenderManager::RenderPhaseStatistics reusedPickPhases = + manager.getRenderPhaseStatistics(); + if (reusedPickPhases.pickBufferRefreshes != 0 || + reusedPickPhases.pickBufferUpdateNanoseconds != 0 || + reusedPickPhases.pickQueryNanoseconds == 0) { + std::cerr << "FAIL: reused pick buffer phases were misreported" << std::endl; + result = 1; + } + + manager.setRenderPhaseTimingEnabled(FALSE); + const SoRenderManager::RenderPhaseStatistics disabledPhases = + manager.getRenderPhaseStatistics(); + if (disabledPhases.drawListConstructionNanoseconds != 0 || + disabledPhases.pickQueryNanoseconds != 0) { + std::cerr << "FAIL: disabling render phase timing did not reset statistics" + << std::endl; + result = 1; + } manager.setRenderMode(SoRenderManager::WIREFRAME); manager.render(TRUE, TRUE); diff --git a/testsuite/RENDER_BENCHMARKS.md b/testsuite/RENDER_BENCHMARKS.md new file mode 100644 index 00000000000..a78d9ce1da2 --- /dev/null +++ b/testsuite/RENDER_BENCHMARKS.md @@ -0,0 +1,109 @@ +# Renderer benchmarks + +Configure Coin with both test and benchmark targets enabled: + +```sh +cmake -S . -B build-bench \ + -DCOIN_BUILD_TESTS=ON \ + -DCOIN_BUILD_BENCHMARKS=ON \ + -DCMAKE_BUILD_TYPE=Release +cmake --build build-bench --target CoinRenderBenchmarks +``` + +The benchmark executable writes a stable JSON schema containing median and +p95 timings, workload sizes, sample counts, and sanity checksums: + +```sh +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 +``` + +## Viewing generated workloads + +`CoinRenderWorkloadViewer` displays the same deterministic scene graphs used +by the hardware benchmarks. This makes scene construction, camera framing, +transparency, and retained batching behavior inspectable without maintaining +separate visual copies of the workloads. + +```sh +build-bench/bin/CoinRenderWorkloadViewer \ + --workload shared_assembly_recipe --objects 10000 \ + --renderer drawlist --gl-profile core + +build-bench/bin/CoinRenderWorkloadViewer \ + --workload feature_rich_scene_end_to_end --objects 1000 \ + --renderer legacy --gl-profile compat +``` + +Available workload names are `many_small_draws`, `many_material_changes`, +`transparent_sorting`, `single_pick_dense_scene`, +`feature_rich_scene_end_to_end`, `shared_assembly_expanded`, +`shared_assembly_sources`, and `shared_assembly_recipe`. Close the window or +press Escape to exit. Use the mouse wheel to zoom, right- or middle-drag to +pan. `M` toggles mutation playback, Space pauses rendering, and `R` forces a +retained rebuild. The viewer prints frame rate once per second. LegacyGL requires a +compatibility-profile build and `--gl-profile compat`; DrawList supports +compatibility and core profiles. The DrawList path also performs a synchronous +hover pick at the cursor so the interaction path is exercised. + +CTest also registers `CoinRenderWorkloadViewerSmoke`. It uses a hidden core +context to render a small shared-recipe scene, resize its framebuffer, animate +one occurrence, force a retained rebuild, and complete a hover pick. This +checks viewer integration without opening a window or +introducing a timing threshold. + +`RenderWorkloadParityTest` renders the expanded, shared-source, and +shared-recipe assembly representations through the same DrawList core context. +It requires visible, equivalent pixel output while independently checking +the mutation handles exposed by each representation. A failure reports the mismatched-pixel count +and maximum channel difference rather than maintaining screenshot baselines. + +The viewer and parity test share `GLRenderTestSession`. The session accepts an +arbitrary scene and camera and centralizes context/profile selection, +LegacyGL or DrawList setup, viewport resize propagation, rendering, readback, +and teardown. Workload generation and viewer interaction remain +separate so other GL integration tests can reuse the session without depending +on benchmark scenes or UI policy. + +CTest exposes a tiny non-gating smoke run and a separate 10,000-frame stress +run. Timing results are informational; neither test applies wall-clock pass/fail +thresholds. + +```sh +ctest --test-dir build-bench -L benchmark +ctest --test-dir build-bench -L stress +``` + +When GLFW and OpenGL 3.3 are available, `CoinRenderGLBenchmarks` runs the same +semantic scenes through DrawList compatibility and core contexts. Builds with +`COIN_BUILD_LEGACY_GL_RENDERER=ON` additionally run LegacyGL in a compatibility +context. It reports CPU render-call time, GPU timer-query time, end-to-end GPU +completion time, dense-scene closest-pick latency, and a non-empty-frame pixel checksum. Unsupported profiles are +reported in the JSON `unavailable` array rather than being mistaken for results. +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, +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 +total draw-list construction. Backend submission is divided into frame setup, +resource preparation, command execution, and selection overlays. Picking reports +target preparation and rendering, depth rendering and peeling, readback, hit +processing, target restoration, and final scene-result resolution. + +Timing remains disabled for normal `SoRenderManager` users, so clock reads do +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 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 +resource rebuilds. GPU submission/completion and LegacyGL/DrawList A/B runs +belong in an optional GL-backed extension so controlled runners can select the +required compatibility or core context explicitly. `CoinRenderGLBenchmarks` +provides that controlled A/B layer; the dependency-free executable remains the +preferred benchmark smoke test on machines without suitable GL contexts. diff --git a/testsuite/RenderWorkloadParityTest.cpp b/testsuite/RenderWorkloadParityTest.cpp new file mode 100644 index 00000000000..8c62156e175 --- /dev/null +++ b/testsuite/RenderWorkloadParityTest.cpp @@ -0,0 +1,154 @@ +#include "support/GLRenderTestSession.h" +#include "support/RenderWorkloads.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace { + +constexpr int viewportSize = 256; +constexpr int occurrenceCount = 64; + +struct RenderedWorkload { + std::vector pixels; +}; + +int skip(const char * reason) +{ + std::cout << "SKIP: " << reason << std::endl; + return 77; +} + +bool hasVisiblePixel(const std::vector & pixels) +{ + for (size_t offset = 0; offset + 3 < pixels.size(); offset += 4) { + if (pixels[offset] > 4 || pixels[offset + 1] > 4 || + pixels[offset + 2] > 4) return true; + } + return false; +} + +bool renderWorkload(GLRenderTestSession & session, coin_test::WorkloadKind kind, + RenderedWorkload & result) +{ + coin_test::SceneMutationHandles mutations; + SoOrthographicCamera * camera = nullptr; + SoSeparator * scene = coin_test::makeScene( + kind, occurrenceCount, camera, &mutations); + + const int definitions = coin_test::assemblyDefinitionCount(occurrenceCount); + const bool handlesValid = + mutations.transforms.size() == occurrenceCount && + mutations.materials.size() == occurrenceCount && + mutations.coordinates.size() == occurrenceCount && + mutations.definitionCoordinates.size() == static_cast(definitions); + if (!handlesValid) { + std::cerr << "FAIL: " << coin_test::workloadName(kind) + << " exposed inconsistent mutation handles" << std::endl; + camera->unref(); + scene->unref(); + return false; + } + + session.setScene(scene, camera); + session.manager().setLightingMode(SoRenderManager::UNLIT); + session.render(); + session.render(); + result.pixels = session.readPixels(); + + const bool structureValid = + session.manager().getLastRenderResult().rendered && + session.manager().getLastRenderResult().usedPipeline == + SoRenderManager::RenderPipeline::DRAW_LIST && + hasVisiblePixel(result.pixels); + if (!structureValid) { + std::cerr << "FAIL: " << coin_test::workloadName(kind) + << " did not produce visible DrawList output" << std::endl; + } + + session.setScene(nullptr, nullptr); + camera->unref(); + scene->unref(); + return structureValid; +} + +bool comparePixels(const RenderedWorkload & expected, + const RenderedWorkload & actual, + const char * actualName) +{ + if (expected.pixels.size() != actual.pixels.size()) { + std::cerr << "FAIL: " << actualName << " produced a different image size" + << std::endl; + return false; + } + size_t mismatchedPixels = 0; + int maximumChannelDifference = 0; + for (size_t offset = 0; offset < expected.pixels.size(); offset += 4) { + bool pixelMismatch = false; + for (size_t channel = 0; channel < 4; ++channel) { + const int difference = std::abs( + static_cast(expected.pixels[offset + channel]) - + static_cast(actual.pixels[offset + channel])); + maximumChannelDifference = std::max(maximumChannelDifference, difference); + if (difference > 1) pixelMismatch = true; + } + if (pixelMismatch) ++mismatchedPixels; + } + if (mismatchedPixels != 0) { + std::cerr << "FAIL: " << actualName << " differs from " + << coin_test::workloadName( + coin_test::WorkloadKind::SharedAssemblyExpanded) + << " at " << mismatchedPixels << " pixels; maximum channel " + "difference is " << maximumChannelDifference << std::endl; + return false; + } + return true; +} + +} // namespace + +static int runTest() +{ + GLRenderTestConfig config; + config.profile = GLTestProfile::Core; + config.width = viewportSize; + config.height = viewportSize; + GLRenderTestSession session; + if (!session.initialize(config)) { + return skip("core GLFW OpenGL context is unavailable"); + } + + const coin_test::WorkloadKind kinds[] = { + coin_test::WorkloadKind::SharedAssemblyExpanded, + coin_test::WorkloadKind::SharedAssemblySources, + coin_test::WorkloadKind::SharedAssemblyRecipe + }; + RenderedWorkload rendered[3]; + bool valid = true; + for (int i = 0; i < 3; ++i) + valid = renderWorkload(session, kinds[i], rendered[i]) && valid; + if (valid) { + valid = comparePixels(rendered[0], rendered[1], + coin_test::workloadName(kinds[1])) && valid; + valid = comparePixels(rendered[0], rendered[2], + coin_test::workloadName(kinds[2])) && valid; + } + + return valid ? 0 : 1; +} + +int main() +{ + SoDB::init(); + const int result = runTest(); + SoDB::finish(); + return result; +} diff --git a/testsuite/support/GLRenderTestSession.cpp b/testsuite/support/GLRenderTestSession.cpp new file mode 100644 index 00000000000..fd81ef5e055 --- /dev/null +++ b/testsuite/support/GLRenderTestSession.cpp @@ -0,0 +1,153 @@ +#include "GLRenderTestSession.h" + +#include +#include +#if COIN_HAVE_LEGACY_GL_RENDERER +#include +#endif + +#include + +struct GLRenderTestSession::Impl { + GLTestContext context; + SoRenderManager manager; +#if COIN_HAVE_LEGACY_GL_RENDERER + SoGLRenderAction * legacyAction = nullptr; +#endif + bool initialized = false; + int width = 0; + int height = 0; +}; + +GLRenderTestSession::GLRenderTestSession() + : impl_(new Impl) +{ +} + +GLRenderTestSession::~GLRenderTestSession() +{ + this->shutdown(); + delete impl_; +} + +bool GLRenderTestSession::initialize(const GLRenderTestConfig & config) +{ + if (impl_->initialized) return true; + if (config.pipeline == SoRenderManager::RenderPipeline::LEGACY_GL && + config.profile != GLTestProfile::Compatibility) { + std::cerr << "LegacyGL requires a compatibility OpenGL profile" + << std::endl; + return false; + } +#if !COIN_HAVE_LEGACY_GL_RENDERER + if (config.pipeline == SoRenderManager::RenderPipeline::LEGACY_GL) { + std::cerr << "LegacyGL is unavailable in this build" << std::endl; + return false; + } +#endif + + GLTestContextConfig contextConfig; + contextConfig.profile = config.profile; + contextConfig.width = config.width; + contextConfig.height = config.height; + contextConfig.visible = config.visible; + contextConfig.vsync = config.vsync; + if (!impl_->context.initialize(contextConfig)) return false; + + const SbVec2s size(static_cast(config.width), + static_cast(config.height)); + const SbViewportRegion viewport(size); + impl_->manager.setViewportRegion(viewport); + impl_->manager.setRenderPipeline(config.pipeline); +#if COIN_HAVE_LEGACY_GL_RENDERER + if (config.pipeline == SoRenderManager::RenderPipeline::LEGACY_GL) { + impl_->legacyAction = new SoGLRenderAction(viewport); + impl_->legacyAction->setCacheContext(impl_->context.contextId()); + impl_->legacyAction->setTransparencyType( + SoGLRenderAction::SORTED_OBJECT_BLEND); + impl_->manager.setGLRenderAction(impl_->legacyAction); + } +#endif + impl_->width = config.width; + impl_->height = config.height; + impl_->initialized = true; + return true; +} + +void GLRenderTestSession::shutdown() +{ + impl_->manager.setCamera(nullptr); + impl_->manager.setSceneGraph(nullptr); +#if COIN_HAVE_LEGACY_GL_RENDERER + if (impl_->legacyAction) { + impl_->manager.setGLRenderAction(nullptr); + delete impl_->legacyAction; + impl_->legacyAction = nullptr; + } +#endif + impl_->context.shutdown(); + impl_->initialized = false; + impl_->width = 0; + impl_->height = 0; +} + +void GLRenderTestSession::setScene(SoNode * scene, SoCamera * camera) +{ + if (!scene && !camera) { + impl_->manager.setCamera(nullptr); + impl_->manager.setSceneGraph(nullptr); + } + else { + impl_->manager.setSceneGraph(scene); + impl_->manager.setCamera(camera); + } +} + +bool GLRenderTestSession::render(const bool clearColor, const bool clearDepth) +{ + if (!impl_->initialized) return false; + impl_->context.bindFramebuffer(); + impl_->manager.render(clearColor ? TRUE : FALSE, + clearDepth ? TRUE : FALSE); + return impl_->manager.getLastRenderResult().rendered != FALSE; +} + +bool GLRenderTestSession::resize(const int width, const int height) +{ + if (!impl_->initialized || + !impl_->context.resizeFramebuffer(width, height)) return false; + const SbVec2s size(static_cast(width), static_cast(height)); + impl_->manager.setViewportRegion(SbViewportRegion(size)); +#if COIN_HAVE_LEGACY_GL_RENDERER + if (impl_->legacyAction) + impl_->legacyAction->setViewportRegion(SbViewportRegion(size)); +#endif + impl_->width = width; + impl_->height = height; + return true; +} + +std::vector GLRenderTestSession::readPixels() const +{ + return impl_->context.readPixels(); +} + +bool GLRenderTestSession::initialized() const +{ + return impl_->initialized; +} + +GLTestContext & GLRenderTestSession::context() +{ + return impl_->context; +} + +SoRenderManager & GLRenderTestSession::manager() +{ + return impl_->manager; +} + +const SoRenderManager & GLRenderTestSession::manager() const +{ + return impl_->manager; +} diff --git a/testsuite/support/GLRenderTestSession.h b/testsuite/support/GLRenderTestSession.h new file mode 100644 index 00000000000..1beb54d1152 --- /dev/null +++ b/testsuite/support/GLRenderTestSession.h @@ -0,0 +1,49 @@ +#ifndef COIN_TEST_GLRENDERTESTSESSION_H +#define COIN_TEST_GLRENDERTESTSESSION_H + +#include "GLTestContext.h" + +#include + +#include +#include + +class SoCamera; +class SoNode; + +struct GLRenderTestConfig { + GLTestProfile profile = GLTestProfile::Core; + SoRenderManager::RenderPipeline pipeline = + SoRenderManager::RenderPipeline::DRAW_LIST; + int width = 64; + int height = 64; + bool visible = false; + bool vsync = false; +}; + +class GLRenderTestSession { +public: + GLRenderTestSession(); + ~GLRenderTestSession(); + + GLRenderTestSession(const GLRenderTestSession &) = delete; + GLRenderTestSession & operator=(const GLRenderTestSession &) = delete; + + bool initialize(const GLRenderTestConfig & config); + void shutdown(); + void setScene(SoNode * scene, SoCamera * camera); + bool render(bool clearColor = true, bool clearDepth = true); + bool resize(int width, int height); + std::vector readPixels() const; + + bool initialized() const; + GLTestContext & context(); + SoRenderManager & manager(); + const SoRenderManager & manager() const; + +private: + struct Impl; + Impl * impl_; +}; + +#endif // COIN_TEST_GLRENDERTESTSESSION_H diff --git a/testsuite/support/GLTestContext.cpp b/testsuite/support/GLTestContext.cpp index ae24cda0114..848c0af7238 100644 --- a/testsuite/support/GLTestContext.cpp +++ b/testsuite/support/GLTestContext.cpp @@ -3,6 +3,7 @@ #include #include +#include namespace { @@ -57,7 +58,7 @@ GLTestContext::initialize(const GLTestContextConfig & config) ++glfwUsers; glfwUser_ = true; - glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); + glfwWindowHint(GLFW_VISIBLE, config.visible ? GLFW_TRUE : GLFW_FALSE); glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, config.major); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, config.minor); @@ -83,12 +84,14 @@ GLTestContext::initialize(const GLTestContextConfig & config) profile_ = config.profile; glfwMakeContextCurrent(window_); - glfwSwapInterval(0); + glfwSwapInterval(config.vsync ? 1 : 0); - GLint major = 0; - GLint minor = 0; - glGetIntegerv(GL_MAJOR_VERSION, &major); - glGetIntegerv(GL_MINOR_VERSION, &minor); + contextId_ = nextContextId++; + const cc_glglue * glue = cc_glglue_instance(contextId_); + unsigned int major = 0; + unsigned int minor = 0; + unsigned int release = 0; + cc_glglue_glversion(glue, &major, &minor, &release); majorVersion_ = static_cast(major); minorVersion_ = static_cast(minor); if (!versionAtLeast(majorVersion_, minorVersion_, config.major, config.minor)) { @@ -111,11 +114,10 @@ GLTestContext::initialize(const GLTestContextConfig & config) } #endif - if (!framebuffer_.initialize(config.width, config.height)) { + if (!framebuffer_.initialize(glue, config.width, config.height)) { this->shutdown(); return false; } - contextId_ = nextContextId++; this->bindFramebuffer(); glDisable(GL_SCISSOR_TEST); glClearColor(0.0f, 0.0f, 0.0f, 0.0f); @@ -155,6 +157,18 @@ GLTestContext::makeCurrent() return glfwGetCurrentContext() == window_; } +bool +GLTestContext::resizeFramebuffer(const int width, const int height) +{ + if (window_ == NULL || width <= 0 || height <= 0) return false; + glfwMakeContextCurrent(window_); + if (!framebuffer_.initialize(cc_glglue_instance(contextId_), width, height)) { + return false; + } + this->bindFramebuffer(); + return true; +} + void GLTestContext::bindFramebuffer() { @@ -162,6 +176,27 @@ GLTestContext::bindFramebuffer() framebuffer_.bind(); } +void +GLTestContext::present() +{ + if (window_ == NULL) return; + glfwMakeContextCurrent(window_); + framebuffer_.blitToDefault(); + glfwSwapBuffers(window_); +} + +void GLTestContext::pollEvents() +{ + glfwPollEvents(); + if (window_ && glfwGetKey(window_, GLFW_KEY_ESCAPE) == GLFW_PRESS) + glfwSetWindowShouldClose(window_, GLFW_TRUE); +} + +bool GLTestContext::shouldClose() const +{ + return window_ == NULL || glfwWindowShouldClose(window_) != 0; +} + std::vector GLTestContext::readPixels() const { diff --git a/testsuite/support/GLTestContext.h b/testsuite/support/GLTestContext.h index 7699606e8f2..8f7937b8791 100644 --- a/testsuite/support/GLTestContext.h +++ b/testsuite/support/GLTestContext.h @@ -20,6 +20,8 @@ struct GLTestContextConfig { int minor = 3; int width = 64; int height = 64; + bool visible = false; + bool vsync = false; }; class GLTestContext { @@ -34,7 +36,12 @@ class GLTestContext { void shutdown(); bool makeCurrent(); + bool resizeFramebuffer(int width, int height); void bindFramebuffer(); + void present(); + void pollEvents(); + bool shouldClose() const; + GLFWwindow * window() const { return window_; } std::vector readPixels() const; bool isCoreProfile() const { return profile_ == GLTestProfile::Core; } diff --git a/testsuite/support/GLTestFramebuffer.cpp b/testsuite/support/GLTestFramebuffer.cpp index bab20bb6e91..da37e55b725 100644 --- a/testsuite/support/GLTestFramebuffer.cpp +++ b/testsuite/support/GLTestFramebuffer.cpp @@ -2,8 +2,37 @@ #include +#include + +#ifndef APIENTRY +#define APIENTRY +#endif + +namespace { + +typedef void (APIENTRY * CoinTestBlitFramebufferProc)( + GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, + GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, + GLbitfield mask, GLenum filter); + +CoinTestBlitFramebufferProc +getBlitFramebufferProc(const cc_glglue * glue) +{ + CoinTestBlitFramebufferProc proc = + reinterpret_cast( + cc_glglue_getprocaddress(glue, "glBlitFramebuffer")); + if (proc == NULL) { + proc = reinterpret_cast( + cc_glglue_getprocaddress(glue, "glBlitFramebufferEXT")); + } + return proc; +} + +} // namespace + GLTestFramebuffer::GLTestFramebuffer() - : framebuffer_(0), + : glue_(NULL), + framebuffer_(0), colorTexture_(0), depthRenderbuffer_(0), width_(0), @@ -17,7 +46,8 @@ GLTestFramebuffer::~GLTestFramebuffer() } bool -GLTestFramebuffer::initialize(const int width, const int height) +GLTestFramebuffer::initialize(const cc_glglue * glue, + const int width, const int height) { if (width <= 0 || height <= 0) { std::cerr << "Invalid GL test framebuffer size: " << width << "x" @@ -26,11 +56,12 @@ GLTestFramebuffer::initialize(const int width, const int height) } this->shutdown(); + glue_ = glue; width_ = width; height_ = height; - glGenFramebuffers(1, &framebuffer_); - glBindFramebuffer(GL_FRAMEBUFFER, framebuffer_); + cc_glglue_glGenFramebuffers(glue_, 1, &framebuffer_); + cc_glglue_glBindFramebuffer(glue_, GL_FRAMEBUFFER, framebuffer_); glGenTextures(1, &colorTexture_); glBindTexture(GL_TEXTURE_2D, colorTexture_); @@ -40,28 +71,31 @@ GLTestFramebuffer::initialize(const int width, const int height) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width_, height_, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, - GL_TEXTURE_2D, colorTexture_, 0); + cc_glglue_glFramebufferTexture2D(glue_, GL_FRAMEBUFFER, + GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, + colorTexture_, 0); - glGenRenderbuffers(1, &depthRenderbuffer_); - glBindRenderbuffer(GL_RENDERBUFFER, depthRenderbuffer_); - glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, - width_, height_); - glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, - GL_RENDERBUFFER, depthRenderbuffer_); + cc_glglue_glGenRenderbuffers(glue_, 1, &depthRenderbuffer_); + cc_glglue_glBindRenderbuffer(glue_, GL_RENDERBUFFER, depthRenderbuffer_); + cc_glglue_glRenderbufferStorage(glue_, GL_RENDERBUFFER, + GL_DEPTH_COMPONENT24, width_, height_); + cc_glglue_glFramebufferRenderbuffer(glue_, GL_FRAMEBUFFER, + GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, + depthRenderbuffer_); - const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + const GLenum status = cc_glglue_glCheckFramebufferStatus( + glue_, GL_FRAMEBUFFER); if (status != GL_FRAMEBUFFER_COMPLETE) { std::cerr << "GL test framebuffer is incomplete: 0x" << std::hex << status << std::dec << std::endl; - glBindFramebuffer(GL_FRAMEBUFFER, 0); + cc_glglue_glBindFramebuffer(glue_, GL_FRAMEBUFFER, 0); this->shutdown(); return false; } glBindTexture(GL_TEXTURE_2D, 0); - glBindRenderbuffer(GL_RENDERBUFFER, 0); - glBindFramebuffer(GL_FRAMEBUFFER, 0); + cc_glglue_glBindRenderbuffer(glue_, GL_RENDERBUFFER, 0); + cc_glglue_glBindFramebuffer(glue_, GL_FRAMEBUFFER, 0); return true; } @@ -69,7 +103,7 @@ void GLTestFramebuffer::shutdown() { if (depthRenderbuffer_ != 0) { - glDeleteRenderbuffers(1, &depthRenderbuffer_); + cc_glglue_glDeleteRenderbuffers(glue_, 1, &depthRenderbuffer_); depthRenderbuffer_ = 0; } if (colorTexture_ != 0) { @@ -77,20 +111,34 @@ GLTestFramebuffer::shutdown() colorTexture_ = 0; } if (framebuffer_ != 0) { - glDeleteFramebuffers(1, &framebuffer_); + cc_glglue_glDeleteFramebuffers(glue_, 1, &framebuffer_); framebuffer_ = 0; } width_ = 0; height_ = 0; + glue_ = NULL; } void GLTestFramebuffer::bind() const { - glBindFramebuffer(GL_FRAMEBUFFER, framebuffer_); + cc_glglue_glBindFramebuffer(glue_, GL_FRAMEBUFFER, framebuffer_); glViewport(0, 0, width_, height_); } +void +GLTestFramebuffer::blitToDefault() const +{ + if (!this->isInitialized()) return; + cc_glglue_glBindFramebuffer(glue_, GL_READ_FRAMEBUFFER, framebuffer_); + cc_glglue_glBindFramebuffer(glue_, GL_DRAW_FRAMEBUFFER, 0); + CoinTestBlitFramebufferProc proc = getBlitFramebufferProc(glue_); + if (proc != NULL) { + proc(0, 0, width_, height_, 0, 0, width_, height_, + GL_COLOR_BUFFER_BIT, GL_NEAREST); + } +} + std::vector GLTestFramebuffer::readPixels() const { @@ -112,7 +160,7 @@ GLTestFramebuffer::readPixels() const glGetIntegerv(GL_PACK_IMAGE_HEIGHT, &packImageHeight); glGetIntegerv(GL_PACK_SKIP_IMAGES, &packSkipImages); glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &pixelPackBuffer); - glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + cc_glglue_glBindBuffer(glue_, GL_PIXEL_PACK_BUFFER, 0); glPixelStorei(GL_PACK_ALIGNMENT, 1); glPixelStorei(GL_PACK_ROW_LENGTH, 0); glPixelStorei(GL_PACK_SKIP_ROWS, 0); @@ -128,6 +176,7 @@ GLTestFramebuffer::readPixels() const glPixelStorei(GL_PACK_SKIP_PIXELS, packSkipPixels); glPixelStorei(GL_PACK_IMAGE_HEIGHT, packImageHeight); glPixelStorei(GL_PACK_SKIP_IMAGES, packSkipImages); - glBindBuffer(GL_PIXEL_PACK_BUFFER, static_cast(pixelPackBuffer)); + cc_glglue_glBindBuffer(glue_, GL_PIXEL_PACK_BUFFER, + static_cast(pixelPackBuffer)); return pixels; } diff --git a/testsuite/support/GLTestFramebuffer.h b/testsuite/support/GLTestFramebuffer.h index a6db23a5ef3..e570d0d2f9c 100644 --- a/testsuite/support/GLTestFramebuffer.h +++ b/testsuite/support/GLTestFramebuffer.h @@ -6,6 +6,8 @@ #include +struct cc_glglue; + class GLTestFramebuffer { public: GLTestFramebuffer(); @@ -14,9 +16,10 @@ class GLTestFramebuffer { GLTestFramebuffer(const GLTestFramebuffer &) = delete; GLTestFramebuffer & operator=(const GLTestFramebuffer &) = delete; - bool initialize(int width, int height); + bool initialize(const cc_glglue * glue, int width, int height); void shutdown(); void bind() const; + void blitToDefault() const; bool isInitialized() const { return framebuffer_ != 0; } int width() const { return width_; } @@ -25,6 +28,7 @@ class GLTestFramebuffer { std::vector readPixels() const; private: + const cc_glglue * glue_; GLuint framebuffer_; GLuint colorTexture_; GLuint depthRenderbuffer_; diff --git a/testsuite/support/RenderWorkloadViewerController.cpp b/testsuite/support/RenderWorkloadViewerController.cpp new file mode 100644 index 00000000000..592b8dceb30 --- /dev/null +++ b/testsuite/support/RenderWorkloadViewerController.cpp @@ -0,0 +1,196 @@ +#include "RenderWorkloadViewerController.h" + +#include "GLRenderTestSession.h" +#include "RenderWorkloads.h" + +#include +#include +#include +#include +#include + +#ifndef GLFW_INCLUDE_NONE +#define GLFW_INCLUDE_NONE +#endif +#include + +#include +#include +#include +#include + +namespace coin_test { + +struct RenderWorkloadViewerControllerImpl { + GLRenderTestSession & session; + GLTestContext & context; + SoRenderManager & manager; + SoOrthographicCamera & camera; + SceneMutationHandles & mutations; + int width; + int height; + double cursorX = 0.0; + double cursorY = 0.0; + bool panning = false; + bool animate = false; + bool paused = false; + bool hasHover = false; + std::vector baseTranslations; + std::chrono::steady_clock::time_point animationStart; + + RenderWorkloadViewerControllerImpl( + GLRenderTestSession & sessionValue, + SoOrthographicCamera & cameraValue, + SceneMutationHandles & mutationValue, int widthValue, int heightValue) + : session(sessionValue), context(sessionValue.context()), + manager(sessionValue.manager()), camera(cameraValue), + mutations(mutationValue), width(widthValue), height(heightValue), + animationStart(std::chrono::steady_clock::now()) + { + baseTranslations.reserve(mutations.transforms.size()); + for (SoTranslation * transform : mutations.transforms) + baseTranslations.push_back(transform->translation.getValue()); + } +}; + +namespace { + +RenderWorkloadViewerControllerImpl * state(GLFWwindow * window) +{ + return static_cast( + glfwGetWindowUserPointer(window)); +} + +void framebufferSizeCallback(GLFWwindow * window, int width, int height) +{ + RenderWorkloadViewerControllerImpl * viewer = state(window); + if (!viewer || width <= 0 || height <= 0) return; + if (!viewer->session.resize(width, height)) return; + viewer->width = width; + viewer->height = height; +} + +void scrollCallback(GLFWwindow * window, double, double yoffset) +{ + RenderWorkloadViewerControllerImpl * viewer = state(window); + if (!viewer) return; + const float factor = std::pow(0.85f, static_cast(yoffset)); + viewer->camera.height = std::max( + 0.01f, viewer->camera.height.getValue() * factor); +} + +void mouseButtonCallback(GLFWwindow * window, int button, int action, int) +{ + RenderWorkloadViewerControllerImpl * viewer = state(window); + if (!viewer) return; + if (button == GLFW_MOUSE_BUTTON_RIGHT || button == GLFW_MOUSE_BUTTON_MIDDLE) + viewer->panning = action == GLFW_PRESS; +} + +void cursorCallback(GLFWwindow * window, double x, double y) +{ + RenderWorkloadViewerControllerImpl * viewer = state(window); + if (!viewer) return; + if (viewer->panning) { + const float scale = viewer->camera.height.getValue() / + static_cast(std::max(1, viewer->height)); + SbVec3f position = viewer->camera.position.getValue(); + position[0] -= static_cast(x - viewer->cursorX) * scale; + position[1] += static_cast(y - viewer->cursorY) * scale; + viewer->camera.position = position; + } + viewer->cursorX = x; + viewer->cursorY = y; +} + +void keyCallback(GLFWwindow * window, int key, int, int action, int) +{ + if (action != GLFW_PRESS) return; + RenderWorkloadViewerControllerImpl * viewer = state(window); + if (!viewer) return; + if (key == GLFW_KEY_M) viewer->animate = !viewer->animate; + else if (key == GLFW_KEY_SPACE) viewer->paused = !viewer->paused; + else if (key == GLFW_KEY_R) viewer->manager.invalidateDrawList(); +} + +} // namespace + +RenderWorkloadViewerController::RenderWorkloadViewerController( + GLRenderTestSession & session, + SoOrthographicCamera & camera, SceneMutationHandles & mutations, + int width, int height) + : impl_(new RenderWorkloadViewerControllerImpl( + session, camera, mutations, width, height)) +{ +} + +RenderWorkloadViewerController::~RenderWorkloadViewerController() +{ + if (impl_->context.window()) + glfwSetWindowUserPointer(impl_->context.window(), nullptr); + delete impl_; +} + +void RenderWorkloadViewerController::attach() +{ + GLFWwindow * window = impl_->context.window(); + glfwSetWindowUserPointer(window, impl_); + glfwSetFramebufferSizeCallback(window, framebufferSizeCallback); + glfwSetScrollCallback(window, scrollCallback); + glfwSetMouseButtonCallback(window, mouseButtonCallback); + glfwSetCursorPosCallback(window, cursorCallback); + glfwSetKeyCallback(window, keyCallback); +} + +bool RenderWorkloadViewerController::pollEvents() +{ + impl_->context.pollEvents(); + if (impl_->paused) glfwWaitEventsTimeout(0.05); + return !impl_->paused; +} + +void RenderWorkloadViewerController::beforeRender() +{ + if (!impl_->animate || impl_->mutations.transforms.empty()) return; + const double seconds = std::chrono::duration( + std::chrono::steady_clock::now() - impl_->animationStart).count(); + SbVec3f position = impl_->baseTranslations.front(); + position[0] += 0.25f * std::sin(static_cast(seconds * 5.0)); + impl_->mutations.transforms.front()->translation = position; +} + +void RenderWorkloadViewerController::afterRender(const bool pickingEnabled) +{ + if (!pickingEnabled) return; + const int pickX = static_cast(impl_->cursorX); + const int pickY = impl_->height - 1 - static_cast(impl_->cursorY); + SoPickedPoint * picked = nullptr; + impl_->hasHover = impl_->manager.pickClosest(pickX, pickY, 2, picked); + delete picked; +} + +bool RenderWorkloadViewerController::resize(const int width, const int height) +{ + if (!impl_->session.resize(width, height)) return false; + impl_->width = width; + impl_->height = height; + return true; +} + +void RenderWorkloadViewerController::setCursorPosition(double x, double y) +{ + impl_->cursorX = x; + impl_->cursorY = y; +} + +void RenderWorkloadViewerController::setAnimationEnabled(const bool enabled) +{ + impl_->animate = enabled; +} + +bool RenderWorkloadViewerController::hasHoverTarget() const +{ + return impl_->hasHover; +} + +} // namespace coin_test diff --git a/testsuite/support/RenderWorkloadViewerController.h b/testsuite/support/RenderWorkloadViewerController.h new file mode 100644 index 00000000000..d1c09410036 --- /dev/null +++ b/testsuite/support/RenderWorkloadViewerController.h @@ -0,0 +1,35 @@ +#ifndef COIN_TEST_RENDERWORKLOADVIEWERCONTROLLER_H +#define COIN_TEST_RENDERWORKLOADVIEWERCONTROLLER_H + +class GLRenderTestSession; +class SoOrthographicCamera; +class SoRenderManager; + +namespace coin_test { +struct SceneMutationHandles; +struct RenderWorkloadViewerControllerImpl; + +class RenderWorkloadViewerController { +public: + RenderWorkloadViewerController(GLRenderTestSession & session, + SoOrthographicCamera & camera, + SceneMutationHandles & mutations, + int width, int height); + ~RenderWorkloadViewerController(); + + void attach(); + bool pollEvents(); + void beforeRender(); + void afterRender(bool pickingEnabled); + bool resize(int width, int height); + void setCursorPosition(double x, double y); + void setAnimationEnabled(bool enabled); + bool hasHoverTarget() const; + +private: + RenderWorkloadViewerControllerImpl * impl_; +}; + +} // namespace coin_test + +#endif // COIN_TEST_RENDERWORKLOADVIEWERCONTROLLER_H diff --git a/testsuite/support/RenderWorkloads.cpp b/testsuite/support/RenderWorkloads.cpp new file mode 100644 index 00000000000..4a8d9d7ec49 --- /dev/null +++ b/testsuite/support/RenderWorkloads.cpp @@ -0,0 +1,408 @@ +#include "RenderWorkloads.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace coin_test { +namespace { + +struct AssemblyPart { + SoCoordinate3 * coordinates = nullptr; + SoNormal * normals = nullptr; + SoIndexedFaceSet * faces = nullptr; + SoIndexedLineSet * edges = nullptr; +}; + +SoCoordinate3 * addAssemblyGeometry(SoSeparator * parent, WorkloadKind kind, + const AssemblyPart & part, + SoMaterial * faceMaterial, + SoMaterial * edgeMaterial) +{ + SoIndexedFaceSet * faces = part.faces; + SoIndexedLineSet * edges = part.edges; + SoCoordinate3 * coordinates = part.coordinates; + if (kind == WorkloadKind::SharedAssemblyExpanded) { + coordinates = new SoCoordinate3; + coordinates->point = part.coordinates->point; + SoNormal * normals = new SoNormal; + normals->vector = part.normals->vector; + faces = new SoIndexedFaceSet; + faces->coordIndex = part.faces->coordIndex; + edges = new SoIndexedLineSet; + edges->coordIndex = part.edges->coordIndex; + parent->addChild(coordinates); + parent->addChild(normals); + } + else if (kind == WorkloadKind::SharedAssemblySources) { + faces = new SoIndexedFaceSet; + faces->coordIndex = part.faces->coordIndex; + edges = new SoIndexedLineSet; + edges->coordIndex = part.edges->coordIndex; + } + + SoSeparator * faceBranch = new SoSeparator; + faceBranch->renderCaching = SoSeparator::OFF; + faceBranch->addChild(faceMaterial); + faceBranch->addChild(faces); + parent->addChild(faceBranch); + SoSeparator * edgeBranch = new SoSeparator; + edgeBranch->renderCaching = SoSeparator::OFF; + edgeBranch->addChild(edgeMaterial); + edgeBranch->addChild(edges); + parent->addChild(edgeBranch); + return coordinates; +} + +} // namespace + +const char * workloadName(WorkloadKind kind) +{ + switch (kind) { + case WorkloadKind::ManyDraws: return "many_small_draws"; + case WorkloadKind::MaterialChurn: return "many_material_changes"; + case WorkloadKind::Transparency: return "transparent_sorting"; + case WorkloadKind::DensePicking: return "single_pick_dense_scene"; + case WorkloadKind::FeatureRich: return "feature_rich_scene_end_to_end"; + case WorkloadKind::SharedAssemblyExpanded: + return "shared_assembly_expanded"; + case WorkloadKind::SharedAssemblySources: + return "shared_assembly_sources"; + case WorkloadKind::SharedAssemblyRecipe: + return "shared_assembly_recipe"; + } + return "unknown"; +} + +bool parseWorkloadKind(const char * name, WorkloadKind & kind) +{ + if (!name) return false; + const WorkloadKind workloads[] = { + WorkloadKind::ManyDraws, + WorkloadKind::MaterialChurn, + WorkloadKind::Transparency, + WorkloadKind::DensePicking, + WorkloadKind::FeatureRich, + WorkloadKind::SharedAssemblyExpanded, + WorkloadKind::SharedAssemblySources, + WorkloadKind::SharedAssemblyRecipe + }; + for (WorkloadKind candidate : workloads) { + if (std::strcmp(name, workloadName(candidate)) == 0) { + kind = candidate; + return true; + } + } + return false; +} + +bool isAssemblyWorkload(WorkloadKind kind) +{ + return kind == WorkloadKind::SharedAssemblyExpanded || + kind == WorkloadKind::SharedAssemblySources || + kind == WorkloadKind::SharedAssemblyRecipe; +} + +int assemblyDefinitionCount(int occurrenceCount) +{ + return std::min(20, std::max(1, + static_cast(std::sqrt(static_cast(occurrenceCount))))); +} + +void populateAssemblyScene(SoSeparator * root, WorkloadKind kind, + int occurrenceCount, + SceneMutationHandles * mutations = nullptr) +{ + // A small deterministic set of reusable definitions is enough to expose + // the ownership difference. Geometry size varies by definition so the + // workload also contains a realistic mixture of small and medium parts. + const int definitionCount = assemblyDefinitionCount(occurrenceCount); + std::vector parts(static_cast(definitionCount)); + for (int definition = 0; definition < definitionCount; ++definition) { + AssemblyPart & part = parts[static_cast(definition)]; + part.coordinates = new SoCoordinate3; + part.normals = new SoNormal; + part.faces = new SoIndexedFaceSet; + part.edges = new SoIndexedLineSet; + const int triangleCount = 8 + (definition % 5) * 8; + std::vector positions; + std::vector normals; + std::vector faceIndices; + std::vector edgeIndices; + positions.reserve(static_cast(triangleCount + 1)); + normals.reserve(static_cast(triangleCount + 1)); + faceIndices.reserve(static_cast(triangleCount * 4)); + edgeIndices.reserve(static_cast(triangleCount * 3)); + positions.push_back(SbVec3f(0.0f, 0.0f, 0.04f)); + normals.push_back(SbVec3f(0.0f, 0.0f, 1.0f)); + for (int vertex = 0; vertex < triangleCount; ++vertex) { + const float angle = static_cast(vertex) * + 6.28318530718f / static_cast(triangleCount); + const float radius = 0.25f + 0.015f * static_cast(definition); + positions.push_back(SbVec3f(std::cos(angle) * radius, + std::sin(angle) * radius, + 0.01f * static_cast(vertex % 3))); + normals.push_back(SbVec3f(0.0f, 0.0f, 1.0f)); + const int current = vertex + 1; + const int next = ((vertex + 1) % triangleCount) + 1; + faceIndices.push_back(0); + faceIndices.push_back(current); + faceIndices.push_back(next); + faceIndices.push_back(-1); + edgeIndices.push_back(current); + edgeIndices.push_back(next); + edgeIndices.push_back(-1); + } + part.coordinates->point.setValues(0, static_cast(positions.size()), + positions.data()); + part.normals->vector.setValues(0, static_cast(normals.size()), + normals.data()); + part.faces->coordIndex.setValues(0, static_cast(faceIndices.size()), + faceIndices.data()); + part.edges->coordIndex.setValues(0, static_cast(edgeIndices.size()), + edgeIndices.data()); + if (mutations) mutations->definitionCoordinates.push_back(part.coordinates); + } + + SoNormalBinding * normalBinding = new SoNormalBinding; + normalBinding->value = SoNormalBinding::PER_VERTEX_INDEXED; + root->addChild(normalBinding); + std::vector definitionBranches( + static_cast(definitionCount)); + for (int definition = 0; definition < definitionCount; ++definition) { + SoSeparator * branch = new SoSeparator; + branch->renderCaching = SoSeparator::OFF; + if (kind != WorkloadKind::SharedAssemblyExpanded) { + const AssemblyPart & part = parts[static_cast(definition)]; + branch->addChild(part.coordinates); + branch->addChild(part.normals); + } + definitionBranches[static_cast(definition)] = branch; + root->addChild(branch); + } + const int occurrencesPerDefinition = + (occurrenceCount + definitionCount - 1) / definitionCount; + const int layoutSlots = occurrencesPerDefinition * definitionCount; + const int columns = static_cast(std::ceil(std::sqrt( + static_cast(layoutSlots)))); + const int rows = (layoutSlots + columns - 1) / columns; + const float spacing = 1.15f; + const SbColor palette[] = { + SbColor(0.32f, 0.62f, 0.78f), + SbColor(0.42f, 0.72f, 0.48f), + SbColor(0.76f, 0.55f, 0.30f), + SbColor(0.65f, 0.45f, 0.72f), + SbColor(0.30f, 0.70f, 0.68f), + SbColor(0.78f, 0.43f, 0.45f), + SbColor(0.58f, 0.62f, 0.34f), + SbColor(0.48f, 0.52f, 0.68f) + }; + for (int occurrence = 0; occurrence < occurrenceCount; ++occurrence) { + SoSeparator * instance = new SoSeparator; + instance->renderCaching = SoSeparator::OFF; + const int definition = std::min(definitionCount - 1, + occurrence / occurrencesPerDefinition); + const int definitionOccurrence = occurrence % occurrencesPerDefinition; + // Keep traversal grouped by definition for retained batching, but place + // definitions next to each other so ownership is visible in the grid. + const int layoutIndex = definitionOccurrence * definitionCount + definition; + const int layoutRow = layoutIndex / columns; + const int rowWidth = std::min(columns, layoutSlots - layoutRow * columns); + SoTranslation * placement = new SoTranslation; + placement->translation.setValue( + (static_cast(layoutIndex % columns) - + static_cast(rowWidth - 1) * 0.5f) * spacing, + (static_cast(layoutRow) - + static_cast(rows - 1) * 0.5f) * spacing, + -0.002f * static_cast(occurrence % 7)); + instance->addChild(placement); + if (mutations) mutations->transforms.push_back(placement); + SoMaterial * material = new SoMaterial; + material->diffuseColor = palette[definition % 8]; + if (mutations) mutations->materials.push_back(material); + SoMaterial * edgeMaterial = new SoMaterial; + edgeMaterial->diffuseColor.setValue(0.10f, 0.11f, 0.14f); + SoCoordinate3 * occurrenceCoordinates = addAssemblyGeometry(instance, kind, + parts[static_cast(definition)], material, edgeMaterial); + if (mutations) mutations->coordinates.push_back(occurrenceCoordinates); + definitionBranches[static_cast(definition)]->addChild(instance); + } +} + +SoSeparator * makeScene(WorkloadKind kind, int drawCount, + SoOrthographicCamera *& camera, + SceneMutationHandles * mutations) +{ + SoSeparator * root = new SoSeparator; + root->ref(); + root->renderCaching = SoSeparator::OFF; + camera = new SoOrthographicCamera; + camera->ref(); + camera->position.setValue(0.0f, 0.0f, 10.0f); + camera->height = 24.0f; + camera->nearDistance = 0.1f; + camera->farDistance = 100.0f; + camera->focalDistance = 10.0f; + SoLightModel * lightModel = new SoLightModel; + lightModel->model = kind == WorkloadKind::FeatureRich + ? SoLightModel::PHONG : SoLightModel::BASE_COLOR; + root->addChild(lightModel); + if (kind == WorkloadKind::FeatureRich) { + SoDirectionalLight * light = new SoDirectionalLight; + light->direction.setValue(0.0f, 0.0f, -1.0f); + root->addChild(light); + } + SoMaterial * defaultMaterial = new SoMaterial; + defaultMaterial->diffuseColor.setValue(0.3f, 0.7f, 1.0f); + root->addChild(defaultMaterial); + + if (isAssemblyWorkload(kind)) { + const int definitions = assemblyDefinitionCount(drawCount); + const int occurrencesPerDefinition = + (drawCount + definitions - 1) / definitions; + const int layoutSlots = occurrencesPerDefinition * definitions; + const int columns = static_cast(std::ceil(std::sqrt( + static_cast(layoutSlots)))); + const int rows = (layoutSlots + columns - 1) / columns; + camera->height = std::max(8.0f, rows * 1.15f + 1.0f); + populateAssemblyScene(root, kind, drawCount, mutations); + return root; + } + + const SbVec3f triangle[] = { + SbVec3f(-0.42f, -0.42f, 0.0f), + SbVec3f(0.42f, -0.42f, 0.0f), + SbVec3f(0.0f, 0.42f, 0.0f) + }; + const int columns = static_cast(std::ceil(std::sqrt( + static_cast(drawCount)))); + const int rows = (drawCount + columns - 1) / columns; + SoCoordinate3 * sharedCoordinates = nullptr; + SoNormal * sharedNormals = nullptr; + SoNormalBinding * sharedNormalBinding = nullptr; + SoTextureCoordinate2 * sharedTexcoords = nullptr; + SoTexture2 * sharedTexture = nullptr; + SoMaterial * litMaterial = nullptr; + SoMaterial * vertexMaterial = nullptr; + SoMaterialBinding * vertexBinding = nullptr; + SoMaterial * transparentMaterial = nullptr; + SoFaceSet * sharedFace = nullptr; + if (kind == WorkloadKind::FeatureRich) { + sharedCoordinates = new SoCoordinate3; + sharedCoordinates->point.setValues(0, 3, triangle); + const SbVec3f normals[] = { + SbVec3f(0.0f, 0.0f, 1.0f), SbVec3f(0.0f, 0.0f, 1.0f), + SbVec3f(0.0f, 0.0f, 1.0f) + }; + sharedNormals = new SoNormal; + sharedNormals->vector.setValues(0, 3, normals); + sharedNormalBinding = new SoNormalBinding; + sharedNormalBinding->value = SoNormalBinding::PER_VERTEX; + const SbVec2f textureCoordinates[] = { + SbVec2f(0.0f, 0.0f), SbVec2f(1.0f, 0.0f), SbVec2f(0.5f, 1.0f) + }; + sharedTexcoords = new SoTextureCoordinate2; + sharedTexcoords->point.setValues(0, 3, textureCoordinates); + const unsigned char texels[] = { + 220, 80, 40, 255, 40, 180, 220, 255, + 40, 180, 220, 255, 220, 80, 40, 255 + }; + sharedTexture = new SoTexture2; + sharedTexture->image.setValue(SbVec2s(2, 2), 4, texels); + litMaterial = new SoMaterial; + litMaterial->diffuseColor.setValue(0.65f, 0.72f, 0.85f); + const SbColor vertexColors[] = { + SbColor(1.0f, 0.2f, 0.2f), SbColor(0.2f, 1.0f, 0.2f), + SbColor(0.2f, 0.2f, 1.0f) + }; + vertexMaterial = new SoMaterial; + vertexMaterial->diffuseColor.setValues(0, 3, vertexColors); + vertexBinding = new SoMaterialBinding; + vertexBinding->value = SoMaterialBinding::PER_VERTEX; + transparentMaterial = new SoMaterial; + transparentMaterial->diffuseColor.setValue(0.65f, 0.72f, 0.85f); + transparentMaterial->transparency = 0.45f; + sharedFace = new SoFaceSet; + sharedFace->numVertices.set1Value(0, 3); + } + for (int i = 0; i < drawCount; ++i) { + SoSeparator * draw = new SoSeparator; + draw->renderCaching = SoSeparator::OFF; + SoTranslation * translation = new SoTranslation; + const float x = kind == WorkloadKind::DensePicking ? 0.0f : + (static_cast(i % columns) - + static_cast(columns - 1) * 0.5f) * 1.05f; + const float y = kind == WorkloadKind::DensePicking ? 0.0f : + (static_cast(i / columns) - + static_cast(rows - 1) * 0.5f) * 1.05f; + const float z = kind == WorkloadKind::Transparency + ? -static_cast(i % 32) * 0.01f + : (kind == WorkloadKind::DensePicking + ? -static_cast(i) * 0.001f : 0.0f); + translation->translation.setValue(x, y, z); + draw->addChild(translation); + if (mutations) mutations->transforms.push_back(translation); + + if (kind == WorkloadKind::FeatureRich) { + const int group = i < drawCount * 2 / 5 ? 0 + : (i < drawCount * 3 / 5 ? 1 + : (i < drawCount * 4 / 5 ? 2 : 3)); + if (group == 2) { + draw->addChild(vertexMaterial); + draw->addChild(vertexBinding); + } + else { + draw->addChild(group == 3 ? transparentMaterial : litMaterial); + } + if (group == 1) { + draw->addChild(sharedTexture); + draw->addChild(sharedTexcoords); + } + draw->addChild(sharedCoordinates); + draw->addChild(sharedNormals); + draw->addChild(sharedNormalBinding); + draw->addChild(sharedFace); + } + else if (kind != WorkloadKind::ManyDraws) { + SoMaterial * material = new SoMaterial; + const float value = static_cast((i * 17) % 101) / 100.0f; + material->diffuseColor.setValue(0.2f + value * 0.8f, + 0.9f - value * 0.7f, + 0.3f + value * 0.5f); + if (kind == WorkloadKind::Transparency) material->transparency = 0.35f; + draw->addChild(material); + if (mutations) mutations->materials.push_back(material); + } + if (kind != WorkloadKind::FeatureRich) { + SoCoordinate3 * coordinates = new SoCoordinate3; + coordinates->point.setValues(0, 3, triangle); + if (mutations) mutations->coordinates.push_back(coordinates); + SoFaceSet * face = new SoFaceSet; + face->numVertices.set1Value(0, 3); + draw->addChild(coordinates); + draw->addChild(face); + } + root->addChild(draw); + } + return root; +} + +} // namespace coin_test diff --git a/testsuite/support/RenderWorkloads.h b/testsuite/support/RenderWorkloads.h new file mode 100644 index 00000000000..6a9d4687a7c --- /dev/null +++ b/testsuite/support/RenderWorkloads.h @@ -0,0 +1,42 @@ +#ifndef COIN_TEST_RENDERWORKLOADS_H +#define COIN_TEST_RENDERWORKLOADS_H + +#include + +class SoCoordinate3; +class SoMaterial; +class SoOrthographicCamera; +class SoSeparator; +class SoTranslation; + +namespace coin_test { + +enum class WorkloadKind { + ManyDraws, + MaterialChurn, + Transparency, + DensePicking, + FeatureRich, + SharedAssemblyExpanded, + SharedAssemblySources, + SharedAssemblyRecipe +}; + +struct SceneMutationHandles { + std::vector transforms; + std::vector materials; + std::vector coordinates; + std::vector definitionCoordinates; +}; + +const char * workloadName(WorkloadKind kind); +bool parseWorkloadKind(const char * name, WorkloadKind & kind); +bool isAssemblyWorkload(WorkloadKind kind); +int assemblyDefinitionCount(int occurrenceCount); +SoSeparator * makeScene(WorkloadKind kind, int drawCount, + SoOrthographicCamera *& camera, + SceneMutationHandles * mutations = nullptr); + +} // namespace coin_test + +#endif // COIN_TEST_RENDERWORKLOADS_H