Skip to content

perf: RT hot-path optimizations — TLAS refits, opaque geometry, NRD churn, shadow-ray early-outs - #38

Open
ShugokiFable wants to merge 13 commits into
Gistix:mainfrom
ShugokiFable:perf/ultimate-4080
Open

perf: RT hot-path optimizations — TLAS refits, opaque geometry, NRD churn, shadow-ray early-outs#38
ShugokiFable wants to merge 13 commits into
Gistix:mainfrom
ShugokiFable:perf/ultimate-4080

Conversation

@ShugokiFable

@ShugokiFable ShugokiFable commented Jul 18, 2026

Copy link
Copy Markdown

Summary

A pass over the per-frame hot paths in GI/PT modes, motivated by poor FPS on high-end hardware (RTX 4080 Super, 9800X3D, 4K). Each commit is self-contained and fixes one specific, measured-by-inspection inefficiency. No architectural changes; no behavior changes outside the noted caveats.

Changes

  1. Disable NVRHI validation layer by defaultRendererSettings::ValidationLayer defaulted to true, wrapping every NVRHI call (binding-set validation per dispatch, duplicated state tracking per barrier) in release builds.
  2. Refit TLAS instead of full rebuild — the scene TLAS was fully rebuilt every frame with PreferFastTrace. Now created with AllowUpdate; per-ring-slot FNV hash of instance count + BLAS pointers selects PerformUpdate refits when the instance set is unchanged (transforms may still change freely).
  3. Stop BLAS refits for rigid world-transform changes — geometry descs store cluster-relative transforms and the TLAS instance transform is rewritten every frame, so a moving ref produced a bit-identical BLAS yet paid a refit. DirtyFlags::Transform removed from the BLAS-update trigger (kept for prev-transform bookkeeping).
  4. Mark guaranteed-opaque geometry GeometryFlags::Opaque — no geometry ever set the flag, so every ray ran the any-hit/candidate path (3 vertex loads + material load + texture sample) even against fully opaque walls/terrain. Predicate mirrors exactly what ConsiderTransparentMaterial[Shadow] treats as always-commit; alpha-state flips rebuild the BLAS via MarkDirty(Mesh).
  5. Cache NRD per-dispatch binding sets; drop full-res motion-vector copycreateBindingSet was called ~10×/frame inside the dispatch loop; the MV scratch copy duplicated a texture of identical format. Cache invalidates on texture-pointer identity (safe: binding sets refcount resources).
  6. Tame Reblur defaultsmaxStabilizedFrameNum 63→32, prepass blur radii 30/50→20/32.
  7. Default hair BSDF → ChiangBSDF — FarFieldBCSDF (RTXCR path) is disproportionately expensive as a default.
  8. Fix inverted if (child) in RootRenderNode::ResolutionChanged — the check skipped every existing child, so resolution changes never propagated to any pass (NRD resource dirty flags included).
  9. Stop flushing the log to disk on info-level messagesflush_on(info) plus per-frame logger::info in BLAS build paths did file I/O on the render thread.
  10. Shadow-ray early-out once fully occluded — ray-query loops kept Proceed()-ing through all remaining candidates after Beer-Lambert/alpha drove transmission to ~0; both the ray-query loops and ShadowAnyHit now terminate (early-out ordered before IgnoreHit).
  11. Coarse-mip (3) alpha test for shadow rays only — shadow candidates sampled mip 0 of every alpha texture per candidate; primary/bounce paths unchanged. Caveat: alpha-tested foliage/hair shadows get slightly softer edges.
  12. Gate LandLOD occlusion updates on actual input change; fix map data race — terrain-LOD BLASes were refit every frame while in range, and GetLandLODMeshUpdates() was written from worker threads without a lock.

Deliberately not done

  • SHaRC resolve slicing: SharcResolveEntry is not stateless per entry (cross-entry probe window, adjacent-level blends, per-entry frame-cadence counters, frameIndex % 32 fade bits). Slicing would change cache behavior, not just cost.
  • Async-compute restructure, RTXMU/BLAS compaction, GI half-res tracing, OMM/SER: larger efforts, left for follow-ups.

Testing

  • Builds clean with /W4 /WX (preset SkyrimAll, VS 2026, Release).
  • No in-game benchmark yet — opened for review and community A/B testing. Happy to rebase/split/drop individual commits; each is independent.

