Verify punctual shadow reuse, and measure what it is worth - #145
Merged
Merged
Conversation
Arc 2 #15 asked for punctual-shadow change detection. Arc 2 #4 had already built it: the residency mechanism is family-agnostic, a point face's light position and range are in the content descriptor, and per-face frustum filtering predates both. So this branch changes no engine code. What it owes instead is evidence, because "we believe the mechanism covers this family" is not the same claim as "it does". THE AUDIT. Every shadow SAMPLING input — spotViewProj, cascadeViewProj, selfShadowViewProj and all four *BiasMetrics arrays — is copied wholesale from the completed view set every frame, unconditionally, never from the plan and never gated on whether a family recorded. Every RASTER input is in the content descriptor. Nothing sits outside both, which is what makes a reused map unable to go stale through a sampling parameter. Shadow draws are also VDPM-free by construction (object.cpp clears the indirect handle and the GPU front), so a per-frame GPU-emitted index buffer — whose handle repeats out of a two-deep ring while its contents change — can never enter the descriptor. WHAT IS PINNED. Eight headless cases, each a way a convincing stale map could have survived. A RANGE-only change re-records all six faces: every matrix is identical across the two frames and every texel still differs, because the stored value is a ratio against that range — the case a transform-only descriptor would miss. A moving light re-records all six, since its position is an input to the radial depth each face stores. A slot inherited by another light re-records rather than lighting one light with its predecessor's shadows, pinned for a single spot view and for a whole cube, because a law that held for one entry could still leave five faces of a departed light's map in place. Bias metrics reuse, on both paths, which is the audit's boundary stated as a test. PER-FACE GRANULARITY is where the punctual saving lives, and it is caster-driven: a cube is compared face by face, but a light's own movement invalidates every face through the content law, so only a caster can change one face and not another. ShadowResidencyCasterMotionTest moves one rigid caster inside a single face and holds recorded=1 reused=5 at a median 0.007 ms, against 0.141 ms forced on the same scene — about twenty times less for an identical image. ShadowResidencyLightMotionTest is the ceiling: recorded=6 reused=0 at 0.172 ms, which is what punctual reuse cannot save. Two files rather than two phases of one animation, because the evidence is a per-family count and a scene that changed regime part-way would make every aggregate a mixture of two answers. SLOT CHURN is parked, not solved. Punctual slots are assigned per frame in gather order, so a light leaving compacts the ones after it and every inherited slot re-records a cube it could in principle have kept. That is safe — identity is part of the comparison, and two tests pin it — and its cost is currently unreachable: there is no runtime light removal or enable/disable path and gatherLights() walks a stable node order, so churn frequency in production is zero. A stable-assignment scheme would be machinery guarding against nothing. Trigger for revisiting it is recorded in the roadmap. Two traps found in the existing test fixture along the way, both now commented where the next person will hit them: populatedViews() gives all six point faces the same marked matrix, so their frusta are identical and every face admits every caster; and candidateDraws counts draws OFFERED while drawnDraws counts those the filter ACCEPTED, so a premise checked against the wrong counter reports six faces seeing an on-axis caster.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Arc 2 #15 asked for punctual-shadow change detection. Arc 2 #4 had already built it: the residency mechanism is family-agnostic, a point face's light position and range are in the content descriptor, and per-face frustum filtering predates both. So this branch changes no engine code. What it owes instead is evidence, because "we believe the mechanism covers this family" is not the same claim as "it does".
THE AUDIT. Every shadow SAMPLING input — spotViewProj, cascadeViewProj, selfShadowViewProj and all four *BiasMetrics arrays — is copied wholesale from the completed view set every frame, unconditionally, never from the plan and never gated on whether a family recorded. Every RASTER input is in the content descriptor. Nothing sits outside both, which is what makes a reused map unable to go stale through a sampling parameter. Shadow draws are also VDPM-free by construction (object.cpp clears the indirect handle and the GPU front), so a per-frame GPU-emitted index buffer — whose handle repeats out of a two-deep ring while its contents change — can never enter the descriptor.
WHAT IS PINNED. Eight headless cases, each a way a convincing stale map could have survived. A RANGE-only change re-records all six faces: every matrix is identical across the two frames and every texel still differs, because the stored value is a ratio against that range — the case a transform-only descriptor would miss. A moving light re-records all six, since its position is an input to the radial depth each face stores. A slot inherited by another light re-records rather than lighting one light with its predecessor's shadows, pinned for a single spot view and for a whole cube, because a law that held for one entry could still leave five faces of a departed light's map in place. Bias metrics reuse, on both paths, which is the audit's boundary stated as a test.
PER-FACE GRANULARITY is where the punctual saving lives, and it is caster-driven: a cube is compared face by face, but a light's own movement invalidates every face through the content law, so only a caster can change one face and not another.
ShadowResidencyCasterMotionTest moves one rigid caster inside a single face and holds recorded=1 reused=5 at a median 0.007 ms, against 0.141 ms forced on the same scene — about twenty times less for an identical image. ShadowResidencyLightMotionTest is the ceiling: recorded=6 reused=0 at 0.172 ms, which is what punctual reuse cannot save. Two files rather than two phases of one animation, because the evidence is a per-family count and a scene that changed regime part-way would make every aggregate a mixture of two answers.
SLOT CHURN is parked, not solved. Punctual slots are assigned per frame in gather order, so a light leaving compacts the ones after it and every inherited slot re-records a cube it could in principle have kept. That is safe — identity is part of the comparison, and two tests pin it — and its cost is currently unreachable: there is no runtime light removal or enable/disable path and gatherLights() walks a stable node order, so churn frequency in production is zero. A stable-assignment scheme would be machinery guarding against nothing. Trigger for revisiting it is recorded in the roadmap.
Two traps found in the existing test fixture along the way, both now commented where the next person will hit them: populatedViews() gives all six point faces the same marked matrix, so their frusta are identical and every face admits every caster; and candidateDraws counts draws OFFERED while drawnDraws counts those the filter ACCEPTED, so a premise checked against the wrong counter reports six faces seeing an on-axis caster.