Skip to content

Shadow lod diagnostics - #123

Merged
nnewson merged 4 commits into
mainfrom
shadow-lod-diagnostics
Jul 26, 2026
Merged

Shadow lod diagnostics#123
nnewson merged 4 commits into
mainfrom
shadow-lod-diagnostics

Conversation

@nnewson

@nnewson nnewson commented Jul 26, 2026

Copy link
Copy Markdown
Owner

No description provided.

nnewson added 4 commits July 26, 2026 16:31
A caster's shadow LOD is selected from the **camera** view and the
single resulting draw command is replayed into every shadow view
(`object.cpp`, `selectLod` on `cameraPosition` / `proj[1,1]` /
`viewportHeight`). SH-03 fixes that; nothing today can *measure* it —
one `ProfilePass::Shadow` total can't say whether cost moved between
cascades, punctual lights, or self-shadowing, and no counter reports
which geometry level a given shadow view rasterised. This adds the
evidence layer first, deliberately, so the fix can be judged rather than
asserted.
assets/physics_demos/generate.py owned both the demo scenes and the
machinery to build them. The shadow-LOD arc needs the same machinery, so
the geometry primitives, quaternion helpers and self-contained-glTF
assembler move to tools/assetgen/ and the script keeps only its scenes.
The move is output-preserving: the committed .gltf regenerate
byte-for-byte from any working directory (Scene now takes its
`generator` string instead of hard-coding one script's name).

On top of that, a correctness fix that is NOT output-preserving. Three
primitives wound their triangles against their authored normals:
subdivided_box (4 of 6 faces), cylinder (both caps), and sphere — the
last pre-existing, and shipped by the physics demos, which is why
RestitutionDemo and StaticMeshDemo change here. The defect is invisible
in the forward pass, which shades from the authored normal, and corrupts
only the shadow pass, which culls by winding: it produced self-shadow
acne, visible triangle edges, and holes punched out of ground shadows.

Scene.mesh() now asserts per-triangle winding against the vertex normals
for every mesh from every generator, so no future primitive can repeat
it.
Adds what SH-01 needs before any selection policy changes:

- makeForwardPushConstants — one DrawCommand→push-constant packer shared
by the forward and transmission recorders. They had drifted:
transmission never set lodLevel, so every transmissive draw reported LOD
0 to the debug tint. Push-constant offsets are now static_asserted.
- shadowLodLevel through to DebugView::ShadowLod, tinting each mesh by
the level its shadow draw picked, with a kNoShadowLod sentinel so a
non-caster reads neutral grey instead of "full detail chosen".
- A Shadows overlay panel: per view family and physical slot, raster
passes and drawn/candidate draws + triangles kept visually separate from
the LOD histograms (work vs selection samples), timings from
shadowProfilePass(group).
- extras.Shadow = {"Casts": false} authors a receive-only surface. A
wide flat caster writes its own depth into every cascade and greys its
whole area out; the built-in -f floor has always set this in code and an
authored scene had no way to say it. The block is closed — any other
key, including Receives, is rejected rather than ignored.

Shadow eligibility lives on Object::GeometryBindings, not Geometry: a
Geometry is shared by every instance that draws it, so a flag there
could not describe two nodes instancing one mesh as caster and receiver.
Two generated scenes: ShadowLodDemo (fully static — the measurement
baseline, so overlay numbers and captures are comparable run to run) and
ShadowLodMotionDemo (adds motion for a qualitative no-chatter loop; an
animated frame has no reproducible timestamp, so it gets no reference
image). validate() checks the coverage claims the runbook makes before
either is written — named nodes, all three light types, skin and morph
data, a MASK material with a real texture and UVs, front-lit sun, every
LOD exposure above the engine's 512-triangle threshold and connected.

--capture <path.png> writes a frame and exits, so the references are
regenerated by command rather than grabbed from a window. It copies the
final swapchain image (post-process + overlay, immediately before
present), requests TRANSFER_SRC only when asked for and only after
checking the surface supports it, and rejects any swapchain format that
isn't 8-bit BGRA/RGBA rather than guessing the channel order.
--capture-frame counts frames, never seconds. --no-lod gives the
full-detail half of the A/B and governs shadow selection too.

The PNG encodes deterministically, but Vulkan output is not guaranteed
byte-identical across GPUs or drivers, so these are documented
baselines, not a hash gate — the tolerance arrives with SH-02's
shadow-texel metric.
@nnewson
nnewson merged commit 7698c9e into main Jul 26, 2026
4 checks passed
@nnewson
nnewson deleted the shadow-lod-diagnostics branch July 26, 2026 20:12
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