Target: GI and PT modes at 4K on Ada-class GPUs; no intended visual changes except the shadow-alpha mip note above.

Summary by CodeRabbit

  • Bug Fixes

    • Corrected resolution-change propagation to scene children.
    • Improved shadow-ray transparency so fully occluded cases stop early.
    • Ensured material opacity changes correctly trigger geometry rebuilds.
    • Prevented redundant terrain occlusion resubmissions and improved thread-safe updates.
  • Performance

    • Improved BLAS/TLAS update/refit decisions and update behavior.
    • Reduced motion-vector copying and reused NRD binding sets.
    • Avoided unnecessary occlusion and dispatch binding work.
  • Configuration

    • Disabled validation layers by default.
    • Reduced routine log flushing in release builds.
    • Updated default hair BSDF model.

The validation wrapper adds per-call CPU overhead to every NVRHI command
(command list recording, state tracking, binding validation) in all builds.
Keep the member and runtime branch in Renderer::Initialize so it can be
re-enabled for debugging.
The scene TLAS was fully rebuilt every frame with PreferFastTrace. Create the
TLAS with AllowUpdate and, per ring slot, hash the instance descriptor set
(instance count + BLAS pointers). When the set matches the previous build on
that slot, issue the build with PerformUpdate (refit): instance transforms
may change freely during an update, while count/BLAS membership may not
(enforced by the hash and asserted by NVRHI). A capacity resize or any
membership change falls back to a full rebuild.
BaseMesh::Update marks DirtyFlags::Transform whenever the mesh world transform
changes, and BLASCluster treated that as a BLAS refit (PerformUpdate) trigger.
But geometry descs store cluster-relative local transforms (rewritten every
frame by UpdateLocalTransform), and the world motion is carried by the TLAS
instance transform, which is written unconditionally every frame for every
valid cluster (BLASCluster::Update writes InstanceData; TopLevelAS::Update
rebuilds/refits the TLAS with fresh InstanceDescs). The BLAS content is
identical after a rigid move, so the refit was pure waste - one refit per
moving cluster per frame.

The Transform flag is kept for prev-transform/motion-vector bookkeeping and
SubIndexSegmentMesh sync; it just no longer triggers BLAS work.
Geometry descs were built with no flags, so every triangle generated a
non-opaque candidate and ran the any-hit transparency evaluation
(ConsiderTransparentMaterial/Shadow) even for plain opaque lighting materials.

BaseMesh::Update now recomputes an opacity predicate each frame from the
freshly-synced Properties and material data: lighting-family material
(Lighting/landscape/TruePBR), no alpha test/blend/additive/transmission, and
no refraction/kAssumeShadowmask shader flags. That exactly matches the
conditions under which both transparency functions unconditionally commit the
hit, so skipping the callback is behavior-preserving. When the computed state
changes (e.g. runtime alpha property toggle), the cluster is marked for a
BLAS rebuild since the flag is baked into the AS at build time.

SubIndexSegmentMesh syncs properties via SyncFrom instead of Update, so it
reapplies the flags there.
NRDIntegration created a resource binding set for every Reblur dispatch every
frame (~30 sets/frame), churning the descriptor heap. Cache one binding set
per dispatch index and only recreate it when the bound texture pointers
actually change (resolution change, resource recreation). NVRHI binding sets
hold references to bound resources, so cached entries keep their textures
alive and a pointer mismatch always means the texture identity changed.

Also drop the per-frame full-resolution copy of the motion vector texture
into a scratch texture: the scratch was created from the source desc with an
identical format (RG16_FLOAT game MVs for GI, RGBA16_FLOAT MotionVectors3D
for PT), and the copy performed no conversion, so the source can be bound
directly as IN_MV. Behavior change: if the source MV texture is missing, the
dispatch now aborts with the existing missing-resource warning instead of
denoising with stale scratch contents.
…0/50->20/32)

The stabilized-history pass at 63 frames and the oversized diffuse/specular
prepass blur radii cost significant denoiser time at 4K for marginal quality
gain over the NRD-recommended ranges. Remains user-configurable.
The far-field BCSDF hair model is considerably more expensive per hair hit
than Chiang. Switch the default; remains user-configurable.
The loop skipped every non-null child (inverted guard), so passes were never
notified of resolution changes from the root node - e.g. NRD kept stale
resource sizes after a resize. Matches the guards in SettingsChanged/Execute.
- XSEPlugin: flush_on(info) -> flush_on(warn); every per-frame info log was
  forcing a file flush.
