diff --git a/docs/ISSUES.md b/docs/ISSUES.md index c4376a55..cc50640e 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -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. - 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 **Status:** OPEN — filed 2026-08-22 at Campaign VM VM5 (post-M7 / with GPU culling). diff --git a/docs/plans/2026-08-21-atmospheric-rendering.md b/docs/plans/2026-08-21-atmospheric-rendering.md index d540c0d9..47e661c7 100644 --- a/docs/plans/2026-08-21-atmospheric-rendering.md +++ b/docs/plans/2026-08-21-atmospheric-rendering.md @@ -580,7 +580,9 @@ 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). +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 diff --git a/docs/plans/2026-08-22-visualmaster-campaign.md b/docs/plans/2026-08-22-visualmaster-campaign.md index 349c72c0..630914c9 100644 --- a/docs/plans/2026-08-22-visualmaster-campaign.md +++ b/docs/plans/2026-08-22-visualmaster-campaign.md @@ -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) | | 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) | -| 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** | | VM5 | F8 — volumetric banding; F7 filed | **CLOSED 2026-08-22** (#421) | | VM6 | Foliage wind | automated + **owner visual** | | 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. --- @@ -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 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 +`|Δ|=1` (float/pow round-trip noise, sub-visible) and 95 foliage-silhouette +pixels at `|Δ|≥5` (58 isolated, max 73 — cutout-edge rasterization between two +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, 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.9–1.0 (mid-greys will + brighten ~10 %) or lower **Filmic strength** to 0.7–0.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) **OUTCOME (2026-08-22): CLOSED.** Corrected in place, each with a dated diff --git a/docs/research/evidence/vm3/gate-sheet-retail_high_noon_top_dusk_bottom.png b/docs/research/evidence/vm3/gate-sheet-retail_high_noon_top_dusk_bottom.png new file mode 100644 index 00000000..4627158c Binary files /dev/null and b/docs/research/evidence/vm3/gate-sheet-retail_high_noon_top_dusk_bottom.png differ diff --git a/docs/research/evidence/vm3/neutral-identity-diff-x32.png b/docs/research/evidence/vm3/neutral-identity-diff-x32.png new file mode 100644 index 00000000..8b89c03c Binary files /dev/null and b/docs/research/evidence/vm3/neutral-identity-diff-x32.png differ diff --git a/src/AcDream.App/Rendering/Packs/AtmosphericPostProcessGraph.cs b/src/AcDream.App/Rendering/Packs/AtmosphericPostProcessGraph.cs index 3f1047e3..9c86b6af 100644 --- a/src/AcDream.App/Rendering/Packs/AtmosphericPostProcessGraph.cs +++ b/src/AcDream.App/Rendering/Packs/AtmosphericPostProcessGraph.cs @@ -40,7 +40,7 @@ internal readonly record struct AtmosphericPostProcessSettings( Read(descriptor, preset, userSettingOverrides, RenderSettingSemantic.Exposure, 1f), Read(descriptor, preset, userSettingOverrides, RenderSettingSemantic.GradeSaturation, 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)); } @@ -139,6 +139,9 @@ internal sealed class AtmosphericPostProcessGraph : // 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. internal const float BloomThresholdLinear = 1f; + /// Fallback when a descriptor declares no vignette setting; must equal the shipped declaration (pinned by AtmosphericColorPipelineTests). + internal const float DefaultVignetteStrengthFallback = 0.245f; + internal const float BloomKneeLinear = 0.73f; internal AtmosphericPostProcessGraph( diff --git a/tests/AcDream.App.Tests/Rendering/Packs/AtmosphericColorPipelineTests.cs b/tests/AcDream.App.Tests/Rendering/Packs/AtmosphericColorPipelineTests.cs index 64d6ecf8..05ad82dc 100644 --- a/tests/AcDream.App.Tests/Rendering/Packs/AtmosphericColorPipelineTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Packs/AtmosphericColorPipelineTests.cs @@ -151,10 +151,34 @@ public sealed class AtmosphericColorPipelineTests // strength of 0.12 would now produce. See the "vignette-strength" // comment in BuiltInAtmosphericRenderPack.cs for the full // 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; Vector3 displayed = AtmosphericColorPipeline.Encode( new Vector3(cornerMultiplier, cornerMultiplier, cornerMultiplier)); 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); + } } +