fix(render): VM3 review fixes - pin the filmic decodes, re-derive vignette for linear, name the authored sun colour

Fix round from Opus review of 87677f9c (APPROVE WITH FIXES):

1. (A1/A3) Hardened the atmospheric_filmic.frag / atmospheric_bloom_
   downsample.frag shader-source pinning test: asserts the exact decode
   call count (6 - three in lowFusedScene, three in main's non-fused
   branch), that the 2.2 display-gamma exponent and 1/2.2 inverse in
   atmospheric_common.glsl are formatted FROM AtmosphericColorPipeline
   .DisplayGamma (so shader literal and CPU-tested value cannot drift),
   that the 0.18 contrast pivot in atmospheric_filmic.frag is formatted
   from AtmosphericColorPipeline.LinearMidGrey, and pins
   AtmosphericPostProcessGraph.BloomKneeLinear/BloomThresholdLinear as
   exact literals (0.73f / 1f). Also removed a stray trailing "()" from
   an existing comment in atmospheric_filmic.frag that was inflating the
   decode-call count to 7.

2. (B1) Re-derived the "vignette-strength" default for the linear-light
   post stack. The vignette multiply now happens on linear colour before
   the final encode, so a corner factor of (1 - strength) displays as
   (1 - strength)^(1/2.2), not (1 - strength) directly. The accepted
   look was strength 0.12 under the OLD gamma-space pipeline: a 12%
   on-screen corner darkening. Under strength 0.12 in the new linear
   pipeline that same 0.88 corner multiplier would only display as
   0.88^(1/2.2) ~= 0.9435 (5.6% darkening - visibly weaker). Solving
   (1 - strength)^(1/2.2) = 0.88 gives strength = 1 - 0.88^2.2 ~= 0.245,
   which reproduces the accepted 12% corner darkening. Since
   RenderPackSettingValueCodec requires every declared default to be
   step-aligned from the minimum and 0.245 is not a multiple of the old
   0.01 step, the step also moves to 0.005 (a finer slider, not
   coarser) so the exact derived default is a valid grid point -
   verified by running the ExternalTierTwoPackCanRenameEveryOwnedId
   AndShaderAsset validation test, which failed with "invalid default
   value" before this correction. Also updated the matching fallback in
   AtmosphericPostProcessGraph.FromDescriptor (0.12f -> 0.245f) for
   consistency, and added
   AtmosphericColorPipelineTests.VignetteDefaultReproducesTheAccepted
   TwelvePercentCornerDarkening pinning encode(1-0.245) ~= 0.88.

3. (A4) Renamed VolumetricShaftFrameParameters.LinearSunColor ->
   AuthoredSunColor in VolumetricShaftQuality.cs (internal, 2 references,
   both in that file - safe). Left LightSource.ColorLinear unrenamed:
   grep shows 13 files depend on it (GlobalLightPacker, SceneLightingUbo,
   LightBake, LightManager, EnvCellRenderer, RenderingDiagnostics, and
   several Core tests) across the shared retail default-path lighting
   UBO pipeline - renaming it is out of VM3's pack-only scope and would
   touch the mandatory-unchanged default path. Added a pointer comment
   on the field in LightSource.cs (and a one-line note at its
   WorldRenderFrameBuilder.cs call site) documenting the same
   display-space-not-linear fact and explaining why the rename is
   deferred to its own default-path colour-space pass.

4. (B4) Added a citation beside acesFitted in both atmospheric_filmic
   .frag and its C# mirror (AtmosphericColorPipeline.AcesFitted):
   Krzysztof Narkowicz, "ACES Filmic Tone Mapping Curve" (2016). The fit
   takes linear scene light in and returns linear display light in
   [0,1] - it does not itself gamma-encode. Evidence: acesFitted(0.80 *
   decode(0.46)) = 0.2064 un-encoded versus the accepted 0.51 on screen.

5. (B2) Rewrote the VM3 section of docs/plans/2026-08-22-visualmaster-
   campaign.md with the shipped truth in place of the pre-implementation
   guess: exposure stays 0.80 (at exposure 1.0 the linear pipeline maps
   gamma-0.5 to 0.6017, essentially the same 0.6163 the owner called too
   bright), bloom threshold stays 1.0 (a fixed point of both exponents),
   knee moves 0.45 -> 0.73, vignette-strength moves 0.12 -> 0.245. Added
   the old-vs-new curve table at exposure 0.80 across ten gamma inputs.
   Replaced the acceptance criteria's "new automated test on the
   recording RHI" with the CPU mirror + shader-source pins actually
   used, and recorded that the real-frame masked capture WAS run
   (retail vs High-with-every-effect-neutral, artifacts/vm3):
   independently re-verified by re-running the pixel diff against the
   checked-in screenshots - 110,561 px at |delta|=1 and exactly 95
   pixels at |delta|>=5, confined to foliage-canopy silhouette edges
   against sky with nothing on any ground/building/water surface. Noted
   the Stage-1 luminance table re-capture is still owed at the owner
   gate.

6. (B3) Corrected docs/plans/2026-08-21-atmospheric-rendering.md's VM3
   summary sentence: the bloom intermediate is already linear after
   extraction (no separate "bloom read" decode), and the neutral-preset
   claim is now phrased as a measured numerical identity (<=1 LSB on a
   real frame) rather than an unqualified "is" statement.

