From 754d59d949b00a003c200116154e349b2d653ed9 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 23 Aug 2026 03:58:41 +0200 Subject: [PATCH] fix(render): shadow-gated-off receiver frames light from the authored sun again (Campaign VM VM6 review 5) Round 4 (eec95535) fixed wind but introduced a new lighting bug: the receiver VERTEX shaders source the sun direction from the shadow block, not only the shadow visibility term. mesh_atmospheric.vert's accumulateLights read uShadowLightDirectionAndSource unconditionally for every directional light; terrain_atmospheric.vert did the same for its single sun term. Round 4's PublishDisabledReceiverBinding writes direction (0,0,1) into that block on every shadow-gated-off frame (user sun-shadow-strength 0 in daylight, indoor/portal cover, night), so every such frame was lighting outdoor terrain and objects from straight overhead instead of the authored sun. Publishing the environment's real direction would not have restored parity either -- the celestial shadow source direction (sun/moon disc) is not the authored light direction. F1 (BLOCKER): fixed in the shaders themselves, exact parity with the plain pipeline. Both receiver verts now branch on the same flag bit acdreamDirectionalShadowVisibility already reads ((uShadowTextureAndFlags.w & 1u) == 0u) and, when clear, use the EXACT plain-pipeline expression instead of the shadow block's direction: -uLights[i].dirAndRange.xyz in mesh_atmospheric.vert (matching mesh_modern.vert, hoisted out of the light loop as a uniform branch); -uLights[0].dirAndRange.xyz in terrain_atmospheric.vert (matching terrain_modern.vert's sunDir/-sunDir form). The (0,0,1) word in the disabled block stays as the documented normalize()-cannot-NaN guard; its comment now says so explicitly since it is no longer read as a light direction when the flag is clear. F2: RenderPrepared's cascadeCount == 0 return is a third bufferless- disabled path reachable from a frame that already passed Render's own two gates (the cascade fitter can still find zero usable cascades) -- publishes the same disabled binding now, via the same PublishDisabledReceiverBinding helper (re-signatured to take a bare AtmosphericFrameBufferBinding so all three call sites -- Render's two early-outs plus this one -- share it). F3: removed a stray duplicated " -- Closeout and merge" fragment under the plan's VM7 heading. F4: corrected the false "the flag bit makes it numerically the plain lighting sum" claim in the plan's round-4 paragraph and in WbDrawDispatcher.DirectionalShadowReceivers.cs -- the flag bit alone only fixed the shadow VISIBILITY term (already correct before round 4); it took both that AND round 5's light-DIRECTION fallback to actually match the plain pipeline. T1: extracted Render's gate prologue (environment evaluate -> two early-outs -> PublishDisabledReceiverBinding) into internal EvaluateGateAndPublishDisabledBinding(frame, in input, out environment, out environmentGateTicks), behaviour-preserving, called by Render before it touches world/terrain -- the ArgumentNullException.ThrowIfNull(world)/ ThrowIfNull(terrain) calls keep their exact position relative to the gate. No test in this suite constructs a real WbDrawDispatcher + TerrainModernRenderer pair (still true), so this extraction is what makes the gate itself testable; two new tests drive it directly with PlayerInsideCell: true and with ResidentMaximumReachMeters <= CameraNearMeters, asserting TryGetCurrentFrameBinding true / IsValidFor false for both. T2: proves the actual composition WbDrawDispatcher.PipelinesFor and TerrainModernRenderer both use -- TryGetCurrentFrameBinding feeding ShouldSelectReceiverPipeline -- selects the receiver pipeline for the atmospheric world pass once a disabled binding is published, and still refuses a non-atmospheric pass name. T3: shader-source guard (same style as AtmosphericPostProcessGraphTests' existing shader-text tests) pinning that both receiver verts contain the flag-gated fallback and reference the same uLights expression the plain verts use, so a future edit that drops the fallback fails this test instead of only showing up in a pixel capture. T4: the (0,0,1) test's doc comment and an inline assertion comment now say the value is a NaN guard, not a light direction. Regenerated SPIR-V: mesh_atmospheric.vert and terrain_atmospheric.vert recompiled to different bytes this time (a real code change, not a comment); manifest updated to match. Verify: Release build 0 warnings/0 errors. App hermetic-lane filter 6,054/0 failed. Core.Tests 4,695/0 failed. RenderPackValidator 30/30. Full hermetic-filtered solution: 15,282/0 failed across 15 projects. Co-Authored-By: Claude Sonnet 5 --- .../plans/2026-08-22-visualmaster-campaign.md | 56 +++++- .../Rendering/DirectionalSunShadowRenderer.cs | 86 +++++++-- .../Rendering/Shaders/mesh_atmospheric.vert | 17 +- .../Shaders/spv/mesh_atmospheric.vert.spv | Bin 13676 -> 13960 bytes .../Shaders/spv/shaders.manifest.json | 4 +- .../Shaders/spv/terrain_atmospheric.vert.spv | Bin 10168 -> 10452 bytes .../Shaders/terrain_atmospheric.vert | 12 +- ...awDispatcher.DirectionalShadowReceivers.cs | 17 +- .../Rendering/DirectionalShadowGpuTests.cs | 170 ++++++++++++++++-- .../Packs/AtmosphericPostProcessGraphTests.cs | 49 +++++ 10 files changed, 360 insertions(+), 51 deletions(-) diff --git a/docs/plans/2026-08-22-visualmaster-campaign.md b/docs/plans/2026-08-22-visualmaster-campaign.md index 8127b462..b3f6b676 100644 --- a/docs/plans/2026-08-22-visualmaster-campaign.md +++ b/docs/plans/2026-08-22-visualmaster-campaign.md @@ -82,10 +82,10 @@ single-pass path darkens by ~10 %. Neither explains a scene-wide level. | 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** | +| VM6 | Foliage wind | **CODE-COMPLETE 2026-08-23** — `0930c35d`, `39e8408c`, `43e3abed`, `a82959f1`, `fccba839`, `eec95535`; four Opus review rounds (round 3 APPROVE, round 4 narrow re-review of `fccba839`+`eec95535`); pixel proof [note](../research/2026-08-23-vm6-foliage-wind-pixel-proof.md) (13,854 robust wind px vs 19 floor, scenery only); **owner visual gate OWED** (§VM6 Acceptance) | | VM7 | Closeout: full gates, register, roadmap, merge | **owner** | -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 +Order is VM2 (done) → VM0 (done) → VM1 (done) → VM4 (done) → VM5 (done) → VM3 (code-complete, gate owed) → VM6 (code-complete, gate owed) → VM7. VM0 goes first because everything after it is measured against the baseline it establishes. --- @@ -495,7 +495,7 @@ direction to read, and the register row says so. indoors — nothing moves; (6) pack off — nothing moves. The owner's words decide; the numbers above are starting points to tune live. -### VM6 outcome (implementation landed 2026-08-22; owner visual gate outstanding) +### VM6 outcome (implementation landed 2026-08-22; code-complete 2026-08-23 at `eec95535`; owner visual gate outstanding) Two commits (VM6a shader ABI v2 plumbing, VM6b the feature) shipped the design above; a same-day fix-round commit corrected the weather-table key. @@ -694,13 +694,53 @@ direction `(0,0,1)` so a fragment shader's `normalize()` can never produce NaN. `BindDirectionalShadowReceiver` and `TerrainModernRenderer`'s shadow-buffer bind now check `Buffer is not null` instead of `Enabled`, so this disabled block actually gets bound -once it is selected. Consequence in one sentence: the built-in pack's -world pass now always runs the receiver shader; with shadows gated off, -the shadow block's own flag bit makes its shadow term numerically the -plain lighting sum, while wind keeps moving correctly. +once it is selected. + +**Review fix round 5 (2026-08-23) correction to the paragraph above:** the +claim "the flag bit makes it numerically the plain lighting sum" was +false as written — F1 BLOCKER, found by the reviewer's own offline pixel +apparatus reading the shader source, not by a repro capture (the disabled +block's `(0,0,1)` direction differs too little from straight-overhead to +show up above the apparatus's noise floor at these amplitudes, so the +mislighting was invisible to that specific test even though it is real). +Both receiver VERTEX shaders (`mesh_atmospheric.vert`, +`terrain_atmospheric.vert`) sourced the sun direction used to compute +`directionalLit`/`vDirectionalLit` from the shadow block's own +`uShadowLightDirectionAndSource`, unconditionally — not only the +*visibility* term, which was already correctly flag-gated. Every shadow- +gated-off frame was lighting outdoor terrain and objects from the +disabled block's `(0,0,1)` placeholder — straight overhead — regardless +of the authored sun's actual position. The celestial shadow source +direction is not the authored light direction either, so publishing it +instead would not have restored parity. Fixed in the shaders themselves: +both receiver verts now branch on the SAME flag bit +(`(uShadowTextureAndFlags.w & 1u) == 0u`) and, when clear, use the EXACT +plain-pipeline expression (`-uLights[i].dirAndRange.xyz` in +`mesh_atmospheric.vert`, matching `mesh_modern.vert`; +`-uLights[0].dirAndRange.xyz` in `terrain_atmospheric.vert`, matching +`terrain_modern.vert`) instead of the shadow block's direction. Corrected +statement: with the flag bit clear the receiver shaders take visibility +1.0 AND fall back to the authored `uLights` direction (round 5), so a +gated-off frame is numerically the plain pipeline. Wind is unaffected — +`foliage_wind.glsl` reads only the AtmosphericFrame half of set 3, never +the shadow block. + +**Pixel proof after round 4 (2026-08-23, `eec95535`):** the same +apparatus that found the defect — High preset, `sun-shadow-strength=0` so +no shadow term can move, same pinned clock, wind on vs off, two captures per +arm so the repeat pairs measure the run-to-run floor — gives a robust wind +mask of **13,854 px** (amplified: strength 2, lean/branch 1 m, flutter +0.5 m) against a **19 px** floor, and **1,402 px** at the default Clear +strength; every marked pixel is a treeline tree, hillside tree or shoreline +bush, and no house, fence, road, lifestone, ground, water or UI pixel moves. +The first method tried (clock pin 0 s vs 3 s, wind-off pair as the control) +is recorded as confounded in the note and must not be reused: the treeline +silhouette carries a bimodal 0-or-~280 px rasterisation churn between runs +that lands on either side of the subtraction by luck. Tool: +`tools/vm6/wind-pixel-proof.py`; note: +`docs/research/2026-08-23-vm6-foliage-wind-pixel-proof.md`. ## VM7 — Closeout and merge - — Closeout and merge - Full gates: `tools/run-release-gate.ps1` (hermetic lanes), the AR reference matrix re-run for the changed presets, VM0's masked comparison diff --git a/src/AcDream.App/Rendering/DirectionalSunShadowRenderer.cs b/src/AcDream.App/Rendering/DirectionalSunShadowRenderer.cs index 152b7657..f2691404 100644 --- a/src/AcDream.App/Rendering/DirectionalSunShadowRenderer.cs +++ b/src/AcDream.App/Rendering/DirectionalSunShadowRenderer.cs @@ -316,28 +316,37 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS return !_disposed && binding.IsBindableFor(frame); } - internal DirectionalSunShadowDiagnostics Render( + /// + /// Campaign VM VM6 review fix round 5 (T1): the gate prologue Render + /// runs before it touches world/terrain, extracted into its own + /// internal method so a hermetic test can drive it directly — + /// constructing a real WbDrawDispatcher/TerrainModernRenderer pair + /// just to reach these two early-out paths is not a cheap test (see + /// PublishDisabledReceiverBinding's own doc comment). Returns the + /// disabled diagnostics Render should return immediately, or null when + /// the gate passed and Render should continue (in which case + /// and + /// are the validated values + /// Render's remaining stages use). Behaviour-preserving: identical + /// order, identical PublishDisabledReceiverBinding calls, identical + /// Disabled(...) construction as before the extraction. + /// + internal DirectionalSunShadowDiagnostics? EvaluateGateAndPublishDisabledBinding( IGpuFrame frame, in DirectionalSunShadowRenderInput input, - WbDrawDispatcher world, - TerrainModernRenderer terrain) + out DirectionalShadowEnvironmentState environment, + out long environmentGateTicks) { - ObjectDisposedException.ThrowIf(_disposed, this); - ArgumentNullException.ThrowIfNull(frame); - _currentFrameBinding = DirectionalShadowFrameBinding.Disabled; - ArgumentNullException.ThrowIfNull(world); - ArgumentNullException.ThrowIfNull(terrain); long cpuStageStarted = input.MeasureCpuStages ? Stopwatch.GetTimestamp() : 0L; - DirectionalShadowEnvironmentState environment = - DirectionalShadowEnvironmentGate.Evaluate( - input.Environment, - _atmospherePolicy); - long environmentGateTicks = input.MeasureCpuStages + environment = DirectionalShadowEnvironmentGate.Evaluate( + input.Environment, + _atmospherePolicy); + environmentGateTicks = input.MeasureCpuStages ? Stopwatch.GetTimestamp() - cpuStageStarted : 0L; if (!environment.ShouldRender) { - PublishDisabledReceiverBinding(frame, in input); + PublishDisabledReceiverBinding(frame, input.AtmosphericFrame); return Disabled( in environment, new DirectionalSunShadowCpuStageTicks( @@ -349,14 +358,35 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS { Reason = DirectionalShadowGateReason.ResidentWindowUnavailable, }; - PublishDisabledReceiverBinding(frame, in input); + PublishDisabledReceiverBinding(frame, input.AtmosphericFrame); return Disabled( in environment, new DirectionalSunShadowCpuStageTicks( environmentGateTicks, 0L, 0L, 0L, 0L)); } + return null; + } - cpuStageStarted = input.MeasureCpuStages ? Stopwatch.GetTimestamp() : 0L; + internal DirectionalSunShadowDiagnostics Render( + IGpuFrame frame, + in DirectionalSunShadowRenderInput input, + WbDrawDispatcher world, + TerrainModernRenderer terrain) + { + ObjectDisposedException.ThrowIf(_disposed, this); + ArgumentNullException.ThrowIfNull(frame); + _currentFrameBinding = DirectionalShadowFrameBinding.Disabled; + ArgumentNullException.ThrowIfNull(world); + ArgumentNullException.ThrowIfNull(terrain); + DirectionalSunShadowDiagnostics? gated = EvaluateGateAndPublishDisabledBinding( + frame, + in input, + out DirectionalShadowEnvironmentState environment, + out long environmentGateTicks); + if (gated is not null) + return gated.Value; + + long cpuStageStarted = input.MeasureCpuStages ? Stopwatch.GetTimestamp() : 0L; DirectionalShadowPreparedDraws worldDraws = world.PrepareDirectionalShadowDraws(input.Casters); DirectionalShadowTerrainPreparedDraws terrainDraws = @@ -495,9 +525,9 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS /// internal void PublishDisabledReceiverBinding( IGpuFrame frame, - in DirectionalSunShadowRenderInput input) + AtmosphericFrameBufferBinding atmosphericFrame) { - if (!input.AtmosphericFrame.IsBound) + if (!atmosphericFrame.IsBound) return; var disabledUniforms = new DirectionalShadowUniforms( @@ -509,6 +539,14 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS Vector4.Zero, Vector4.Zero, new UInt4(0u, 0u, 0u, 0u), + // Campaign VM VM6 review fix round 5 (F1): (0,0,1) is a + // normalize()-cannot-NaN guard ONLY — round 5 fixed + // mesh_atmospheric.vert/terrain_atmospheric.vert to fall back + // to the plain pipeline's uLights-derived direction whenever + // this block's flags bit 0 is clear, so this vector is never + // actually read as a light direction any more. It stays a + // unit vector regardless, in case a future reader forgets that + // rule and reads it directly. new Vector4(0f, 0f, 1f, 0f)); GpuRingAllocation allocation = frame.AllocateRing( DirectionalShadowUniforms.SizeInBytes, @@ -523,7 +561,7 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS DirectionalShadowUniforms.SizeInBytes, GpuTextureSlot.Unassigned, CascadeCount: 0, - AtmosphericFrame: input.AtmosphericFrame); + AtmosphericFrame: atmosphericFrame); } internal static DirectionalShadowCasterClassDiagnostics @@ -632,6 +670,16 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS { Reason = DirectionalShadowGateReason.ResidentWindowUnavailable, }; + // Campaign VM VM6 review fix round 5 (F2): a third bufferless- + // disabled exit reachable from a frame that already passed + // Render's own two gates (ShouldRender, the constructor-level + // ResidentMaximumReachMeters check) — the cascade fitter can + // still find zero usable cascades. Publish the same disabled + // receiver binding as Render's two early-outs so this path + // doesn't reintroduce the F1/round-4 bug for the rarer case + // where a frame draws the world but the shadow fitter itself + // bails out. + PublishDisabledReceiverBinding(frame, atmosphericFrame); return Disabled(in unavailable, cpuStages); } diff --git a/src/AcDream.App/Rendering/Shaders/mesh_atmospheric.vert b/src/AcDream.App/Rendering/Shaders/mesh_atmospheric.vert index 7dd43406..967b40ae 100644 --- a/src/AcDream.App/Rendering/Shaders/mesh_atmospheric.vert +++ b/src/AcDream.App/Rendering/Shaders/mesh_atmospheric.vert @@ -269,11 +269,26 @@ vec3 accumulateAmbientLocalLights( // get no sun even in windowed buildings where the player's frame is not sun-killed. if (uLightingMode == 0) { if (instanceIndoor[instanceIndex] == 0u) { // #142: outdoor objects only get the sun + // Campaign VM VM6 review fix round 5 (F1 BLOCKER): when the + // shadow block's flag bit is clear (a gated-off frame — see + // DirectionalSunShadowRenderer.PublishDisabledReceiverBinding), + // uShadowLightDirectionAndSource carries only a (0,0,1) NaN + // guard, NOT a real light direction — using it here regardless + // lit every gated-off frame (every night, sun-shadow-strength + // 0, indoors, portal cover) from straight overhead. Fall back + // to the EXACT plain mesh_modern.vert expression + // (-uLights[i].dirAndRange.xyz, unnormalized — matched + // bit-for-bit) so a gated-off frame is numerically the plain + // pipeline. Hoisted out of the loop: a uniform branch, + // evaluated once per vertex regardless of light count. + bool shadowGatedOff = (uShadowTextureAndFlags.w & 1u) == 0u; int activeLights = int(uCellAmbient.w); for (int i = 0; i < 8; ++i) { if (i >= activeLights) break; if (int(uLights[i].posAndKind.w) != 0) continue; // directional only - vec3 Ldir = normalize(uShadowLightDirectionAndSource.xyz); + vec3 Ldir = shadowGatedOff + ? -uLights[i].dirAndRange.xyz + : normalize(uShadowLightDirectionAndSource.xyz); float ndl = max(0.0, dot(N, Ldir)); directionalLit += uLights[i].colorAndIntensity.xyz * uLights[i].colorAndIntensity.w * ndl; diff --git a/src/AcDream.App/Rendering/Shaders/spv/mesh_atmospheric.vert.spv b/src/AcDream.App/Rendering/Shaders/spv/mesh_atmospheric.vert.spv index 3cc72ed5ea7ab11e4a13a2e3592a89b05b56a5fe..8ffa95f09bce2a867363a167e8ceef5635a6817f 100644 GIT binary patch literal 13960 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 E0ao)e>i_@% literal 13676 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 KpPAk(^?w1eS^<^- diff --git a/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json b/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json index d6d0d9ca..c8cd1094 100644 --- a/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json +++ b/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json @@ -215,7 +215,7 @@ "stages": [ { "stage": "vert", - "sourceSha256": "a00aa83a8bd8ab4791b60bfb967a80b43bafcb0bf758f5e2e056b4272f3c8b1b", + "sourceSha256": "a6b7aa2b0c1c38f92e4b35e76a58acdf5adc916f6ed0cebf94c8bea3f8843666", "compiled": true }, { @@ -327,7 +327,7 @@ "stages": [ { "stage": "vert", - "sourceSha256": "06258c7ead0e123740e325c802156987ed17dabe355d591d0e90facb420b8e04", + "sourceSha256": "8cc199b3c6582831e5c9614e19afdff4f0f5fcdb8d96271c8cb3f427e29c68c9", "compiled": true }, { diff --git a/src/AcDream.App/Rendering/Shaders/spv/terrain_atmospheric.vert.spv b/src/AcDream.App/Rendering/Shaders/spv/terrain_atmospheric.vert.spv index d672dcb9b90f7eb3f1af4f5103148421d3eb34ea..171b44609bddac2fd113ec1a524c91f9cbff976f 100644 GIT binary patch literal 10452 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 literal 10168 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 Ruj;M( [Fact] public void PublishDisabledReceiverBinding_IsBindableButNotValidWithZeroFlagsAndUnitDirection() @@ -563,16 +570,10 @@ public sealed class DirectionalShadowGpuTests atmosphericBuffer, OffsetBytes: 0u, SizeBytes: 192u); - var input = new DirectionalSunShadowRenderInput( - default, - Matrix4x4.Identity, - Matrix4x4.Identity, - new DirectionalShadowCasterFrame(), - AtmosphericFrame: atmosphericFrame); device.Clear(); using IGpuFrame frame = device.BeginFrame(); - renderer.PublishDisabledReceiverBinding(frame, in input); + renderer.PublishDisabledReceiverBinding(frame, atmosphericFrame); Assert.True(renderer.TryGetCurrentFrameBinding(frame, out DirectionalShadowFrameBinding binding)); Assert.True(binding.IsBindableFor(frame)); @@ -588,6 +589,8 @@ public sealed class DirectionalShadowGpuTests (int)binding.OffsetBytes, DirectionalShadowUniforms.SizeInBytes)); Assert.Equal(0u, written.TextureAndFlags.W); + // (0,0,1) is the NaN guard, not a light direction — see the doc + // comment above and PublishDisabledReceiverBinding's own comment. Assert.Equal(new Vector4(0f, 0f, 1f, 0f), written.LightDirectionAndSource); } @@ -602,20 +605,157 @@ public sealed class DirectionalShadowGpuTests // binding 5) is unaffected. using var device = new RecordingGpuDevice(); using var renderer = new DirectionalSunShadowRenderer(device, DirectionalShadowPreset.Low); - var input = new DirectionalSunShadowRenderInput( - default, - Matrix4x4.Identity, - Matrix4x4.Identity, - new DirectionalShadowCasterFrame()); device.Clear(); using IGpuFrame frame = device.BeginFrame(); - renderer.PublishDisabledReceiverBinding(frame, in input); + renderer.PublishDisabledReceiverBinding(frame, default); Assert.False(renderer.TryGetCurrentFrameBinding(frame, out _)); Assert.Empty(device.OfKind()); } + /// + /// Campaign VM VM6 review fix round 5, test T1 (indoor early-out): + /// no test calls DirectionalSunShadowRenderer.Render because it needs + /// a real WbDrawDispatcher + TerrainModernRenderer to construct (still + /// true — nothing in this suite stands either up), so the gate + /// prologue Render runs before touching either is extracted into + /// EvaluateGateAndPublishDisabledBinding (behaviour-preserving; see + /// its own doc comment) and tested directly here. PlayerInsideCell is + /// the simplest trigger for Render's FIRST early-out + /// (!environment.ShouldRender). + /// + [Fact] + public void EvaluateGateAndPublishDisabledBinding_PlayerInsideCellPublishesBindableNotValidBinding() + { + using var device = new RecordingGpuDevice(); + using var renderer = new DirectionalSunShadowRenderer(device, DirectionalShadowPreset.Low); + using IGpuBuffer atmosphericBuffer = Buffer(device, "gate-indoor-frame", GpuBufferUsage.Uniform); + var atmosphericFrame = new AtmosphericFrameBufferBinding(atmosphericBuffer, 0u, 192u); + var input = new DirectionalSunShadowRenderInput( + new DirectionalShadowEnvironmentInput( + PackEnabled: true, + PortalOrLoginCoverVisible: false, + PlayerInsideCell: true, + Source: AuthoredCelestialShadowSource.None(), + Atmosphere: default), + Matrix4x4.Identity, + Matrix4x4.Identity, + new DirectionalShadowCasterFrame(), + AtmosphericFrame: atmosphericFrame); + device.Clear(); + using IGpuFrame frame = device.BeginFrame(); + + DirectionalSunShadowDiagnostics? gated = renderer.EvaluateGateAndPublishDisabledBinding( + frame, + in input, + out DirectionalShadowEnvironmentState environment, + out long environmentGateTicks); + + Assert.NotNull(gated); + Assert.Equal(DirectionalShadowGateReason.Indoor, gated.Value.GateReason); + Assert.Equal(DirectionalShadowGateReason.Indoor, environment.Reason); + Assert.True(renderer.TryGetCurrentFrameBinding(frame, out DirectionalShadowFrameBinding binding)); + Assert.True(binding.IsBindableFor(frame)); + Assert.False(binding.IsValidFor(frame)); + } + + /// + /// Campaign VM VM6 review fix round 5, test T1 (resident-window + /// early-out): Render's SECOND early-out. Reaching it needs a fully + /// valid environment (ShouldRender true), so the environment input + /// below constructs a real celestial source above the horizon with + /// energy, and a fully-progressed atmosphere snapshot — anything less + /// would gate off at the FIRST check instead of reaching + /// ResidentMaximumReachMeters. + /// + [Fact] + public void EvaluateGateAndPublishDisabledBinding_ResidentWindowUnavailablePublishesBindableNotValidBinding() + { + using var device = new RecordingGpuDevice(); + using var renderer = new DirectionalSunShadowRenderer(device, DirectionalShadowPreset.Low); + using IGpuBuffer atmosphericBuffer = Buffer(device, "gate-resident-frame", GpuBufferUsage.Uniform); + var atmosphericFrame = new AtmosphericFrameBufferBinding(atmosphericBuffer, 0u, 192u); + var validSource = new AuthoredCelestialShadowSource( + AuthoredCelestialShadowSourceKind.Sun, + ObjectIndex: 0, + GfxObjId: 1u, + SurfaceToLightDirection: Vector3.UnitZ, + ElevationSin: 0.5f, + AuthoredEnergy: 1f); + var validAtmosphere = new AtmosphereSnapshot( + WeatherKind.Clear, + Intensity: 1f, + FogColor: Vector3.Zero, + FogStart: 0f, + FogEnd: 0f, + FogMode: default, + LightningFlash: 0f, + Override: default); + var input = new DirectionalSunShadowRenderInput( + new DirectionalShadowEnvironmentInput( + PackEnabled: true, + PortalOrLoginCoverVisible: false, + PlayerInsideCell: false, + Source: validSource, + Atmosphere: validAtmosphere, + ActiveDayGroupMultiplier: 1f), + Matrix4x4.Identity, + Matrix4x4.Identity, + new DirectionalShadowCasterFrame(), + CameraNearMeters: 0.1f, + ResidentMaximumReachMeters: 0.05f, // <= CameraNearMeters + AtmosphericFrame: atmosphericFrame); + device.Clear(); + using IGpuFrame frame = device.BeginFrame(); + + DirectionalSunShadowDiagnostics? gated = renderer.EvaluateGateAndPublishDisabledBinding( + frame, + in input, + out DirectionalShadowEnvironmentState environment, + out long environmentGateTicks); + + Assert.NotNull(gated); + Assert.Equal(DirectionalShadowGateReason.ResidentWindowUnavailable, gated.Value.GateReason); + Assert.Equal(DirectionalShadowGateReason.ResidentWindowUnavailable, environment.Reason); + Assert.True(renderer.TryGetCurrentFrameBinding(frame, out DirectionalShadowFrameBinding binding)); + Assert.True(binding.IsBindableFor(frame)); + Assert.False(binding.IsValidFor(frame)); + } + + [Fact] + public void ShouldSelectReceiverPipelineComposesWithTheRealBindingSourceAfterADisabledPublish() + { + // Campaign VM VM6 review fix round 5, test T2: proves the ACTUAL + // composition WbDrawDispatcher.PipelinesFor and + // TerrainModernRenderer both use — TryGetCurrentFrameBinding + // feeding ShouldSelectReceiverPipeline — selects the receiver + // pipeline for the atmospheric world pass once a disabled binding + // is published, and still refuses a non-atmospheric pass name. + // ShouldSelectReceiverPipeline itself is unchanged this round; + // this is the composition its own DirectionalShadowReceiverTests + // parametrized coverage does not exercise. + using var device = new RecordingGpuDevice(); + using var renderer = new DirectionalSunShadowRenderer(device, DirectionalShadowPreset.Low); + using IGpuBuffer atmosphericBuffer = Buffer(device, "gate-pipeline-frame", GpuBufferUsage.Uniform); + var atmosphericFrame = new AtmosphericFrameBufferBinding(atmosphericBuffer, 0u, 192u); + device.Clear(); + using IGpuFrame frame = device.BeginFrame(); + + renderer.PublishDisabledReceiverBinding(frame, atmosphericFrame); + bool bindingValid = renderer.TryGetCurrentFrameBinding(frame, out _); + + Assert.True(bindingValid); + Assert.True(DirectionalShadowReceiverPolicy.ShouldSelectReceiverPipeline( + DirectionalShadowReceiverPolicy.AtmosphericWorldPassName, + sourcePresent: true, + bindingValid)); + Assert.False(DirectionalShadowReceiverPolicy.ShouldSelectReceiverPipeline( + "vk-world", + sourcePresent: true, + bindingValid)); + } + [Fact] public void BindDirectionalShadowReceiver_WithADisabledBindingEmitsBothShadowAndAtmosphericBinds() { diff --git a/tests/AcDream.App.Tests/Rendering/Packs/AtmosphericPostProcessGraphTests.cs b/tests/AcDream.App.Tests/Rendering/Packs/AtmosphericPostProcessGraphTests.cs index e84bc758..0c8293ea 100644 --- a/tests/AcDream.App.Tests/Rendering/Packs/AtmosphericPostProcessGraphTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Packs/AtmosphericPostProcessGraphTests.cs @@ -1222,6 +1222,55 @@ public sealed class AtmosphericPostProcessGraphTests // ── Campaign VM VM6: foliage wind ──────────────────────────────────── + [Fact] + public void ReceiverVertexShadersFallBackToThePlainLightDirectionWhenTheShadowFlagIsClear() + { + // Campaign VM VM6 review fix round 5 (F1 BLOCKER, test T3): pins + // the shader SOURCE TEXT so a future edit that removes the + // flag-gated fallback (reintroducing "a shadow-gated-off frame + // lights outdoor terrain/objects from the disabled block's + // (0,0,1) placeholder instead of the authored sun") fails this + // test instead of only showing up in a pixel capture. Both + // receiver vertex shaders must (1) test the SAME flags bit + // acdreamDirectionalShadowVisibility already tests, and (2) + // reference the SAME uLights-derived expression the corresponding + // plain (mesh_modern.vert / terrain_modern.vert) pipeline uses, + // proving the fallback branch is not some other, unverified + // direction. + string shaderRoot = Path.Combine( + RepositoryRoot(), + "src", + "AcDream.App", + "Rendering", + "Shaders"); + string meshAtmospheric = File.ReadAllText(Path.Combine(shaderRoot, "mesh_atmospheric.vert")); + string meshModern = File.ReadAllText(Path.Combine(shaderRoot, "mesh_modern.vert")); + string terrainAtmospheric = File.ReadAllText(Path.Combine(shaderRoot, "terrain_atmospheric.vert")); + string terrainModern = File.ReadAllText(Path.Combine(shaderRoot, "terrain_modern.vert")); + + const string flagTest = "(uShadowTextureAndFlags.w & 1u) == 0u"; + Assert.Contains(flagTest, meshAtmospheric, StringComparison.Ordinal); + Assert.Contains(flagTest, terrainAtmospheric, StringComparison.Ordinal); + + // mesh: both files negate the per-light uniform inline (no + // intermediate variable), so the literal expression matches + // string-for-string. + const string meshPlainDirection = "-uLights[i].dirAndRange.xyz"; + Assert.Contains(meshPlainDirection, meshModern, StringComparison.Ordinal); + Assert.Contains(meshPlainDirection, meshAtmospheric, StringComparison.Ordinal); + + // terrain: terrain_modern.vert reads the uniform into `sunDir` + // then negates that variable two lines later, so only the + // shared uniform-access substring (not the exact negation + // placement) is asserted identical between the two files. + const string terrainLightUniform = "uLights[0].dirAndRange.xyz"; + Assert.Contains(terrainLightUniform, terrainModern, StringComparison.Ordinal); + Assert.Contains( + "-" + terrainLightUniform, + terrainAtmospheric, + StringComparison.Ordinal); + } + [Fact] public void FirstAdvanceSnapsExactlyToTheWeatherTargetInsteadOfEasingFromZero() {