- Scene::SetLogLevel: clamp flush_on to >= warn in release builds (debug
  builds keep flushing at the configured level).
- BLASCluster::BuildUpdate: demote the two per-frame 'already built this
  frame' / 'no dirty flags set' messages from info to trace.

Other logger::info call sites were checked and are init/resize/error paths
only.
Shadow rays with translucent candidates (water, glass, emissive windows)
accumulate Beer-Lambert transmission and keep traversing. Transmission only
decreases along the ray, so once all channels are <= 1e-3 the result is zero
shadow regardless of what remains:

- Ray query path (TraceRayShadowFinite): break out of the Proceed loop.
  Uncommitted + transmission~0 returns transmission*missed(1) ~ 0, matching
  the result of running the loop to completion.
- DXR pipeline path (ShadowAnyHit): AcceptHitAndEndSearch() once the payload
  is ~zero, which ends the search with a committed hit (missed stays 0).
ConsiderTransparentMaterialShadow fetched the diffuse texture alpha at mip 0
for the cutout/blend coverage test. Shadow rays don't need exact coverage -
use a fixed mip 3 (SHADOW_ALPHA_MIP) instead: better texture cache behavior
and no fine-mip residency dependency. The primary/bounce path
(ConsiderTransparentMaterial) keeps the exact mip-0 fetch, and the colored
transmission/specular tint fetches in the shadow path are unchanged.

Caveat: alpha-tested foliage/hair shadows get slightly softer/fuller edges
since mip-3 alpha is locally averaged.
…data race

LandLODMesh::UpdateOcclusion ran for every intersecting mesh every frame:
it wrote an occluder update entry and marked DirtyFlags::Vertex, forcing a
BLAS refit per LandLOD mesh per frame. The occluder is stateless given
(original vertices, transform, loadedRange), and loadedRange only moves in
discrete cell-sized steps, so cache the last submitted transform+loadedRange
and skip the resubmit (and refit) while both are within epsilon. The live
vertex buffer and BLAS remain valid in between.

Also fix a data race: UpdateOcclusion wrote
SceneGraph::m_LandLODMeshUpdates (eastl::unordered_map) directly from
parallel worker threads in SceneGraph::Update phase B. Route writes through
SceneGraph::SetLandLODMeshUpdate under a dedicated mutex. The reader
(LandLODOccluder pass) runs later on the render thread after the workers
have joined, so it needs no lock. Also demotes the per-frame 'No geometry'
log to trace.
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This change updates ray-traced shadow handling, mesh and acceleration-structure updates, Land LOD occlusion submission, NRD resource binding, renderer defaults, logging, scene traversal, and resolution propagation.

Changes

Ray-tracing and acceleration structures

Layer / File(s) Summary
Shadow transparency sampling and termination
shaders/raytracing/include/Rays.hlsli, shaders/raytracing/include/Transparency.hlsli
Shadow alpha tests use mip level 3, and fully occluded ray-query candidates terminate early.
Mesh opacity and BLAS update decisions
src/Core/BaseMesh.*, src/Core/SubIndexSegmentMesh.cpp, src/Core/BLASCluster.cpp
Geometry opacity flags, transform dirtiness, BLAS flags, fallback transforms, and diagnostic logging are updated.
TLAS instance-set refit tracking
src/Types/TopLevelAS.h
Per-ring-slot instance hashes determine whether TLAS builds use refit flags.

Render resource and occlusion flow

Layer / File(s) Summary
Land LOD occlusion update synchronization
src/Core/LandLODMesh.*, src/SceneGraph.h
Occlusion submissions include loaded-range state, skip unchanged inputs, and use a mutex-protected setter.
NRD motion-vector and binding-set reuse
src/Pass/NRD/NRDIntegration.*
NRD binds renderer motion vectors directly and reuses per-dispatch binding sets when texture lists remain unchanged.

Runtime defaults and diagnostics

