Skip to content

A clear covers every layer of the bound render target view (Little Nightmares point-light shadows) - #808

Closed
keanu-stitch8 wants to merge 2 commits into
seedds:mainfrom
keanu-stitch8:layered-clears
Closed

keanu-stitch8 wants to merge 2 commits into
seedds:mainfrom
keanu-stitch8:layered-clears

Conversation

@keanu-stitch8

@keanu-stitch8 keanu-stitch8 commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

What

NVN clears every layer of the render target view that is bound (the whole texture when the view sets no layer range). The host folds a clear into the next Metal pass's load action, and a pass that is not layered (renderTargetArrayLength unset) clears only its one attached layer. Layers above the first of an array, cube or 3D target kept their old or zero content.

rp_begin_ex_ already clears an attachment that is larger than the pass in a pass of its own (Clears of attachments larger than the render pass, Bayonetta 2). This PR does the same for layers. A cleared attachment whose view covers more than one layer, in a pass that is not layered, is cleared in a pass of its own with renderTargetArrayLength set to the view's layers (att_layers), then loaded in the real pass. A view limited to some layers clears only those. Layered passes and single-layer targets are unchanged.

Found on

Little Nightmares (01002FC00412C000, 1.0.4, Unreal Engine 4, SDK 3.5.1, provisional profile):

  • Each point light's one-pass cube shadow (geometry shader ffc21034b958840b, emitting every triangle into six faces) clears the 512x512 D32F_S8 cube in the pass that captures the vertex shader's output. That pass is not layered.
  • Only face 0 (+X) was cleared. Faces 1-5 kept the zero depth tex_zero_fill gives a new texture, so every fragment of the layered pass failed the depth test there.
  • The projection's comparison sampler then read those faces as fully shadowed. The lantern and the lighter lit only one side of the room, with straight edges at the face boundaries.
  • With the fix, all six faces hold depth (0.986–0.999 against a light radius of 450), the static and final cubes differ only by the moving character, and the room is lit all around with Six's shadow on the floor.

Diagnostics (first commit)

DM_CTL texraw saved slice 0 only, which hid the bug (face 0 looked plausible). It now saves level 0 of every slice, one after another, with the slice count after the header's four fields when there is more than one. A single-slice file is unchanged.

Tests

  • New t_clear_layers (production replay, MTL_DEBUG_LAYER=1) covers:
    • a depth clear of a cube with no view (all six faces) and through a one-face view (that face only);
    • colour clears of a 2D array with no view, from layer 1 on, and through a two-layer view;
    • a plain 2D target, which stays one layer.
  • Without the fix, 4 of the 6 cases fail, exactly on the layers above the first. With it: 30 checks, 0 failed.
  • make -C dm/gfx/tests GAME=010015100B514000 run: all pass (on the fork's tree, which carries this change).
  • t_clear_ds, t_clear_tex, t_clear_layers and dm/tools/tests/test_docs.py pass on this branch.
  • Regression harness: scripts/regress.py check --games was not run. This Mac has no NSP folders laid out for the harness (<name> [<id>] with <id>_build) for the other local games. The change only acts when a cleared attachment has more than one layer in a pass that is not layered, and the pass dump prints C … cleared in a pass of its own (… N layers …) whenever it does.

🤖 Generated with Claude Code

keanu-stitch8 and others added 2 commits October 31, 2026 23:01
texraw saved slice 0 only, so a cube, array or layered render target
could be checked only through its first face or layer. It now saves level
0 of every slice one after another (a cube's six faces), with the slice
count after the header's four fields when there is more than one. A
single-slice texture's file is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NVN clears every layer of the render target view that is bound (the whole
texture when the view sets no layer range). A clear folded into a Metal
pass's load action clears only the layers the pass attaches, and a pass
that is not layered attaches one, so the other layers of an array, cube or
3D target kept their old or zero content.

Little Nightmares (01002FC00412C000, Unreal Engine 4) clears the 512x512
cube of each point light's one-pass shadow (geometry shader
ffc21034b958840b) in the pass that captures the vertex shader's output,
which is not layered. Only the +X face was cleared; faces 1-5 kept the zero
depth they were created with, every fragment the layered pass drew there
failed the depth test, and the comparison sampler read them as fully
shadowed: the lantern and the lighter lit only one side of the room, with
straight edges.

