Skip to content

Reuse a shadow map when its content has not changed - #144

Merged
nnewson merged 1 commit into
mainfrom
shadow-residency-reuse
Sep 4, 2026
Merged

nnewson merged 1 commit into
mainfrom
shadow-residency-reuse

Conversation

@nnewson

@nnewson nnewson commented Sep 4, 2026

Copy link
Copy Markdown
Owner

The shadow pass has known what each view WOULD draw since the plan landed; it just drew it anyway. Every view came out Recorded, which is what let the preparation restructure be verified as decision-identical, and left the saving itself for this branch.

ShadowResidencyStore records what each physical view's depth image holds. Preparation compares this frame's prepared content against it and marks each view Reused or Recorded; recordPass skips a reused view entirely — no barrier, no clear, no draw — and a family that records nothing opens no timing span, so its GPU time is absent rather than zero.

The store belongs to Shadows, beside the images it describes. That is the whole invalidation story: recreating the images reconstructs the record, so there is no invalidate() for anyone to forget, and no window in which the two can disagree. If in-place recreation ever arrives, the targets and the store move into one private aggregate together.

Two rules live inside the store rather than at its call site. Only a Recorded view commits: a Reused view never touched its image, and its prepared work differs in the diagnostic fields, so adopting it would leave the record describing a frame that wrote nothing. An Invalid slot keeps what it had: nothing recorded means nothing overwrote the image.

Adoption sits between submission and presentation, and submitAndPresent is split in two to make that expressible. Once submit2 returns the GPU owns the work; presentation is a separate act that can fail — raii presentKHR throws on an out-of-date swapchain — so a resize could have thrown past the commit while the frame's depth was already being rasterised, leaving the store describing the previous frame. A later frame preparing that content would then reuse an image holding something else: the exact failure this whole item exists to prevent, arriving through the error path. Adoption is also noexcept, moving the recorded views out of the plan rather than copying them, because after a submit there is no useful answer to a failed allocation.

shadowResidencyReuseEnabled (--no-shadow-reuse, or the overlay's "Reuse unchanged shadow views") forces every engaged view to record. It is scheduling, not pixels, so it is an argument to the law and never part of the content descriptor — a frame recorded with reuse off commits as usual and is reusable the moment it is back on. Each SH-01 row carries the disposition it ended up with, because zero raster passes alone cannot separate "reused" from "never engaged".

Measured on the purpose-built ShadowResidencyTest scene (one static point light, six cube faces, camera parked, macOS/arm64 + MoltenVK): forced record holds the point family at a median 0.264 ms per frame while reuse issues no timing span at all. The cold recorded frame, a reused frame 240 frames later, and a same-state forced-record frame are byte-identical captures, VUID-free.

What this does not buy is CPU preparation: a reused view is filtered, resolved and observed in full, because the comparison needs the work that produces its operand. The durable win is therefore punctual rather than CSM — a cascade's matrix moves with the camera.

The shadow pass has known what each view WOULD draw since the plan
landed; it just drew it anyway. Every view came out Recorded, which is
what let the preparation restructure be verified as decision-identical,
and left the saving itself for this branch.

ShadowResidencyStore records what each physical view's depth image
holds. Preparation compares this frame's prepared content against it and
marks each view Reused or Recorded; recordPass skips a reused view
entirely — no barrier, no clear, no draw — and a family that records
nothing opens no timing span, so its GPU time is absent rather than
zero.

The store belongs to Shadows, beside the images it describes. That is
the whole invalidation story: recreating the images reconstructs the
record, so there is no invalidate() for anyone to forget, and no window
in which the two can disagree. If in-place recreation ever arrives, the
targets and the store move into one private aggregate together.

Two rules live inside the store rather than at its call site. Only a
Recorded view commits: a Reused view never touched its image, and its
prepared work differs in the diagnostic fields, so adopting it would
leave the record describing a frame that wrote nothing. An Invalid slot
keeps what it had: nothing recorded means nothing overwrote the image.

Adoption sits between submission and presentation, and submitAndPresent
is split in two to make that expressible. Once submit2 returns the GPU
owns the work; presentation is a separate act that can fail — raii
presentKHR throws on an out-of-date swapchain — so a resize could have
thrown past the commit while the frame's depth was already being
rasterised, leaving the store describing the previous frame. A later
frame preparing that content would then reuse an image holding something
else: the exact failure this whole item exists to prevent, arriving
through the error path. Adoption is also noexcept, moving the recorded
views out of the plan rather than copying them, because after a submit
there is no useful answer to a failed allocation.

shadowResidencyReuseEnabled (--no-shadow-reuse, or the overlay's "Reuse
unchanged shadow views") forces every engaged view to record. It is
scheduling, not pixels, so it is an argument to the law and never part
of the content descriptor — a frame recorded with reuse off commits as
usual and is reusable the moment it is back on. Each SH-01 row carries
the disposition it ended up with, because zero raster passes alone
cannot separate "reused" from "never engaged".

Measured on the purpose-built ShadowResidencyTest scene (one static
point light, six cube faces, camera parked, macOS/arm64 + MoltenVK):
forced record holds the point family at a median 0.264 ms per frame
while reuse issues no timing span at all. The cold recorded frame, a
reused frame 240 frames later, and a same-state forced-record frame are
byte-identical captures, VUID-free.

What this does not buy is CPU preparation: a reused view is filtered,
resolved and observed in full, because the comparison needs the work
that produces its operand. The durable win is therefore punctual rather
than CSM — a cascade's matrix moves with the camera.
@nnewson
nnewson merged commit b8d6005 into main Sep 4, 2026
5 checks passed
@nnewson
nnewson deleted the shadow-residency-reuse branch September 4, 2026 21:03
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