Layer / File(s) Summary
Renderer and rendering defaults
src/Renderer.h, src/Types/Settings.h
Validation-layer and hair-BSDF defaults are changed.
Logging and resolution handling
src/Scene.cpp, src/XSEPlugin.cpp, src/Renderer/RootRenderNode.cpp
Log flush thresholds are adjusted, and resolution changes skip null children while notifying valid ones.
Scene traversal safety checks
src/Utils/Traversal.h
Switch-node indices and portal children are validated before traversal.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Renderer
  participant NRDIntegration
  participant BindingCache
  participant NRDDispatch
  Renderer->>NRDIntegration: provide current motion-vector texture
  NRDIntegration->>BindingCache: compare SRV/UAV texture vectors
  BindingCache-->>NRDIntegration: reuse or create binding set
  NRDIntegration->>NRDDispatch: issue compute dispatch with cached bindings
Loading

Possibly related PRs

Suggested reviewers: gistix

Poem

A rabbit hops through rays of light,
Coarse alpha makes shadows right.
Meshes rebuild when flags agree,
Cached binds race swiftly and free.
“Squeak!” says the bunny—clean and bright.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main performance-focused ray-tracing changes, including TLAS refits, opaque geometry tagging, NRD caching, and shadow-ray early-outs.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/Pass/NRD/NRDIntegration.cpp (1)

625-626: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Avoid vector copies by moving the local vectors.

Since srvTextures and uavTextures are local vectors that are no longer needed after building the binding set, you can eastl::move them into the cache. This skips a heap allocation and pointer copy for the vector contents.

♻️ Proposed refactor to move vectors
-				bindingCache.srvTextures = srvTextures;
-				bindingCache.uavTextures = uavTextures;
+				bindingCache.srvTextures = eastl::move(srvTextures);
+				bindingCache.uavTextures = eastl::move(uavTextures);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Pass/NRD/NRDIntegration.cpp` around lines 625 - 626, Update the
assignments in the binding-cache construction to move the local srvTextures and
uavTextures vectors into bindingCache.srvTextures and bindingCache.uavTextures
using eastl::move, preserving the existing values while avoiding copies.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Pass/NRD/NRDIntegration.cpp`:
- Around line 540-542: Update the cache sizing logic around
m_DispatchBindingCache to call resize(dispatchCount) unconditionally, rather
than only when dispatchCount grows. This must remove trailing binding sets when
the dispatch count shrinks so their texture references are released, while
preserving existing entries when the count is unchanged or increases.

In `@src/Types/TopLevelAS.h`:
- Around line 122-125: Update the buildFlags initialization in the TLAS
construction path to include AllowUpdate for full rebuilds when refit is false,
while preserving PerformUpdate for refits and PreferFastTrace in both cases. Use
the existing nvrhi::rt::AccelStructBuildFlags symbols.

---

Nitpick comments:
In `@src/Pass/NRD/NRDIntegration.cpp`:
- Around line 625-626: Update the assignments in the binding-cache construction
to move the local srvTextures and uavTextures vectors into
bindingCache.srvTextures and bindingCache.uavTextures using eastl::move,
preserving the existing values while avoiding copies.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3caf419b-529b-4b78-857e-ba3389c1ce90

📥 Commits

Reviewing files that changed from the base of the PR and between b291772 and c85a811.