rp_begin_ex_ now clears such an attachment in a pass of its own with
renderTargetArrayLength set to the view's layers (att_layers), then loads
it in the real pass, as it already did for attachments larger than the
pass. A view limited to some layers clears only those; layered passes and
single-layer targets are unchanged. t_clear_layers runs the production
replay on a cube depth target and a 2D array colour target, with and
without layer ranges (4 of its 6 cases fail without the fix).

Checked on the Mac: Little Nightmares' shadow cube holds depth on all six
faces after the fix (texraw), and the lantern room is lit all around with
Six's shadow on the floor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
seedds added a commit that referenced this pull request Nov 1, 2026
NVN clears all layers of the bound render-target view. A load-action
clear in a non-layered Metal pass covered only its first layer, leaving
the remaining shadow-cube faces and array layers unchanged.

Extend the existing standalone clear pass to cover the view's full layer
range before loading it in the draw pass. Extend texraw diagnostics to
save every array slice and cube face, and test the production replay.

Integrate with #730 by marking a raster clear only when the draw pass's
actual load action clears. A clear already completed in a standalone
pass must not keep a stale tile store over later shader image writes.
The literal GPU regression fails four checks without this tracking fix.

Before rebasing, the full Metal graphics suite, LLVM 23 lint, all 11
documentation tests and Breath of the Wild's full outdoors route passed.
Those comparisons used the documented Xcode 27.2 compiler override on
both revisions against the old compiler lock. After rebasing onto the
Xcode 27.2 pin and #730, focused lint and documentation checks pass, as
do 120 image-store checks and 30 layered-clear checks under Metal
validation. The maintainer stopped the remaining Tears of the Kingdom
and Pokemon Scarlet regression runs; document that partial coverage.

Co-authored-by: Riski Aditiawarman <62930684+keanu-stitch8@users.noreply.github.com>
@seedds

seedds commented Nov 1, 2026

Copy link
Copy Markdown
Owner

Landed on main as ffdf67de. Thank you, @keanu-stitch8, for the diagnosis, implementation and GPU coverage. Your clear implementation, texraw improvement and tests are retained, and you are credited as co-author.

This is good general graphics design: NVN clears the complete bound view, so the Metal load-action path must cover that view. High confidence in the original bug and fix; this is a generalized root-cause correction with no game-specific rule or renderer workaround.

The rebase onto #730 exposed one integration issue. A clear already completed in a standalone pass was still counted as a tile write in the following pass, retaining a stale store over shader image writes. Store tracking now uses the following pass's actual clear load action. The added literal GPU case failed four assertions before the correction and passes afterward.

Validation:

  • Before rebase: full Metal graphics suite, LLVM 23 lint and all 11 documentation tests passed. Breath of the Wild's full 110-second route passed: 58.4 → 58.0 fps, p99 33.56 → 33.52 ms, matching screenshots and zero red diagnostics.
  • After rebase: image-store GPU test 120/120, layered-clear GPU test 30/30, focused LLVM lint with no new findings, and all 11 documentation tests passed.
  • Regression risk was moderate because this is a shared path with a narrow trigger. The impact report selected Breath of the Wild, Tears of the Kingdom and Pokemon Scarlet. The remaining game checks were stopped at the maintainer's request: Tears of the Kingdom's candidate paused after focus loss with all GPU fences completed, its foreground-controlled repeat was canceled, and Scarlet's runtime comparison was not completed. Those two games are not claimed as validated.
  • The earlier runtime comparison used the documented Xcode 27.2 compiler override against the old compiler lock. Fresh main now pins Xcode 27.2. Validation notes and the completed BotW record are committed.

Closing after landing the reviewed change on main.

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.

2 participants