docs(vm3): closeout - owner gate brief with the measured curve table; #422 filed; tests pin the shipped defaults

Opus narrow re-review of 51178f7c: APPROVE. Residuals closed: the AR plan
no longer says '<=1 LSB' unqualified (99.99% of pixels; 95 foliage-
silhouette pixels up to 73 LSB, 58 isolated); the campaign doc says the
same; AtmosphericColorPipelineTests now read the SHIPPED exposure/vignette
defaults from BuiltInAtmosphericRenderPack.Descriptor and the graph's named
DefaultVignetteStrengthFallback instead of literals.

Measured for the gate (offline Holtburg hillside, High defaults vs pack
off): mean luminance -17% noon, -44% dusk, p95 unchanged, clip 0.06% both;
neutral High vs pack off: 110,561 px at |d|=1, 95 at >=5 (foliage edges).
Evidence images under docs/research/evidence/vm3/.

#422 filed: one High-default offline capture exited with
STATUS_HEAP_CORRUPTION after a clean managed shutdown; 1 in 8 runs, never
under validation layers. VM7 gate item.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-22 23:53:26 +02:00
parent 51178f7c77
commit a0157693ec
7 changed files with 111 additions and 8 deletions

View file

@ -24,6 +24,35 @@ What does NOT go here:
- Every session: scan OPEN issues at start; promote/close anything we touched during the session before ending. - Every session: scan OPEN issues at start; promote/close anything we touched during the session before ending.
- Promoting to a Phase: mark as `DONE (promoted to Phase X)` + commit SHA where the Phase entry landed. - Promoting to a Phase: mark as `DONE (promoted to Phase X)` + commit SHA where the Phase entry landed.
## #422 — Intermittent heap-corruption exit (0xC0000374) after a pack-on offline capture
**Status:** OPEN — filed 2026-08-22 at Campaign VM VM3; VM7 gate item (fix or characterise before merge).
**Component:** rendering / render packs (Campaign AR) — native teardown
**Description:** `tools/run-offline-pixel-gate.ps1 -RenderPackPreset high` (shipped
High defaults: shadows, rays, volumetrics, bloom all active) captured its
screenshot normally, the managed shutdown ran to completion (`MossTank
disabled` is the last log line, stderr empty), and the process then exited
with `-1073740940` = `STATUS_HEAP_CORRUPTION`. **1 occurrence in 8 runs** of
the identical command on the same binary (`51178f7c`); the other 7 exited 0,
including 4 runs with `ACDREAM_DEVTOOLS=1` (Vulkan validation layers) that
reported no validation message at all. `retail/off` and `high` with every
effect neutral (no shadow/ray/volumetric work) have never reproduced it.
Campaign VM changed no native lifetime code (VM1 added a ring-section bind;
VM3/VM5 are shader/uniform changes), so this is most likely a Campaign AR
teardown race (a pack image/buffer/pipeline or directional depth target
destroyed while still referenced, or a double free) that the recording-RHI
convergence fixtures cannot see and that a 1-in-8 rate hides from the six
connected lifecycle processes.
**What to do:** reproduce under the Windows debug heap / Application Verifier
(`gflags /p /enable AcDream.App.exe /full`) or with `VK_LAYER_KHRONOS_validation`
plus `VK_VALIDATION_FEATURE_ENABLE_SYNCHRONIZATION_VALIDATION`, looping the
offline High capture until it fires, and capture the crash stack. Fix the root
cause — no try/catch, no "skip teardown" guard. Evidence: `artifacts/vm3/
high-default/` (the crashing run's log; no crash dump was configured).
## #421 — Directional-shadow pass uploads its own transform buffer instead of binding the main pass's instance SSBO ## #421 — Directional-shadow pass uploads its own transform buffer instead of binding the main pass's instance SSBO
**Status:** OPEN — filed 2026-08-22 at Campaign VM VM5 (post-M7 / with GPU culling). **Status:** OPEN — filed 2026-08-22 at Campaign VM VM5 (post-M7 / with GPU culling).

View file

@ -580,7 +580,9 @@ bloom intermediate is already linear (decoded once at extraction, so
downstream blur/composite reads need no further decode). Tonemap/grade/ downstream blur/composite reads need no further decode). Tonemap/grade/
vignette run in linear and the result is re-encoded; the post stack's 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 neutral settings are a numerical identity (measured on a real frame at
Campaign VM VM3: ≤1 LSB). Campaign VM VM3: ≤1 LSB on 99.99 % of pixels; 95 foliage-silhouette pixels
— 58 isolated — differ by up to 73 LSB from cutout-edge rasterization between
two separate client runs, none on any surface).
### Slice 2 — Tier-1 screen-space sun rays ### Slice 2 — Tier-1 screen-space sun rays

View file

@ -79,13 +79,13 @@ single-pass path darkens by ~10 %. Neither explains a scene-wide level.
| VM0 | F1 — default-path invariance | **CLOSED 2026-08-22 — PASS**; [report](../research/2026-08-22-vm0-default-path-invariance.md) | | VM0 | F1 — default-path invariance | **CLOSED 2026-08-22 — PASS**; [report](../research/2026-08-22-vm0-default-path-invariance.md) |
| VM1 | F2/F3 — #226 single-pass re-port + fade removal | **CLOSED 2026-08-22**`05970306`, `388457a7`, `ae651312`, closeout; Opus APPROVE | | VM1 | F2/F3 — #226 single-pass re-port + fade removal | **CLOSED 2026-08-22**`05970306`, `388457a7`, `ae651312`, closeout; Opus APPROVE |
| VM2 | F3 — which retail detail path ran | **CLOSED 2026-08-22** — single-pass; see [cdb note](../research/2026-08-22-vm2-retail-detail-path-cdb.md) | | VM2 | F3 — which retail detail path ran | **CLOSED 2026-08-22** — single-pass; see [cdb note](../research/2026-08-22-vm2-retail-detail-path-cdb.md) |
| VM3 | F4 — linear-light post stack | automated + **owner visual** | | VM3 | F4 — linear-light post stack | **CODE-COMPLETE 2026-08-22**`87677f9c`, `51178f7c`, closeout; Opus APPROVE; **owner visual gate OWED** (brief below) |
| VM4 | F5 — overclaiming docs, incl. the reviewer's own | **CLOSED 2026-08-22** | | VM4 | F5 — overclaiming docs, incl. the reviewer's own | **CLOSED 2026-08-22** |
| VM5 | F8 — volumetric banding; F7 filed | **CLOSED 2026-08-22** (#421) | | VM5 | F8 — volumetric banding; F7 filed | **CLOSED 2026-08-22** (#421) |
| VM6 | Foliage wind | automated + **owner visual** | | VM6 | Foliage wind | automated + **owner visual** |
| VM7 | Closeout: full gates, register, roadmap, merge | **owner** | | VM7 | Closeout: full gates, register, roadmap, merge | **owner** |
Order is VM2 (done) → VM0 (done) → VM1 (done) → VM4 (done) → VM5 → VM3 → VM6 → VM7. VM0 goes first because everything after it is measured Order is VM2 (done) → VM0 (done) → VM1 (done) → VM4 (done) → VM5 (done) → VM3 (code-complete, gate owed) → VM6 → VM7. VM0 goes first because everything after it is measured
against the baseline it establishes. against the baseline it establishes.
--- ---
@ -254,12 +254,57 @@ half-an-8-bit-step identity over a 0..255 grey sweep) plus shader-source
pins (`AtmosphericPostProcessGraphTests`) guarding the decode/encode call pins (`AtmosphericPostProcessGraphTests`) guarding the decode/encode call
sites against silent removal. The real-frame masked capture **was run** sites against silent removal. The real-frame masked capture **was run**
(retail vs High-with-every-effect-neutral, `artifacts/vm3`): 110,561 px at (retail vs High-with-every-effect-neutral, `artifacts/vm3`): 110,561 px at
`|Δ|=1` (float/pow round-trip noise, sub-visible) and 95 isolated `|Δ|=1` (float/pow round-trip noise, sub-visible) and 95 foliage-silhouette
foliage-edge pixels at `|Δ|≥5`, nothing on any ground/building/water pixels at `|Δ|≥5` (58 isolated, max 73 — cutout-edge rasterization between two
surface. Stage-1's luminance table re-capture is still **owed** at the separate client runs; ambient motion is the alternative explanation), 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, owner gate; owner visual gate: "same look as accepted, no clipping,
highlights roll off". Budget unchanged (two `pow` per pixel). highlights roll off". Budget unchanged (two `pow` per pixel).
### VM3 owner visual gate — brief (read this, then play)
Launch normally (launcher or `ACDREAM_RETAIL_UI=1` env launch), Options →
Display → render pack **Atmospheric / High**. Every number below is measured,
not predicted; the sliders are live so you can tune while looking.
**What changed on screen, at the shipped defaults (exposure 0.80):**
| input tone (gamma) | before VM3 | after VM3 | Δ |
|---|---|---|---|
| deep shadow 0.10 | 0.091 | 0.052 | 43 % |
| shadow 0.20 | 0.233 | 0.140 | 40 % |
| mid-grey 0.46 | 0.511 | 0.488 | 5 % |
| 0.50 | 0.541 | 0.539 | 0 (crossover) |
| bright 0.70 | 0.652 | 0.735 | +13 % |
| highlight 0.90 | 0.725 | 0.845 | +16 % |
Midtones are where you left them; **shadows are deeper and highlights no
longer crushed** — that is the linear ACES toe/shoulder, not a bug. Offline
Holtburg hillside captures (`docs/research/evidence/vm3/`): mean luminance vs
pack-off **17 % at noon, 44 % at dusk** (codex's accepted look measured
8 % outdoors on a different scene). Vignette was re-derived (0.12 → 0.245) so
the corner darkening on screen is the same 12 % you accepted.
**Checklist (≈10 min):**
1. Noon outdoors, walk a shaded street: do shadows read as "deep" or as
"crushed"? If crushed → raise **Exposure** to 0.91.0 (mid-greys will
brighten ~10 %) or lower **Filmic strength** to 0.70.8 (lifts the toe,
keeps the shoulder).
2. Look at the sky near the sun and a bright roof: highlights should roll off
smoothly with no clipping (clip % measured 0.06, same as pack off).
3. Dusk (`/time` or wait): the 44 % is the effect of long shadows plus the
toe. Judge whether it is atmospheric or merely dark; the same two sliders
apply.
4. Corners: vignette should match the previous feel. If you had a saved
override of 0.12 it is now weaker (the default moved, overrides did not).
5. Switch the pack to **retail/off** and back: with every effect at its
neutral value the image is the pack-off image (measured: ≤1 LSB on 99.99 %
of pixels).
Say which slider values you settle on; they become the shipped defaults in
VM7. Known: #422 (1-in-8 heap-corruption exit after a pack-on run) is open
and is a VM7 gate item, not a VM3 one.
## VM4 — Truthful documents (F5) ## VM4 — Truthful documents (F5)
**OUTCOME (2026-08-22): CLOSED.** Corrected in place, each with a dated **OUTCOME (2026-08-22): CLOSED.** Corrected in place, each with a dated

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 KiB

View file

@ -40,7 +40,7 @@ internal readonly record struct AtmosphericPostProcessSettings(
Read(descriptor, preset, userSettingOverrides, RenderSettingSemantic.Exposure, 1f), Read(descriptor, preset, userSettingOverrides, RenderSettingSemantic.Exposure, 1f),
Read(descriptor, preset, userSettingOverrides, RenderSettingSemantic.GradeSaturation, 1f), Read(descriptor, preset, userSettingOverrides, RenderSettingSemantic.GradeSaturation, 1f),
Read(descriptor, preset, userSettingOverrides, RenderSettingSemantic.GradeContrast, 1f), Read(descriptor, preset, userSettingOverrides, RenderSettingSemantic.GradeContrast, 1f),
Read(descriptor, preset, userSettingOverrides, RenderSettingSemantic.VignetteStrength, 0.245f), Read(descriptor, preset, userSettingOverrides, RenderSettingSemantic.VignetteStrength, AtmosphericPostProcessGraph.DefaultVignetteStrengthFallback),
Read(descriptor, preset, userSettingOverrides, RenderSettingSemantic.SunRayStrength, 0.55f)); Read(descriptor, preset, userSettingOverrides, RenderSettingSemantic.SunRayStrength, 0.55f));
} }
@ -139,6 +139,9 @@ internal sealed class AtmosphericPostProcessGraph :
// knee is threshold - lowerBound = 1.0 - 0.27 ~= 0.73. Same set of // knee is threshold - lowerBound = 1.0 - 0.27 ~= 0.73. Same set of
// pixels blooms; the math now runs in the space ACES/luma assume. // pixels blooms; the math now runs in the space ACES/luma assume.
internal const float BloomThresholdLinear = 1f; internal const float BloomThresholdLinear = 1f;
/// <summary>Fallback when a descriptor declares no vignette setting; must equal the shipped declaration (pinned by AtmosphericColorPipelineTests).</summary>
internal const float DefaultVignetteStrengthFallback = 0.245f;
internal const float BloomKneeLinear = 0.73f; internal const float BloomKneeLinear = 0.73f;
internal AtmosphericPostProcessGraph( internal AtmosphericPostProcessGraph(

View file

@ -151,10 +151,34 @@ public sealed class AtmosphericColorPipelineTests
// strength of 0.12 would now produce. See the "vignette-strength" // strength of 0.12 would now produce. See the "vignette-strength"
// comment in BuiltInAtmosphericRenderPack.cs for the full // comment in BuiltInAtmosphericRenderPack.cs for the full
// derivation. // derivation.
const float VignetteStrengthDefault = 0.245f; // Read the SHIPPED declaration so the test pins the product, not a copy.
float VignetteStrengthDefault = ShippedDefault("vignette-strength");
Assert.Equal(0.245f, VignetteStrengthDefault, 3);
float cornerMultiplier = 1f - VignetteStrengthDefault; float cornerMultiplier = 1f - VignetteStrengthDefault;
Vector3 displayed = AtmosphericColorPipeline.Encode( Vector3 displayed = AtmosphericColorPipeline.Encode(
new Vector3(cornerMultiplier, cornerMultiplier, cornerMultiplier)); new Vector3(cornerMultiplier, cornerMultiplier, cornerMultiplier));
Assert.InRange(displayed.X, 0.88f - 0.005f, 0.88f + 0.005f); Assert.InRange(displayed.X, 0.88f - 0.005f, 0.88f + 0.005f);
} }
private static float ShippedDefault(string settingId)
{
AcDream.Plugin.Abstractions.Rendering.RenderSettingDeclaration declaration =
Assert.Single(
AcDream.App.Rendering.Packs.BuiltInAtmosphericRenderPack.Descriptor.Settings,
s => s.Id == settingId);
return float.Parse(
declaration.DefaultValue,
System.Globalization.CultureInfo.InvariantCulture);
}
[Fact]
public void ShippedExposureDefaultIsTheDerivedMidtoneValue()
{
// 0.80 is the value whose linear-light result reproduces the accepted
// mid-grey (see BuiltInAtmosphericRenderPack "exposure" derivation).
Assert.Equal(0.80f, ShippedDefault("exposure"), 3);
Assert.Equal(
0.245f,
AcDream.App.Rendering.Packs.AtmosphericPostProcessGraph.DefaultVignetteStrengthFallback, 3);
}
} }