⛔ Files ignored due to path filters (1)
  • shaders/raytracing/Common/ShadowAnyHit.hlsl is excluded by !**/*.hlsl
📒 Files selected for processing (17)
  • shaders/raytracing/include/Rays.hlsli
  • shaders/raytracing/include/Transparency.hlsli
  • src/Core/BLASCluster.cpp
  • src/Core/BaseMesh.cpp
  • src/Core/BaseMesh.h
  • src/Core/LandLODMesh.cpp
  • src/Core/LandLODMesh.h
  • src/Core/SubIndexSegmentMesh.cpp
  • src/Pass/NRD/NRDIntegration.cpp
  • src/Pass/NRD/NRDIntegration.h
  • src/Renderer.h
  • src/Renderer/RootRenderNode.cpp
  • src/Scene.cpp
  • src/SceneGraph.h
  • src/Types/Settings.h
  • src/Types/TopLevelAS.h
  • src/XSEPlugin.cpp

Comment on lines +540 to +542
if (m_DispatchBindingCache.size() < dispatchCount)
m_DispatchBindingCache.resize(dispatchCount);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Prevent GPU memory leaks by unconditionally resizing the cache.

If dispatchCount shrinks (e.g., due to a settings change like toggling an NRD feature), the trailing elements in m_DispatchBindingCache remain untouched because resize is only called when growing. These stale cached binding sets will continue to hold strong references to their bound textures. If a resolution change later recreates the texture pools, the orphaned textures from the old resolution will be kept alive indefinitely by these untouched trailing binding sets, causing a GPU memory leak.

Unconditionally resizing the cache ensures that any unused trailing elements are destroyed, safely releasing their texture references.

🐛 Proposed fix to unconditionally resize
-		if (m_DispatchBindingCache.size() < dispatchCount)
-			m_DispatchBindingCache.resize(dispatchCount);
+		m_DispatchBindingCache.resize(dispatchCount);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (m_DispatchBindingCache.size() < dispatchCount)
m_DispatchBindingCache.resize(dispatchCount);
m_DispatchBindingCache.resize(dispatchCount);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Pass/NRD/NRDIntegration.cpp` around lines 540 - 542, Update the cache
sizing logic around m_DispatchBindingCache to call resize(dispatchCount)
unconditionally, rather than only when dispatchCount grows. This must remove
trailing binding sets when the dispatch count shrinks so their texture
references are released, while preserving existing entries when the count is
unchanged or increases.

Comment thread src/Types/TopLevelAS.h

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/Core/BLASCluster.cpp (1)

64-117: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consolidate fallback logic to eliminate duplication.

The new fallback logic for when a reference loses its 3D (!object) is functionally identical to the existing fallback logic for an orphan cluster (!m_Owner). You can merge both cases into a single else branch, eliminating over 20 lines of duplicate code and simplifying the control flow.

♻️ Proposed refactor
-	if (m_Owner) {
-		const RE::NiAVObject* object = m_Owner->Get3D();
-		if (!object) {
-			// A reference can lose its 3D during unload before every mesh-destroy
-			// notification is consumed. Fall back to a member transform rather
-			// than dereferencing a null scene object on a render worker.
-			if (m_Members.empty()) {
-				m_Transform = Constants::kIdentityTransform;
-				m_PrevTransform = Constants::kIdentityTransform;
-				m_NeedsPrevInit = false;
-			}
-			else {
-				const auto* mesh = m_Members.front();
-				m_Transform = mesh->GetTransform();
-				m_PrevTransform = m_NeedsPrevInit ? m_Transform : mesh->GetPrevTransform();
-				m_NeedsPrevInit = false;
-			}
-
-			m_ClusterPosition = float3(m_Transform._14, m_Transform._24, m_Transform._34);
-			return;
-		}
-
-		float3x4 transform;
-		XMStoreFloat3x4(&transform, Util::Math::GetXMFromNiTransform(object->world));
-
-		if (m_NeedsPrevInit) {
-			m_PrevTransform = transform;
-			m_NeedsPrevInit = false;
-		} else {
-			m_PrevTransform = m_Transform;
-		}
-
-		m_Transform = transform;
-	}
-	else {
-		if (m_Members.empty()) {
-			m_Transform = Constants::kIdentityTransform;
-			m_PrevTransform = Constants::kIdentityTransform;
-			m_NeedsPrevInit = false;
-		}
-		else {
-			const auto& mesh = m_Members.front();
-			m_Transform = mesh->GetTransform();
-
-			if (m_NeedsPrevInit) {
-				m_PrevTransform = m_Transform;
-				m_NeedsPrevInit = false;
-			} else {
-				m_PrevTransform = mesh->GetPrevTransform();
-			}
-		}
-	}
+	const RE::NiAVObject* object = m_Owner ? m_Owner->Get3D() : nullptr;
+
+	if (object) {
+		float3x4 transform;
+		XMStoreFloat3x4(&transform, Util::Math::GetXMFromNiTransform(object->world));
+
+		if (m_NeedsPrevInit) {
+			m_PrevTransform = transform;
+			m_NeedsPrevInit = false;
+		} else {
+			m_PrevTransform = m_Transform;
+		}
+
+		m_Transform = transform;
+	}
+	else {
+		// A reference can lose its 3D during unload before every mesh-destroy
+		// notification is consumed. Fall back to a member transform rather
+		// than dereferencing a null scene object on a render worker.
+		// This also correctly handles orphan (no-owner) clusters.
+		if (m_Members.empty()) {
+			m_Transform = Constants::kIdentityTransform;
+			m_PrevTransform = Constants::kIdentityTransform;
+			m_NeedsPrevInit = false;
+		}
+		else {
+			const auto* mesh = m_Members.front();
+			m_Transform = mesh->GetTransform();
+			m_PrevTransform = m_NeedsPrevInit ? m_Transform : mesh->GetPrevTransform();
+			m_NeedsPrevInit = false;
+		}
+	}

	m_ClusterPosition = float3(m_Transform._14, m_Transform._24, m_Transform._34);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Core/BLASCluster.cpp` around lines 64 - 117, Consolidate the duplicated
fallback handling in the transform-update logic: when m_Owner is absent or
m_Owner->Get3D() returns null, route both cases through the same member/identity
transform path. Preserve the existing owned-object transform and
previous-transform initialization behavior, then keep the shared
m_ClusterPosition update after the unified branches.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Core/BaseMesh.cpp`:
- Around line 60-64: Update the transform dirty-check logic around BaseMesh’s
m_LocalTransform assignment so MatrixNearEqual compares against a separate
last-submitted or last-dirtied transform rather than the continuously updated
logical transform. Preserve m_LocalTransform as the current transform for other
consumers, and update the comparison baseline only when the transform is marked
dirty or submitted, allowing sub-threshold motion to accumulate until
DirtyFlags::Transform is raised.

---

Nitpick comments:
In `@src/Core/BLASCluster.cpp`:
- Around line 64-117: Consolidate the duplicated fallback handling in the
transform-update logic: when m_Owner is absent or m_Owner->Get3D() returns null,
route both cases through the same member/identity transform path. Preserve the
existing owned-object transform and previous-transform initialization behavior,
then keep the shared m_ClusterPosition update after the unified branches.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: fe0ffafd-34a4-4002-bd96-bfd33ea3b776

📥 Commits

Reviewing files that changed from the base of the PR and between c85a811 and 7044ad0.

📒 Files selected for processing (7)
  • src/Core/BLASCluster.cpp
  • src/Core/BaseMesh.cpp
  • src/Core/BaseMesh.h
  • src/Pass/NRD/NRDIntegration.cpp
  • src/Types/Settings.h
  • src/Types/TopLevelAS.h
  • src/Utils/Traversal.h
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/Types/TopLevelAS.h
  • src/Core/BaseMesh.h
  • src/Pass/NRD/NRDIntegration.cpp

Comment thread src/Core/BaseMesh.cpp
Comment on lines +60 to +64
if (!Util::Math::MatrixNearEqual(localTransform, m_LocalTransform))
MarkDirty(DirtyFlags::Transform);

m_LocalTransform = localTransform;
m_PrevLocalTransform = prevLocalTransform;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not advance the dirty-check baseline when suppressing a transform update.

MatrixNearEqual can treat each frame’s small motion as unchanged. Since m_LocalTransform is nevertheless overwritten at Line 63, continuous sub-threshold motion never accumulates and DirtyFlags::Transform may never be raised, leaving the BLAS transform stale. Compare against a separate last-submitted/last-dirtied transform, or accumulate deltas until the tolerance is exceeded.

Suggested direction
-	if (!Util::Math::MatrixNearEqual(localTransform, m_LocalTransform))
+	if (!Util::Math::MatrixNearEqual(localTransform, m_LastSubmittedLocalTransform))
+	{
 		MarkDirty(DirtyFlags::Transform);
+		m_LastSubmittedLocalTransform = localTransform;
+	}

Keep m_LocalTransform as the logical current transform if other consumers require it.

🧰 Tools
🪛 Cppcheck (2.21.0)

[error] 60-60: Found an exit path from function with non-void return type that has missing return statement

(missingReturn)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Core/BaseMesh.cpp` around lines 60 - 64, Update the transform dirty-check
logic around BaseMesh’s m_LocalTransform assignment so MatrixNearEqual compares
against a separate last-submitted or last-dirtied transform rather than the
continuously updated logical transform. Preserve m_LocalTransform as the current
transform for other consumers, and update the comparison baseline only when the
transform is marked dirty or submitted, allowing sub-threshold motion to
accumulate until DirtyFlags::Transform is raised.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant