From 445ce7f6ebbfc85ddfd5049ad72f3a9ded12ecf4 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Tue, 19 Aug 2025 18:31:42 +0200 Subject: [PATCH 1/4] add support for shadow pacing to help improve performance optimise put backobject paging min size add light-sapce texel snapping for cascade 0 --- components/sceneutil/mwshadowtechnique.cpp | 355 ++++++++++++++++----- components/sceneutil/mwshadowtechnique.hpp | 17 + components/sceneutil/shadow.cpp | 3 + components/settings/categories/shadows.hpp | 4 + files/settings-default.cfg | 5 + 5 files changed, 304 insertions(+), 80 deletions(-) diff --git a/components/sceneutil/mwshadowtechnique.cpp b/components/sceneutil/mwshadowtechnique.cpp index 5f033cb612..ce1dc58a67 100644 --- a/components/sceneutil/mwshadowtechnique.cpp +++ b/components/sceneutil/mwshadowtechnique.cpp @@ -25,6 +25,9 @@ #include #include #include +#include +#include +#include #include #include @@ -34,6 +37,37 @@ // NOLINTBEGIN(readability-identifier-naming) +// File-scope constants for shadow pacing thresholds and heuristics. +namespace { + // Pacing cadence and starvation guard + static constexpr int kUpdateCadence = 3; // update all cascades every 3rd frame + static constexpr unsigned int kMaxSkipFrames = 12; // starvation guard per cascade + + // Overlap bias to hide seams while pacing + static constexpr double kOverlapBiasPaced = 0.4; // extra overlap while pacing to hide seams + + // Rotation guardrails + static const double kRotateEpsRad = osg::DegreesToRadians(0.1); // tiny epsilon to detect slow pans + static const double kForceRotateRad = osg::DegreesToRadians(1.5); // stronger rotate guard + static const double kSmallRotateRad = osg::DegreesToRadians(0.5); // small threshold to treat slow mouse-look as rotate + static constexpr unsigned int kRotateBurstFrames = 4; // short burst window after rotation + + // Precomputed cosines to avoid acos per-frame + static const double kCosRotateEps = std::cos(kRotateEpsRad); + static const double kCosForceRotate = std::cos(kForceRotateRad); + static const double kCosSmallRotate = std::cos(kSmallRotateRad); + + // Translation thresholds (world units per frame) + static constexpr double kBackwardEps = 0.02; // motion threshold (any direction) + static constexpr double kBackwardFastEps = 0.15; // higher-speed threshold for bursts (any direction) + static const double kBackwardEps2 = kBackwardEps * kBackwardEps; + static const double kBackwardFastEps2 = kBackwardFastEps * kBackwardFastEps; + static constexpr unsigned int kTranslateBurstFrames = 2; // brief burst when moving fast + + // Shared forward vector (view space -Z axis) + static const osg::Vec3d kViewForward(0.0, 0.0, -1.0); +} + namespace { using namespace osgShadow; @@ -934,6 +968,29 @@ void SceneUtil::MWShadowTechnique::setupCastingShader(Shader::ShaderManager & sh } } +void SceneUtil::MWShadowTechnique::setShadowPacingEnabled(bool enabled) +{ + mShadowPacingEnabled = enabled; + if (!enabled) + { + // reset runtime state + mRotateActiveCountdown = 0; + mLastPoseValid = false; + } +} + +void SceneUtil::MWShadowTechnique::configureShadowPacingDefaults() +{ + // Backward compatibility shim: enabling defaults equals enabling pacing + setShadowPacingEnabled(true); +} + +void SceneUtil::MWShadowTechnique::clearShadowPacing() +{ + // Backward compatibility shim: disabling clears runtime state + setShadowPacingEnabled(false); +} + MWShadowTechnique::ViewDependentData* MWShadowTechnique::createViewDependentData(osgUtil::CullVisitor* /*cv*/) { return new ViewDependentData(this); @@ -1069,6 +1126,9 @@ void MWShadowTechnique::cull(osgUtil::CullVisitor& cv) Uniforms& vddUniforms = vdd->_uniforms[cv.getTraversalNumber() % 2]; ShadowSettings* settings = getShadowedScene()->getShadowSettings(); + const bool debugDraw = settings->getDebugDraw(); + const bool isCascaded = settings->getMultipleShadowMapHint() == ShadowSettings::CASCADED; + const bool isParallelSplit = settings->getMultipleShadowMapHint() == ShadowSettings::PARALLEL_SPLIT; OSG_INFO<<"cv->getProjectionMatrix()="<<*cv.getProjectionMatrix()<getNumShadowMapsPerLight(); + // Experimental pacing: decide whether this frame should fully update shadows + const unsigned int traversalNumber = cv.getTraversalNumber(); + + bool forceFullUpdate = true; + if (mShadowPacingEnabled) + forceFullUpdate = (kUpdateCadence <= 1) || (traversalNumber % static_cast(kUpdateCadence) == 0); + + // Movement/rotation guardrails: when pacing enabled, check rotation to force updates + bool movingTranslate = false; // track motion to keep the next cascade fresher + bool movingTranslateFast = false; // track high-speed movement in any direction (along/lateral) + if (mShadowPacingEnabled && !forceFullUpdate) + { + osg::Matrix invMV = osg::Matrix::inverse(*cv.getModelViewMatrix()); + osg::Vec3d eye = osg::Vec3d(0,0,0) * invMV; + // Derive and normalize a forward direction from the model-view matrix (negative Z axis in view space) + osg::Vec3d fwd = osg::Matrix::transform3x3(kViewForward, invMV); + osg::Vec3d fwdNorm = fwd; fwdNorm.normalize(); + if (mLastPoseValid) + { + // Detect translation magnitude along the view and lateral to the view + osg::Vec3d delta = eye - mLastEye; + double along = delta * fwdNorm; + osg::Vec3d perp = delta - fwdNorm * along; + double lateral2 = perp.length2(); + movingTranslate = (std::fabs(along) > kBackwardEps) || (lateral2 > kBackwardEps2); + movingTranslateFast = (std::fabs(along) > kBackwardFastEps) || (lateral2 > kBackwardFastEps2); + + // Compare dot product against cosine thresholds (avoid acos) + const double dotDir = osg::clampBetween(mLastDir * fwdNorm, -1.0, 1.0); + bool turned = (dotDir <= kCosForceRotate); + // Stronger protection for mouse-look: even small rotation can shimmer; force if above a small threshold + bool forcedRotate = false; + if (!forcedRotate) + { + // Treat any small rotation as mouse-look; use a tiny epsilon ~0.5° + forcedRotate = (dotDir <= kCosSmallRotate); + } + // Rotation burst: on any tiny rotation, force a few full updates to eliminate slow-pan flicker + if (kRotateBurstFrames > 0 && kRotateEpsRad > 0.0) + { + if (dotDir <= kCosRotateEps) + { + mRotateActiveCountdown = kRotateBurstFrames; + } + } + // Translation burst: when moving fast, briefly force full updates + if (movingTranslateFast && kTranslateBurstFrames > 0) + { + if (mRotateActiveCountdown < static_cast(kTranslateBurstFrames)) + mRotateActiveCountdown = kTranslateBurstFrames; + } + forceFullUpdate = turned || forcedRotate; + } + mLastEye = eye; mLastDir = fwdNorm; mLastPoseValid = true; + } + + // If burst is active, force full updates and decrement countdown + if (mShadowPacingEnabled && !forceFullUpdate && mRotateActiveCountdown > 0) + { + forceFullUpdate = true; + --mRotateActiveCountdown; + } + + // If round-robin is enabled and we're not forcing a full update, select a single cascade index this frame + int rrCascadeIndex = -1; + if (mShadowPacingEnabled && !forceFullUpdate && numShadowMapsPerLight > 0) + { + // If stationary, favor keeping cascade 0 fresh and rely on starvation guard for deeper cascades + if (!movingTranslate && mRotateActiveCountdown == 0) + rrCascadeIndex = 0; + else + rrCascadeIndex = static_cast(traversalNumber % numShadowMapsPerLight); + } + LightDataList& pll = vdd->getLightDataList(); for(LightDataList::iterator itr = pll.begin(); itr != pll.end(); @@ -1364,27 +1498,58 @@ void MWShadowTechnique::cull(osgUtil::CullVisitor& cv) osg::ref_ptr camera = sd->_camera; - camera->setProjectionMatrix(projectionMatrix); - camera->setViewMatrix(viewMatrix); - - if (settings->getDebugDraw()) + // Decide whether to render/update this cascade this traversal + bool renderThisCascade = true; + if (mShadowPacingEnabled && !forceFullUpdate) { - camera->getViewport()->x() = pos_x; - pos_x += static_cast(camera->getViewport()->width()) + 40; + // Round-robin: update only one cascade this frame unless forced + renderThisCascade = (static_cast(sm_i) == rrCascadeIndex); + // Ensure we don't starve a cascade: force if skipped too long + if (!renderThisCascade && kMaxSkipFrames > 0) + { + if (traversalNumber - sd->_lastUpdateTraversal >= kMaxSkipFrames) + renderThisCascade = true; + } + // Always keep the nearest cascade fresh to avoid shimmer during camera pans + if (sm_i == 0) + renderThisCascade = true; + // When moving, also refresh the next cascade to keep the seam in sync + if (!renderThisCascade && movingTranslate && sm_i == 1) + renderThisCascade = true; + // At high speeds, also refresh the second cascade if present + if (!renderThisCascade && movingTranslateFast && sm_i == 2) + renderThisCascade = true; } - // transform polytope in model coords into light spaces eye coords. - osg::Matrixd invertModelView; - invertModelView.invert(camera->getViewMatrix()); + if (renderThisCascade) + { + camera->setProjectionMatrix(projectionMatrix); + camera->setViewMatrix(viewMatrix); - osg::Polytope local_polytope(polytope); - local_polytope.transformProvidingInverse(invertModelView); + if (debugDraw) + { + camera->getViewport()->x() = pos_x; + pos_x += static_cast(camera->getViewport()->width()) + 40; + } + } double cascaseNear = reducedNear; double cascadeFar = reducedFar; - if (numShadowMapsPerLight>1) + + // Local polytope used by both PARALLEL_SPLIT plane clipping and CASCADED cropping + osg::Polytope local_polytope; + + if (renderThisCascade) { - // compute the start and end range in non-dimensional coords + // transform polytope in model coords into light space eye coords once + osg::Matrixd invertModelView; + invertModelView.invert(camera->getViewMatrix()); + local_polytope = polytope; + local_polytope.transformProvidingInverse(invertModelView); + + // compute the start and end range in non-dimensional coords if using multiple shadow maps + if (numShadowMapsPerLight>1) + { #if 0 double r_start = (sm_i==0) ? -1.0 : (double(sm_i)/double(numShadowMapsPerLight)*2.0-1.0); double r_end = (sm_i+1==numShadowMapsPerLight) ? 1.0 : (double(sm_i+1)/double(numShadowMapsPerLight)*2.0-1.0); @@ -1433,90 +1598,119 @@ void MWShadowTechnique::cull(osgUtil::CullVisitor& cv) r_end = lightSpacePos.y(); } #endif - // for all by the last shadowmap shift the r_end so that it overlaps slightly with the next shadowmap - // to prevent a seam showing through between the shadowmaps - if (sm_i+10) + { + // not the first shadowmap so insert a polytope to clip the scene from before r_start + osg::Plane plane(0.0,1.0,0.0,-r_start); + plane.transformProvidingInverse(projectionMatrix); + local_polytope.getPlaneList().push_back(plane); + planesPushed = true; + } - // We can't add these clipping planes with cascaded shadow maps as they wouldn't be parallel to the light direction. + if (isParallelSplit && sm_i+1getMultipleShadowMapHint() == ShadowSettings::PARALLEL_SPLIT && sm_i>0) - { - // not the first shadowmap so insert a polytope to clip the scene from before r_start + if (planesPushed) + local_polytope.setupMask(); - // plane in clip space coords - osg::Plane plane(0.0,1.0,0.0,-r_start); + if (isParallelSplit) + { + double mid_r = (r_start + r_end)*0.5; + double range_r = (r_end - r_start); - // transform into eye coords - plane.transformProvidingInverse(projectionMatrix); - local_polytope.getPlaneList().push_back(plane); - - //OSG_NOTICE<<"Adding r_start plane "<getMultipleShadowMapHint() == ShadowSettings::PARALLEL_SPLIT && sm_i+1getMultipleShadowMapHint() == ShadowSettings::PARALLEL_SPLIT) - { - // OSG_NOTICE<<"Need to adjust RTT camera projection and view matrix here, r_start="< mEnableShadowPacing{ mIndex, "Shadows", "enable shadow pacing" }; SettingValue mNumberOfShadowMaps{ mIndex, "Shadows", "number of shadow maps", makeClampSanitizerInt(1, 8) }; SettingValue mMaximumShadowMapDistance{ mIndex, "Shadows", "maximum shadow map distance" }; diff --git a/files/settings-default.cfg b/files/settings-default.cfg index 2c4bbab953..927582931f 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -1021,6 +1021,11 @@ wait for all jobs on exit = false # Enable or disable shadows. Bear in mind that this will force OpenMW to use shaders as if "[Shaders]/force shaders" was set to true. enable shadows = false +# Experimental: reduce shadow map update cost by pacing updates across frames. +# When enabled, the engine updates shadow cascades less frequently with guardrails to maintain quality. +# This can improve FPS in heavy scenes at the cost of slightly stale distant shadows during movement. +enable shadow pacing = false + # How many shadow maps to use - more of these means each shadow map texel covers less area, producing better looking shadows, but may decrease performance. number of shadow maps = 3 From 36ab7da646ae688bc7b0618caf37415b65bf92e9 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Wed, 20 Aug 2025 22:32:47 +0200 Subject: [PATCH 2/4] impliment initial verion of parallel shadow traversal restore logging purge pacing --- components/sceneutil/mwshadowtechnique.cpp | 707 ++++++++++----------- components/sceneutil/mwshadowtechnique.hpp | 23 +- components/sceneutil/shadow.cpp | 6 +- components/settings/categories/shadows.hpp | 6 +- files/settings-default.cfg | 9 +- 5 files changed, 366 insertions(+), 385 deletions(-) diff --git a/components/sceneutil/mwshadowtechnique.cpp b/components/sceneutil/mwshadowtechnique.cpp index ce1dc58a67..d93cb6a188 100644 --- a/components/sceneutil/mwshadowtechnique.cpp +++ b/components/sceneutil/mwshadowtechnique.cpp @@ -26,48 +26,21 @@ #include #include #include +#include +#include #include #include #include #include +#include +#include #include "glextensions.hpp" #include "shadowsbin.hpp" // NOLINTBEGIN(readability-identifier-naming) -// File-scope constants for shadow pacing thresholds and heuristics. -namespace { - // Pacing cadence and starvation guard - static constexpr int kUpdateCadence = 3; // update all cascades every 3rd frame - static constexpr unsigned int kMaxSkipFrames = 12; // starvation guard per cascade - - // Overlap bias to hide seams while pacing - static constexpr double kOverlapBiasPaced = 0.4; // extra overlap while pacing to hide seams - - // Rotation guardrails - static const double kRotateEpsRad = osg::DegreesToRadians(0.1); // tiny epsilon to detect slow pans - static const double kForceRotateRad = osg::DegreesToRadians(1.5); // stronger rotate guard - static const double kSmallRotateRad = osg::DegreesToRadians(0.5); // small threshold to treat slow mouse-look as rotate - static constexpr unsigned int kRotateBurstFrames = 4; // short burst window after rotation - - // Precomputed cosines to avoid acos per-frame - static const double kCosRotateEps = std::cos(kRotateEpsRad); - static const double kCosForceRotate = std::cos(kForceRotateRad); - static const double kCosSmallRotate = std::cos(kSmallRotateRad); - - // Translation thresholds (world units per frame) - static constexpr double kBackwardEps = 0.02; // motion threshold (any direction) - static constexpr double kBackwardFastEps = 0.15; // higher-speed threshold for bursts (any direction) - static const double kBackwardEps2 = kBackwardEps * kBackwardEps; - static const double kBackwardFastEps2 = kBackwardFastEps * kBackwardFastEps; - static constexpr unsigned int kTranslateBurstFrames = 2; // brief burst when moving fast - - // Shared forward vector (view space -Z axis) - static const osg::Vec3d kViewForward(0.0, 0.0, -1.0); -} - namespace { using namespace osgShadow; @@ -276,7 +249,7 @@ class VDSMCameraCullCallback : public osg::NodeCallback { public: - VDSMCameraCullCallback(MWShadowTechnique* vdsm, osg::Polytope& polytope); + VDSMCameraCullCallback(MWShadowTechnique* vdsm, const osg::Polytope& polytope); void operator()(osg::Node*, osg::NodeVisitor* nv) override; @@ -291,47 +264,57 @@ class VDSMCameraCullCallback : public osg::NodeCallback osg::Polytope _polytope; }; -VDSMCameraCullCallback::VDSMCameraCullCallback(MWShadowTechnique* vdsm, osg::Polytope& polytope): +VDSMCameraCullCallback::VDSMCameraCullCallback(MWShadowTechnique* vdsm, const osg::Polytope& polytope): _vdsm(vdsm), _polytope(polytope) -{ -} +{} void VDSMCameraCullCallback::operator()(osg::Node* node, osg::NodeVisitor* nv) { osgUtil::CullVisitor* cv = static_cast(nv); osg::Camera* camera = node->asCamera(); OSG_INFO<<"VDSMCameraCullCallback::operator()(osg::Node* "<getCurrentRenderStage()) + cv->setCurrentRenderBin(stage); -#if 1 if (!_polytope.empty()) { - OSG_INFO<<"Pushing custom Polytope"<getProjectionCullingStack().back(); - - cs.setFrustum(_polytope); - - cv->pushCullingSet(); + auto& stack = cv->getProjectionCullingStack(); + if (!stack.empty()) + { + osg::CullingSet& cs = stack.back(); + cs.setFrustum(_polytope); + cv->pushCullingSet(); + } + else + { + // No projection stack available; skip polytope for safety in parallel paths + } } -#endif + // Push casting state and shadows bin while traversing the RTT camera + if (auto* cast = _vdsm->getCastingStateSet()) + cv->pushStateSet(cast); // bin has to go inside camera cull or the rendertexture stage will override it cv->pushStateSet(_vdsm->getOrCreateShadowsBinStateSet()); if (_vdsm->getShadowedScene()) { _vdsm->getShadowedScene()->osg::Group::traverse(*nv); } - cv->popStateSet(); -#if 1 + cv->popStateSet(); // pop shadows bin stateset + if (_vdsm->getCastingStateSet()) cv->popStateSet(); if (!_polytope.empty()) { - OSG_INFO<<"Popping custom Polytope"<popCullingSet(); + auto& stack = cv->getProjectionCullingStack(); + if (!stack.empty()) + { + cv->popCullingSet(); + } } -#endif - - _renderStage = cv->getCurrentRenderBin()->getStage(); + // Capture the RTT render stage after traversal for PSM adjustments later. + _renderStage = cv->getCurrentRenderStage(); OSG_INFO<<"VDSM second : _renderStage = "<<_renderStage<getComputeNearFarMode() != osg::CullSettings::DO_NOT_COMPUTE_NEAR_FAR) @@ -339,10 +322,11 @@ void VDSMCameraCullCallback::operator()(osg::Node* node, osg::NodeVisitor* nv) // make sure that the near plane is computed correctly. cv->computeNearPlane(); - osg::Matrixd projection = *(cv->getProjectionMatrix()); - + osg::RefMatrix* projRef = cv->getProjectionMatrix(); + if (!projRef) + return; // cannot proceed safely without a projection matrix + osg::Matrixd projection = *projRef; OSG_INFO<<"RTT Projection matrix "<osg::Group::traverse(cv); return; } - + OSG_INFO<_uniforms[cv.getTraversalNumber() % 2]; ShadowSettings* settings = getShadowedScene()->getShadowSettings(); @@ -1132,6 +1085,7 @@ void MWShadowTechnique::cull(osgUtil::CullVisitor& cv) OSG_INFO<<"cv->getProjectionMatrix()="<<*cv.getProjectionMatrix()<getMaximumShadowMapDistance(),maxZFar); if (minZNear>maxZFar) minZNear = maxZFar*settings->getMinimumShadowMapNearFarRatio(); - //OSG_NOTICE<<"maxZFar "<second); // return compute near far mode back to it's original settings @@ -1244,8 +1195,6 @@ void MWShadowTechnique::cull(osgUtil::CullVisitor& cv) // return compute near far mode back to it's original settings cv.setComputeNearFarMode(cachedNearFarMode); - OSG_INFO<<"frustum.eye="< #include #include +#include #include #include @@ -90,11 +91,10 @@ namespace SceneUtil { virtual void setupCastingShader(Shader::ShaderManager &shaderManager); - // Configuration for experimental shadow update pacing - void setShadowPacingEnabled(bool enabled); - // Kept for ABI/source compatibility with recent branches; now just proxies to setShadowPacingEnabled - void configureShadowPacingDefaults(); - void clearShadowPacing(); + // Experimental: enable parallel shadow preparation/culling + void setParallelShadowCullingEnabled(bool enabled) { mParallelShadowCullingEnabled = enabled; } + // Experimental: enable parallel per-cascade RTT traversal + void setParallelShadowTraversalEnabled(bool enabled) { mParallelShadowTraversalEnabled = enabled; } class ComputeLightSpaceBounds : public osg::NodeVisitor, public osg::CullStack { @@ -204,9 +204,6 @@ namespace SceneUtil { unsigned int _sm_i; osg::ref_ptr _texture; osg::ref_ptr _camera; - - // Experimental pacing: last traversal number when this cascade actually rendered - unsigned int _lastUpdateTraversal = 0; }; typedef std::list< osg::ref_ptr > ShadowDataList; @@ -281,6 +278,7 @@ namespace SceneUtil { void setWorldMask(unsigned int worldMask) { _worldMask = worldMask; } osg::ref_ptr getOrCreateShadowsBinStateSet(); + osg::StateSet* getCastingStateSet() const { return _shadowCastingStateSet.get(); } protected: virtual ~MWShadowTechnique(); @@ -318,13 +316,8 @@ namespace SceneUtil { unsigned int _worldMask = ~0u; - // --- Experimental: shadow update pacing (single toggle with internal constants) - bool mShadowPacingEnabled = false; - // Runtime state only - unsigned int mRotateActiveCountdown = 0; - osg::Vec3d mLastEye = osg::Vec3d(); - osg::Vec3d mLastDir = osg::Vec3d(); - bool mLastPoseValid = false; + bool mParallelShadowCullingEnabled = false; + bool mParallelShadowTraversalEnabled = false; class DebugHUD final : public osg::Referenced { diff --git a/components/sceneutil/shadow.cpp b/components/sceneutil/shadow.cpp index 06f6b79925..dfd793ffd5 100644 --- a/components/sceneutil/shadow.cpp +++ b/components/sceneutil/shadow.cpp @@ -81,8 +81,8 @@ namespace SceneUtil else mShadowTechnique->disableDebugHUD(); - // Configure experimental shadow pacing via user setting (replaces prior env toggles) - mShadowTechnique->setShadowPacingEnabled(settings.mEnableShadowPacing); + mShadowTechnique->setParallelShadowCullingEnabled(settings.mParallelShadowCulling); + mShadowTechnique->setParallelShadowTraversalEnabled(settings.mParallelShadowTraversal); } void ShadowManager::disableShadowsForStateSet(osg::StateSet& stateset) const @@ -99,7 +99,7 @@ namespace SceneUtil fakeShadowMapTexture->setShadowComparison(true); fakeShadowMapTexture->setShadowCompareFunc(osg::Texture::ShadowCompareFunc::ALWAYS); for (unsigned int i = mShadowSettings->getBaseShadowTextureUnit(); - i < mShadowSettings->getBaseShadowTextureUnit() + mShadowSettings->getNumShadowMapsPerLight(); ++i) + i < mShadowSettings->getBaseShadowTextureUnit() + mShadowSettings->getNumShadowMapsPerLight(); ++i) { stateset.setTextureAttribute(i, fakeShadowMapTexture, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE | osg::StateAttribute::PROTECTED); diff --git a/components/settings/categories/shadows.hpp b/components/settings/categories/shadows.hpp index 619bdf76f2..feb9091363 100644 --- a/components/settings/categories/shadows.hpp +++ b/components/settings/categories/shadows.hpp @@ -19,10 +19,6 @@ namespace Settings using WithIndex::WithIndex; SettingValue mEnableShadows{ mIndex, "Shadows", "enable shadows" }; - // Experimental: reduce shadow map update cost by pacing updates across frames. - // When enabled, the engine will update shadow cascades less frequently with guardrails - // to maintain visual stability. This can noticeably improve performance in heavy scenes. - SettingValue mEnableShadowPacing{ mIndex, "Shadows", "enable shadow pacing" }; SettingValue mNumberOfShadowMaps{ mIndex, "Shadows", "number of shadow maps", makeClampSanitizerInt(1, 8) }; SettingValue mMaximumShadowMapDistance{ mIndex, "Shadows", "maximum shadow map distance" }; @@ -46,6 +42,8 @@ namespace Settings SettingValue mTerrainShadows{ mIndex, "Shadows", "terrain shadows" }; SettingValue mObjectShadows{ mIndex, "Shadows", "object shadows" }; SettingValue mEnableIndoorShadows{ mIndex, "Shadows", "enable indoor shadows" }; + SettingValue mParallelShadowCulling{ mIndex, "Shadows", "parallel shadow culling" }; + SettingValue mParallelShadowTraversal{ mIndex, "Shadows", "parallel shadow traversal" }; }; } diff --git a/files/settings-default.cfg b/files/settings-default.cfg index 927582931f..a6bd09d43b 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -1021,10 +1021,11 @@ wait for all jobs on exit = false # Enable or disable shadows. Bear in mind that this will force OpenMW to use shaders as if "[Shaders]/force shaders" was set to true. enable shadows = false -# Experimental: reduce shadow map update cost by pacing updates across frames. -# When enabled, the engine updates shadow cascades less frequently with guardrails to maintain quality. -# This can improve FPS in heavy scenes at the cost of slightly stale distant shadows during movement. -enable shadow pacing = false +# compute per-cascade prep and cull RTT cameras in parallel +parallel shadow culling = false + +# compute per-cascade RTT traversal in parallel +parallel shadow traversal = false # How many shadow maps to use - more of these means each shadow map texel covers less area, producing better looking shadows, but may decrease performance. number of shadow maps = 3 From fd6f1b4856d62aad4feb4c133b8457a333fe5460 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Sun, 24 Aug 2025 20:47:45 +0200 Subject: [PATCH 3/4] clang-format --- components/sceneutil/mwshadowtechnique.cpp | 6 +++++- components/sceneutil/shadow.cpp | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/components/sceneutil/mwshadowtechnique.cpp b/components/sceneutil/mwshadowtechnique.cpp index d93cb6a188..354445152f 100644 --- a/components/sceneutil/mwshadowtechnique.cpp +++ b/components/sceneutil/mwshadowtechnique.cpp @@ -295,7 +295,9 @@ void VDSMCameraCullCallback::operator()(osg::Node* node, osg::NodeVisitor* nv) } // Push casting state and shadows bin while traversing the RTT camera if (auto* cast = _vdsm->getCastingStateSet()) + { cv->pushStateSet(cast); + } // bin has to go inside camera cull or the rendertexture stage will override it cv->pushStateSet(_vdsm->getOrCreateShadowsBinStateSet()); if (_vdsm->getShadowedScene()) @@ -303,7 +305,9 @@ void VDSMCameraCullCallback::operator()(osg::Node* node, osg::NodeVisitor* nv) _vdsm->getShadowedScene()->osg::Group::traverse(*nv); } cv->popStateSet(); // pop shadows bin stateset - if (_vdsm->getCastingStateSet()) cv->popStateSet(); + if (_vdsm->getCastingStateSet()){ + cv->popStateSet(); + } if (!_polytope.empty()) { auto& stack = cv->getProjectionCullingStack(); diff --git a/components/sceneutil/shadow.cpp b/components/sceneutil/shadow.cpp index dfd793ffd5..7475a9a061 100644 --- a/components/sceneutil/shadow.cpp +++ b/components/sceneutil/shadow.cpp @@ -99,7 +99,7 @@ namespace SceneUtil fakeShadowMapTexture->setShadowComparison(true); fakeShadowMapTexture->setShadowCompareFunc(osg::Texture::ShadowCompareFunc::ALWAYS); for (unsigned int i = mShadowSettings->getBaseShadowTextureUnit(); - i < mShadowSettings->getBaseShadowTextureUnit() + mShadowSettings->getNumShadowMapsPerLight(); ++i) + i < mShadowSettings->getBaseShadowTextureUnit() + mShadowSettings->getNumShadowMapsPerLight(); ++i) { stateset.setTextureAttribute(i, fakeShadowMapTexture, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE | osg::StateAttribute::PROTECTED); From 890251a757d2fd7803265a4fea1814b2c79b2a63 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Sun, 24 Aug 2025 22:21:05 +0200 Subject: [PATCH 4/4] remove unused captures --- components/sceneutil/mwshadowtechnique.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/components/sceneutil/mwshadowtechnique.cpp b/components/sceneutil/mwshadowtechnique.cpp index 354445152f..74cbd603e3 100644 --- a/components/sceneutil/mwshadowtechnique.cpp +++ b/components/sceneutil/mwshadowtechnique.cpp @@ -1631,10 +1631,8 @@ void MWShadowTechnique::cull(osgUtil::CullVisitor& cv) const auto mainComputeNF = cv.getComputeNearFarMode(); const auto mainTraversalNumber = cv.getTraversalNumber(); osg::ref_ptr mainFrameStamp = cv.getFrameStamp(); - const double adjNear = isCascaded ? cascaseNear : reducedNear; - const double adjFar = isCascaded ? cascadeFar : reducedFar; - parallelCullTasks.emplace_back(std::async(std::launch::async, [taskCamera, taskCb, taskSd, castsMask, mainComputeNF, mainTraversalNumber, mainFrameStamp, sm_i, computedRStart, computedREnd, adjNear, adjFar]() mutable { + parallelCullTasks.emplace_back(std::async(std::launch::async, [taskCamera, taskCb, taskSd, castsMask, mainComputeNF, mainTraversalNumber, mainFrameStamp, sm_i, computedRStart, computedREnd]() mutable { CascadeCullResult res; res.sd = taskSd; res.camera = taskCamera; res.cb = taskCb; res.sm_i = sm_i; res.rendered = false; res.rStart = computedRStart; res.rEnd = computedREnd; osg::ref_ptr localCv = new osgUtil::CullVisitor(); localCv->reset();