Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ add_library(fireengine SHARED
src/graphics/mesh_topology.cpp
src/graphics/shadow_identity.cpp
src/graphics/shadow_lod_resolver.cpp
src/graphics/shadow_caster_deformation.cpp
src/graphics/shadow_render_view.cpp
src/graphics/shadow_diagnostics.cpp
src/graphics/shadow_view.cpp
Expand Down Expand Up @@ -414,6 +415,7 @@ add_executable(test_fire_engine
tests/graphics/test_shadow_view.cpp
tests/graphics/test_shadow_identity.cpp
tests/graphics/test_shadow_lod_resolver.cpp
tests/graphics/test_shadow_caster_deformation.cpp
tests/graphics/test_shadow_render_view.cpp
tests/graphics/test_texture.cpp
tests/graphics/test_sampler_settings.cpp
Expand Down
9 changes: 9 additions & 0 deletions docs/lod.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,15 @@ error — an average, which a locally bad region hides inside. Measured on a UV
**0.0085 against a true surface movement of 0.0170**: half the real deviation. Selecting a shadow
level against it would silently exceed any budget it was given.

**It measures the mesh as authored, and that bounds what it can be used for.** The deviation is
taken on the vertex data the simplifier was handed: bind pose, base weights, the buffer contents at
build time. For geometry deformed afterwards — skinned, morph-capable, or storage-vertex geometry a
compute pass rewrites — the number describes a mesh that is never rasterised, and skinning can carry
a vertex arbitrarily far from where its rest-pose deviation was taken, so it is not a loose estimate
there but an unfounded one. SH-04 therefore refuses to select for those casters at all
(`ShadowCasterDeformation`, classified in `graphics/shadow_caster_deformation.hpp`); a
deformation-aware error model is what would be needed to lift that restriction, and none exists yet.

So the LOD system records a second, separate per-cut number for shadows:

| Stage | Value | Where |
Expand Down
30 changes: 24 additions & 6 deletions docs/onboarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -616,12 +616,18 @@ and resolve pass:
- `--debug-shadow-depth`: show directional receiver/stored depth and cascade index.
- `--debug-velocity`: visualise the TAA motion-vector buffer (|x|, |y| scaled). Zero for a still
camera on rigid/skinned geometry; grows with camera or node-transform motion.
- (overlay only, no CLI flag) **LOD tint** and **Shadow LOD tint**: the first colours a mesh by the
level the *camera* draw selected, the second by the level its *shadow* draw selected (neutral grey
when the mesh casts no shadow). Reading them together is how SH-01's known defect — the shadow
level is chosen from the camera and replayed into every shadow view — shows up on screen.
- `--no-lod`: start with mesh LOD off (full detail everywhere). Seeds `RenderTunables::lodEnabled`, so
it governs the shadow selection too, not just the forward pass — the "before" half of an LOD A/B.
- `--debug-lod` / `--debug-shadow-lod` (also in the overlay): **LOD tint** and **Shadow LOD tint** —
the first colours a mesh by the level the *camera* draw selected, the second by the level its
*shadow* draw selected (neutral grey when the mesh casts no shadow). The shadow tint reads the
level of the FOCUSED shadow view (`--shadow-focus`), because since SH-03 a caster no longer has
"a" level: it selects per view. Reading the two together is how a caster carrying more shadow
detail than its view can resolve shows up on screen — the camera-derived defect these tints were
built to expose is fixed, so what they now show is the per-view selection itself.
- `--no-lod`: start with mesh LOD off (full detail everywhere). Seeds BOTH
`RenderTunables::lodEnabled` and `::shadowLodEnabled` — separate switches since SH-03 — so it is
the "full detail everywhere" half of an A/B. Use `--no-shadow-lod` when you need the shadow half
isolated with forward LOD still selecting; that separation is what makes the shadow-only
comparison in `tools/shadow_lod_sweep.sh` valid.
- `--capture <path.png>` / `--capture-frame N`: write the numbered frame's final swapchain content
(post-process + overlay, immediately before present) to a PNG and exit. Frame-numbered rather than
timed, so any machine captures the same render ordinal — though animation and physics still
Expand Down Expand Up @@ -891,6 +897,18 @@ the same change — most have a test or guard that will catch you, but not all.
shader must match the corresponding `ForwardBinding` / `ForwardGlobalBinding` / `ShadowBinding` /
`SkyboxBinding` / `PostProcessBinding` enumerator. `tests/render/test_pipeline_config.cpp` checks
the C++ side; the GLSL side is on you.
- **A shadow caster that deforms after the simplifier measured it may not select a level** (SH-04).
The deviation channel is measured on the mesh as authored — bind pose, base weights, the vertex
buffer at build time — so for skinned, morph-capable or storage-vertex geometry it describes a mesh
that is never drawn, and skinning can amplify the displacement without bound. Classification lives
in `graphics/shadow_caster_deformation.hpp` and rides on `ShadowGeometryRequest::deformation`,
which defaults to `Deformable` (the safe answer, like `worldScale`'s NaN — a producer that forgets
the field must not get the optimistic one). The resolver answers with
`ShadowLodReason::DeformableFallback`, full detail, and an INFINITE projected error, and stages no
hysteresis history. Do not express this by passing `lodEnabled = false`: that reports
`LodDisabled`, which is a user's toggle, and the panel would then explain a safety fallback with
somebody else's reason. There is deliberately **no shadow-proxy setter** — see `Object`'s header
for what a validated one must enforce before it comes back.
- **A uniform block bound by more than one shader is DECLARED once**, in a shared `shaders/*.glsl`
include — never hand-copied into each shader. `LightUBO` lives in
[`shaders/light_ubo.glsl`](../shaders/light_ubo.glsl); `shader.frag` and `skybox.frag` `#include`
Expand Down
1 change: 1 addition & 0 deletions docs/review-order.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Read these first when a change touches build configuration, CI, or local tooling
| `graphics/shadow_geometry_request.hpp` + `shadow_lod_resolver.hpp` + `shadow_lod_resolver.cpp` | SH-03's command seam. A shadow command carries a `ShadowGeometryRequest` (LOD span, conservative world scale, caster id + generation) and NO index buffer — an unresolved command that still carried one would be indistinguishable from a resolved one. `ShadowLodResolver` holds two stores that must not be conflated, both keyed on the full `(ShadowCasterId, generation, ShadowLogicalViewId)`: a per-FRAME cache — keyed on the LOGICAL view rather than the physical slot, which is why a cascade and its world-only twin (and a self slot's two depth layers) share one decision rather than agreeing by luck, with the caster and generation in the key because a view-only key would hand one caster's answer to another; and the cross-FRAME hysteresis history, STAGED during recording and committed only after a successful submit, so an abandoned frame leaves no dead band. Only a `Selected` reason writes history — a forced fallback says nothing about where the caster sits relative to its budget, and would erase the evidence the dead band is built on. An invalid key still draws but enters neither store. Slice 5 adds the READ-BACK the ShadowLod tint uses: `drawnResolution(group, key)` — what THAT family drew for this caster, or null. Combined on purpose, because asking "which level" and "did this pass draw it" separately invites forgetting the second, and forgetting it is invisible: a cascade and its world-only twin share one resolution by design but draw different casters (world-only excludes skinned ones, and cascades record first), so the level alone reports one pass's decision as another's. `frameResolution(key)` returns that shared decision regardless of provenance and is for inspecting the decision itself, not for attribution. Provenance is a per-family bitmask stored IN the same frame entry as the decision (one record, so it cannot exist without one), stamped at the draw itself and cleared per frame. |
| `graphics/frame_capture.hpp` + `frame_capture.cpp` | The Vulkan-free half of `--capture`: swapchain readback → tightly-packed RGBA8, plus the PNG write (stb_image_write). Two things it exists to get right, both silent when wrong: the BGRA/RGBA channel order, and the row PITCH (a linear image may pad rows — assuming `width * 4` shears the picture). An undersized mapping returns empty by contract, so the caller reports a failed capture rather than encoding whatever followed the buffer. Covered by `tests/graphics/test_frame_capture.cpp`. |
| `tools/assetgen/png.py` | Byte-deterministic RGBA PNG writer. Note WHY it exists: `zlib.compress` output varies with the zlib build, so a committed asset embedding a compressed texture could differ between machines. Image data goes into DEFLATE **stored** blocks (RFC 1951, fully specified) with a hand-written Adler-32. Don't "optimise" it into real compression. |
| `graphics/shadow_caster_deformation.hpp` + `.cpp` | **SH-04's classifier — the one place that decides whether a caster's error claim is about the mesh that gets rasterised.** Returns `Deformable` for three carriers: a skinned/morph-weighted INSTANCE, morph-CAPABLE geometry (deliberately independent of current weights — classifying by weights would swap a caster's error model mid-animation), and storage-vertex geometry whose vertices a compute pass rewrites (cloth). A free function, not an `Object` member, so it is testable against a real `Geometry` without a GPU — classification and the resolver's response to it are pinned by separate tests, so neither is proven only by the other. |
| `tools/shadow_lod_sweep.sh` | SH-03's calibration procedure as a script, so the budget and dead band can be re-derived rather than trusted. Read the header first: it names the three references that were rejected and why (`--no-lod` changes the visible geometry; a tiny budget still runs selection; whole-image PSNR dilutes a localised silhouette error). The metric compares the `--debug-shadow` visibility image and reports differing pixels against the reference's SHADOWED area, plus the worst pixel and an amplified difference image so "edge slivers" is something you look at. The dead-band half aggregates the per-frame `FE_LOG=render:debug` movement record over a whole animated run, and REVERSALS — not transitions — are the column that can justify a ratio. |
| `assets/shadow_lod/generate.py` | The SH-01 acceptance scenes. Two files from one content builder: a fully static baseline (reproducible poses → comparable screenshots and overlay numbers) and an animated variant (a qualitative stability loop only — an animated frame has no reproducible timestamp). `validate()` runs before every write and asserts the coverage claims the runbook makes (named nodes, all three light types, skin + joints/weights, a non-zero morph weight, a MASK material with a real texture and UVs, a distinct double-sided material, no `extras.Physics`, every node reachable). Add a case to the scene → add its assertion. |
| `assets/physics_demos/generate.py` | Owns only the demo SCENES (the authored numbers `tests/physics/test_demos.cpp` replays); the mechanics come from `tools/assetgen/`. Bootstraps `sys.path` from `__file__`, not the working directory, so it runs from anywhere. Run by the `physics_demos` build target before `assets` copies — output must stay byte-identical, so verify by re-running and diffing hashes rather than eyeballing JSON. |
Expand Down
13 changes: 11 additions & 2 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,17 @@ the plan; the priority order is its § Suggested priority.
calibration (slice 6: budget 1 texel, no dead band, both measured against a stated threshold,
CSM-only — see
[`shadowplans.md`](shadowplans.md) § SH-03).
- **SH-04** — deformation / proxy policy (skinned, morphed, cloth: no invalid error claims, explicit
conservative full-detail fallback).
- **SH-04** — deformation / proxy policy. **Deformation half landed** (`shadow-deformation-policy`):
skinned, morph-capable and storage-vertex casters are classified `Deformable` and resolve to full
detail with their own `DeformableFallback` reason, infinite projected error and no hysteresis
history — closing a hole SH-03 left open, where deformable casters selected levels from a
BIND-POSE deviation (live, not theoretical: BrainStem transitioned a skinned caster's shadow level
within seconds). Calibration re-run: the error column did not move (the demo's deformable casters
are not in the measured directional view), but self-shadow cost went 184/1248 → 1248/1248 and the
cascade group 59.9% → 68.2% of full detail; the 0.1% threshold still selects budget 1.
**Proxy half still open** — `Object::shadowGeometry` was REMOVED rather than documented as unsafe,
so there is currently no way to author a proxy; reinstating a validated setter (deformation
compatibility, morph contract, proxy-derived bounds, enforced at load time) is what closes it.

**Milestone 2 — shadow silhouette correctness**
- **SH-05** — material-aware casters (alpha-mask cutout, double-sided sheets).
Expand Down
Loading
Loading