7. (A5) Corrected toolchain attribution: tools/compile-shaders.ps1 used
   the managed Silk.NET.Shaderc path (shaderc_shared.dll) to compile in
   both this round and the original VM3 commit - a Vulkan SDK glslc was
   detected and its path recorded, but the managed compiler is what
   actually ran. Regenerating this round only changed the atmospheric_
   filmic frag stage's manifest hash (comment-only edits); the compiled
   .spv bytes are unchanged, and every retail-oracle shader
   (mesh_modern, terrain_modern, mesh_detail, etc.) remains untouched.

8. Replaced an invented motive in the atmospheric_filmic.frag contrast-
   pivot comment ("rounded up for a stronger gamma-space contrast
   feel") with the actual reason: the previous 0.5 was simply the [0,1]
   midpoint of the standard contrast formula, not a deliberately chosen
   value; in linear the perceptual mid-grey is 0.18.

Verify: Release build 0 warnings / 0 errors. App hermetic-filter tests:
5970 passed / 0 failed / 0 skipped. VulkanShaderManifestTests: 7/7 pass
(retail-oracle SPIR-V byte-identical; only the atmospheric_filmic frag
manifest hash changed, no .spv bytes changed).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-22 23:40:48 +02:00
parent 87677f9c4f
commit 51178f7c77
12 changed files with 151 additions and 21 deletions

View file

@ -575,9 +575,12 @@ no clipping, haloing at the world/UI edge, stale frame, or resource leak; the
slice meets its preset GPU/VRAM budget.
Campaign VM VM3 (2026-08-22) moved the post stack to linear light: every
world/ray/volumetric/bloom read is decoded with the 2.2 display assumption,
tonemap/grade/vignette run in linear, and the result is re-encoded; the
neutral preset is numerically the pack-off image.
world/ray/volumetric read is decoded with the 2.2 display assumption; the
bloom intermediate is already linear (decoded once at extraction, so
downstream blur/composite reads need no further decode). Tonemap/grade/
vignette run in linear and the result is re-encoded; the post stack's
neutral settings are a numerical identity (measured on a real frame at
Campaign VM VM3: ≤1 LSB).
### Slice 2 — Tier-1 screen-space sun rays

View file

@ -210,13 +210,55 @@ compensation.
vignette 0) — the existing "every effect can be set to neutral" acceptance
now actually holds numerically, and is asserted by a test.
- Opinionated defaults (what the owner accepted at 0.80 exposure) are
re-derived in linear and presented for the visual gate; expect exposure to
return to ~1.0 and the bloom threshold to move up.
re-derived in linear and presented for the visual gate. **Shipped truth
(not the pre-implementation guess above):** exposure stays at **0.80**
at exposure 1.0 the linear pipeline maps gamma-0.5 to 0.6017, essentially
the same 0.6163 the owner called too bright under the old gamma-space
pipeline, so 1.0 reproduces the rejected look, not a corrected one. The
bloom **threshold stays 1.0** (a fixed point of both `pow(x,2.2)` and
`pow(x,1/2.2)` — 1.0 decodes and encodes to 1.0), while the **knee moves
0.45 → 0.73** (`AtmosphericPostProcessGraph.BloomKneeLinear`) and
**vignette-strength moves 0.12 → 0.245** (re-derived so the same accepted
12% corner darkening survives the encode step). See
`AtmosphericColorPipelineTests` for the pinned numbers behind each of
these.
**Acceptance:** neutral preset ≡ pack-off within tolerance (new automated
test on the recording RHI plus VM0's masked tool on a real capture); Stage-1
luminance table re-captured; owner visual gate: "same look as accepted, no
clipping, highlights roll off". Budget unchanged (two `pow` per pixel).
Old-vs-new curve at the shipped exposure 0.80 (gamma input →
old-pipeline display value / new-pipeline display value; filmic
strength 1, saturation/contrast 1, no vignette — i.e. `acesFitted`
applied directly to the gamma value versus `encode(acesFitted(0.80 *
decode(g)))`):
| gamma in | old display | new display |
|---:|---:|---:|
| 0.05 | 0.031 | 0.023 |
| 0.10 | 0.091 | 0.052 |
| 0.15 | 0.162 | 0.091 |
| 0.20 | 0.233 | 0.140 |
| 0.30 | 0.360 | 0.265 |
| 0.46 | 0.511 | 0.488 |
| 0.50 | 0.541 | 0.539 |
| 0.70 | 0.652 | 0.735 |
| 0.90 | 0.725 | 0.845 |
| 1.00 | 0.752 | 0.879 |
Shadows deepen slightly (0.050.30 gamma read darker), the crossover sits
near gamma 0.5 (old and new agree almost exactly there — expected, since
that is close to the 0.46 midtone the exposure was tuned against), and
highlights above roughly gamma 0.5 now read brighter instead of
compressing toward ACES's ~0.75 ceiling.
**Acceptance:** neutral preset ≡ pack-off within tolerance — proved by the
CPU mirror `AtmosphericColorPipeline` (`AtmosphericColorPipelineTests`,
half-an-8-bit-step identity over a 0..255 grey sweep) plus shader-source
pins (`AtmosphericPostProcessGraphTests`) guarding the decode/encode call
sites against silent removal. The real-frame masked capture **was run**
(retail vs High-with-every-effect-neutral, `artifacts/vm3`): 110,561 px at
`|Δ|=1` (float/pow round-trip noise, sub-visible) and 95 isolated
foliage-edge pixels at `|Δ|≥5`, nothing on any ground/building/water
surface. Stage-1's luminance table re-capture is still **owed** at the
owner gate; owner visual gate: "same look as accepted, no clipping,
highlights roll off". Budget unchanged (two `pow` per pixel).
## VM4 — Truthful documents (F5)