From 94ddde69af50915271679c119ec30b8f20ea795f Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 5 Sep 2026 03:47:08 +0200 Subject: [PATCH] Fix atmospheric receiver light direction Keep both atmospheric vertex receivers on retail's exact authored, unnormalized uLights direction in every shadow-gate state while preserving the selected celestial direction for fragment shadow projection and volumetrics. Regenerate the affected SPIR-V and amend IA-24. Proof adds committed-SPIR-V dataflow checks, parent plain/pipeline source pins, and a real Vulkan mesh+terrain pixel witness owned by Lane=Vulkan. Mutation first failures (all restored): 1. Restoring the mesh shadow/celestial branch failed CommittedProductionAtmosphericReceivers_KeepAuthoredLightAcrossShadowGate at line 27: expected Pixel 128/128/128/255, actual 0/0/0/255. 2. Restoring the terrain shadow/celestial branch failed the same witness at line 28: expected Pixel 128/128/128/255, actual 0/0/0/255. 3. Normalizing the authored mesh direction failed AssertAuthoredHalfIntensity at line 277: expected 126..129, actual 255. 4. Removing atmospheric_volumetric.frag's celestial xyz use failed ProductionShadowAndVolumetricModules_StillNormalizeTheCelestialProjectionDirection at line 61: Assert.Single found no matching member-5 access. 5. Restoring IA-24's old celestial-base-light claim failed BuiltInAndDeclaredShadowGraphsUseTheSameTypedPriorVisibilitySelector at line 1482: the authored unnormalized uLights sole-base-light assertion was absent. --- .../retail-divergence-register.md | 2 +- .../Rendering/Shaders/mesh_atmospheric.vert | 30 +- .../Shaders/spv/mesh_atmospheric.vert.spv | Bin 13344 -> 12712 bytes .../Shaders/spv/shaders.manifest.json | 4 +- .../Shaders/spv/terrain_atmospheric.vert.spv | Bin 9828 -> 9228 bytes .../Shaders/terrain_atmospheric.vert | 20 +- .../AtmosphericLightingDirectionSpirvTests.cs | 230 +++++++++++++ ...IndexOffscreenTests.AtmosphericLighting.cs | 310 ++++++++++++++++++ .../Vk/MeshModernSharedIndexOffscreenTests.cs | 2 +- .../Packs/AtmosphericPostProcessGraphTests.cs | 68 ++-- 10 files changed, 588 insertions(+), 78 deletions(-) create mode 100644 tests/AcDream.App.Tests/Rendering/Gpu/Vk/AtmosphericLightingDirectionSpirvTests.cs create mode 100644 tests/AcDream.App.Tests/Rendering/Gpu/Vk/MeshModernSharedIndexOffscreenTests.AtmosphericLighting.cs diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index 38ae2e46..328b6ea8 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -62,7 +62,7 @@ accepted-divergence entries (#96, #49, #50). | IA-21 | When ACE sends player BoolProperty `68` (`SpellComponentsRequired`) false, acdream presents the retail scarab/prismatic-taper formula even without a directly carried school focus. With component enforcement enabled, retail's exact focus/infusion versus account-customized selection remains intact. | `src/AcDream.App/Spells/SpellComponentRequirementService.cs` | A component-disabled server has no actionable legacy recipe; explicit product direction is that this client/server mode uses the modern scarab/taper component presentation | A custom server could expect retail's legacy recipe to remain visible even though casting consumes no components | `ClientMagicSystem::AreSpellComponentsRequired @ 0x00567B90`; `ClientMagicSystem::GetAppropriateSpellFormula @ 0x00567D50`; `CSpellBase::InqScarabOnlyFormula @ 0x00597050` | | IA-22 | **Filed 2026-08-13 (#391, user-directed: "we should only support modern resolutions. Not any old format").** The Config Resolution dropdown offers a CURATED list — the monitor's real mode enumeration filtered to modern widescreen families (16:9/16:10/21:9/32:9, ≥1280 wide, fitting the desktop; `DisplayModeCatalog.Curate`) — and its Defaults value is the desktop's own mode. Retail offered the adapter's complete enumeration including 4:3 legacy modes and authored `800x600` as the row default (`gmConfigUI::InitOptions SetDefaultValue(0x03200258)`; `gmClient::Init @0x004047af` `Device::ForceDisplayResolution(1, 0x320, 0x258)`). | `src/AcDream.App/Rendering/DisplayModeCatalog.cs`; `src/AcDream.App/UI/Layout/ConfigOptionsPageController.cs` (Resolution row); fixture fallback `src/AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs` (`AvailableResolutions`, 800x600 removed) | Explicit product direction. **Amended 2026-08-16 (#407, Campaign CC gate round 1):** the dropdown now offers `DisplayModeCatalog.WindowedResolutions` — the curated hardware modes UNIONed with the static modern-ladder sizes that fit the desktop — because a WINDOWED pick is a plain Size write needing no video mode, and remote/RDP virtual displays advertise almost no modes (the live RDP display exposed only 1920x1080 + the 2056x1290 desktop, starving the dropdown). The original "an offered mode is supported by construction" invariant now holds for the FULLSCREEN half only: the fullscreen apply still validates against the hardware `Resolutions` list plus `GlfwDisplayModeSwitcher`'s monitor-mode-list hard guard, so a fullscreen pick of a windowed-only entry refuses safely (log-and-stay, #388; the #392 apply-result seam is that family's open follow-up) — "Graphics mode not supported" crashes remain unreachable from the dropdown. | A user wanting a genuine legacy 4:3 mode cannot pick it; retail-parity comparisons of the Config tab's list/default will show the deviation. | decomp sites in the Divergence column; ISSUES #391 | | IA-23 | **Filed 2026-08-17 at the night-round review fix round (F8).** `gmHouseUI::DisplayPurchaseTimeText @0x004a3110`'s not-yet-expired branch renders `"You may buy another landscape house at " + strftime("%c", localtime(timestamp + 0x278d00)) + ". This restriction does not apply to apartments."` — byte-decoded from raw pushed literals at `@0x004a3265`/`@0x004a321d`/`@0x004a3235` (all three text pieces confirmed; a prior filing had wrongly called this "unrecoverable"). This port renders the SAME three pieces, in the same order, with the same expiry-timestamp math, but formats the middle date/time piece with .NET's culture-default `DateTime.ToString()` (no explicit format string) rather than the C runtime's `strftime("%c", ...)` — the two engines do not share a format table, so the RENDERED SHAPE of the date/time differs (e.g. .NET's short numeric date+time vs the CRT's `Ddd Mon DD HH:MM:SS YYYY`-style locale string) even though both express "the process's own locale's full date+time" and use the SAME underlying instant (local time, matching retail's `localtime()`). | `src/AcDream.Runtime/Gameplay/RuntimeHouseState.cs` (`Recompute`'s not-expired branch) | Both are "whatever the process locale says" full date+time strings; no game-logic reads or parses this text back, it is pure chat-scroll presentation, so a differently-shaped (but equally legible) date string carries no functional risk | A retail-side-by-side visual comparison will show a differently formatted date/time (not a byte-identical `strftime("%c")` reproduction) — cosmetic only | `gmHouseUI::DisplayPurchaseTimeText @0x004a3110`; `strftime`/`localtime` CRT calls at `@0x004a322c`/`@0x004a3216` | -| IA-24 | **Filed 2026-08-22, Campaign AR; amended 2026-09-04, Campaign OVERHAUL S5-c2 and its graphical closeout.** An explicitly selected atmospheric render pack adds cascaded real-time directional shadows from terrain, trees, buildings, players, monsters, and other retained outdoor casters. The one shadow direction follows the visible authored sun, then the dominant haloed moon (`0x01001F6A`), then the secondary moon (`0x01001F67`); a moon supplies direction only while colour/energy remains retail's single interpolated `SkyTimeOfDay.DirColor × DirBright` channel. Its active terrain/object projection borrows retail's **prior successfully completed** landscape visibility by exact set reference, then selects ordinary objects by S2's retained retail CELLARRAY intersection and building shells by their outdoor placement `EffectCellId`; missing membership and no completed view fail closed, with no resident/frustum fallback. **While shadows render, the pack's receiver vertex shaders (`mesh_atmospheric.vert`, `terrain_atmospheric.vert`) also take the outdoor directional LIGHTING direction from that same celestial source instead of retail's authored `uLights[0]` direction, so the lit term agrees with the shadow direction; whenever the shadow gate is closed (night, user strength 0, portal cover, indoor) the flag bit is clear and both shaders fall back to the plain pipeline's authored-light expression (Campaign VM VM6 round 5, `754d59d9`). The switch is binary even when the effective shadow strength is small: S5-c2's exact-parent/candidate graphical A/B (§16) reproduces a conspicuous whole-world darkening across pack re-enable. Issue #469 keeps that pre-existing gate discontinuity open before G4; recording it here does not classify it as an S5-c2 regression.** Retail renders none of these real-time object-shadow maps and does not expose a second moon light. | `src/AcDream.App/Rendering/Packs/AuthoredCelestialShadowSource.cs`; `src/AcDream.App/Rendering/DirectionalSunShadowRenderer.cs`; `src/AcDream.App/Rendering/Scene/DirectionalShadowCasterFrame.cs`; pack-only receiver shaders; evidence `docs/research/2026-08-22-dereth-celestial-shadow-sources.md` and OVERHAUL packet §§14,16 | This is the user-requested headline graphics enhancement and is strictly opt-in. Borrowing the prior completed retail landscape product and exact S2 membership narrows pack-on work without inventing another visibility owner. The retail path remains the default and authoritative fallback; pack-off does not build, select, upload, or draw shadow work and does not change `SceneLighting`. One selected source reuses one cascade array, so moon support does not multiply shadow resources. | Pack-on output intentionally differs from retail and deliberately trails camera visibility by one successfully completed world view. A wrong CELLARRAY/EffectCell identity, stale prior-view transaction, celestial transform, or unresolved #469 gate transition would visibly add, omit, misalign, or abruptly relight the world; pack-off output changing would violate the campaign's primary safety contract. | Landscape input: `LScape::draw_check_blocks @0x00505F80`, `LScape::landcell_check @0x005050A0`, `CLandCell::IsInView @0x00532CB0`; celestial: `SkyDesc::GetLighting @0x00500A80`, `GameSky::UseTime @0x005075B0`; installed Region `0x13000000`; cited research notes | +| IA-24 | **Filed 2026-08-22, Campaign AR; amended 2026-09-04, Campaign OVERHAUL S5-c2 and its graphical closeout; amended 2026-09-05, Campaign OVERHAUL S5 issue #469.** An explicitly selected atmospheric render pack adds cascaded real-time directional shadows from terrain, trees, buildings, players, monsters, and other retained outdoor casters. The one shadow direction follows the visible authored sun, then the dominant haloed moon (`0x01001F6A`), then the secondary moon (`0x01001F67`); a moon supplies direction only while colour/energy remains retail's single interpolated `SkyTimeOfDay.DirColor × DirBright` channel. Its active terrain/object projection borrows retail's **prior successfully completed** landscape visibility by exact set reference, then selects ordinary objects by S2's retained retail CELLARRAY intersection and building shells by their outdoor placement `EffectCellId`; missing membership and no completed view fail closed, with no resident/frustum fallback. **The authored, unnormalized `uLights` direction remains the sole base-light direction in both receiver vertex shaders (`mesh_atmospheric.vert`, `terrain_atmospheric.vert`) in every shadow-gate state, exactly matching their plain counterparts. The selected celestial source drives only the opt-in shadow and volumetric projection. Its direction may therefore visibly differ from the authored base-light angle; that light/shadow-angle separation is the deliberate cost of preserving retail lighting while eliminating the former binary whole-world relight across pack re-enable.** Retail renders none of these real-time object-shadow maps and does not expose a second moon light. | `src/AcDream.App/Rendering/Packs/AuthoredCelestialShadowSource.cs`; `src/AcDream.App/Rendering/DirectionalSunShadowRenderer.cs`; `src/AcDream.App/Rendering/Scene/DirectionalShadowCasterFrame.cs`; pack-only receiver shaders; evidence `docs/research/2026-08-22-dereth-celestial-shadow-sources.md` and OVERHAUL packet §§14,16,31 | This is the user-requested headline graphics enhancement and is strictly opt-in. Borrowing the prior completed retail landscape product and exact S2 membership narrows pack-on work without inventing another visibility owner. The retail path remains the default and authoritative fallback; pack-off does not build, select, upload, or draw shadow work and does not change `SceneLighting`. One selected source reuses one cascade array, so moon support does not multiply shadow resources. | Pack-on output intentionally differs from retail and deliberately trails camera visibility by one successfully completed world view. A wrong CELLARRAY/EffectCell identity, stale prior-view transaction, celestial transform, or authored-light/celestial projection separation would visibly add, omit, misalign, or redirect shadows; any whole-world base-light jump across pack gating or any pack-off output change would violate the campaign's primary safety contract. | Landscape input: `LScape::draw_check_blocks @0x00505F80`, `LScape::landcell_check @0x005050A0`, `CLandCell::IsInView @0x00532CB0`; celestial: `SkyDesc::GetLighting @0x00500A80`, `GameSky::UseTime @0x005075B0`; installed Region `0x13000000`; cited research notes | | IA-25 | **Filed 2026-08-22, Campaign VM VM6.** An explicitly selected atmospheric render pack sways procedural-scenery foliage (trees/bushes — entity ids in the `0x8XXYYIII` `ProceduralSceneryIdAllocator` namespace) in `mesh_atmospheric.vert` and the four `directional_shadow_world_*` caster vertex shaders, driven by a weather-table lean/branch/flutter vertex displacement (`foliage_wind.glsl`, `FoliageWindModel` CPU mirror) whose mean/gust strength is looked up per DAT-classified `AcDream.Core.World.WeatherKind` (Clear/Overcast/Rain/Snow/Storm — the same classification `WeatherState.cs` already derives from the active day group's authored name, not the day group's raw index, which carries no weather meaning by itself) and eases toward its target over `WeatherSystem.TransitionSeconds` (10 s) so a weather change never snaps. Retail's fixed-function renderer applies no per-vertex wind displacement to any scenery mesh — Dereth's trees are static geometry. Wind direction (`wind-direction-degrees`, default 225°) is a plain pack default: there is no authored retail wind direction to read (no wind data exists in retail at all). Render-only: `WorldPicker` picks the undisplaced mesh, so a swaying leaf can be up to `lean + branch` metres from its pick volume at the moment of a click; foliage subsets are cosmetic scenery, not interactable in retail either. | `src/AcDream.App/Rendering/Shaders/foliage_wind.glsl`; `src/AcDream.App/Rendering/Wb/FoliageWindClassification.cs`; `src/AcDream.App/Rendering/Packs/FoliageWindModel.cs`; `src/AcDream.App/Rendering/Packs/AtmosphericPostProcessGraph.cs` (`ResolveFoliageWind`); `src/AcDream.App/Rendering/Packs/BuiltInAtmosphericRenderPack.cs` (wind settings + `FoliageWindByWeather`) | Explicitly opt-in graphics enhancement — the retail path (`mesh_modern`, `terrain_modern`) never reads `BatchData.flags` bits 1/2 and is pixel-identical with the pack off. The classification never touches Runtime/Core physics — the collision BSP is the trunk, and picking against the undisplaced mesh has no gameplay consequence since foliage is not interactable. | Pack-on output intentionally differs from retail (moving foliage where retail has none). A wrong classification bit would sway a non-foliage object or leave a real tree still; a caster/receiver clock or amplitude mismatch would visibly misalign a leaf's shadow from the leaf itself. Pack-off output changing would violate the campaign's primary safety contract. | None — retail applies no vertex wind displacement to any geometry; `ProceduralSceneryIdAllocator` (top-nibble-0x8 entity-id namespace, existing acdream mechanism, not retail) | --- diff --git a/src/AcDream.App/Rendering/Shaders/mesh_atmospheric.vert b/src/AcDream.App/Rendering/Shaders/mesh_atmospheric.vert index 43f426e6..c45f9fda 100644 --- a/src/AcDream.App/Rendering/Shaders/mesh_atmospheric.vert +++ b/src/AcDream.App/Rendering/Shaders/mesh_atmospheric.vert @@ -247,31 +247,19 @@ 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 matches the plain - // pipeline to within float summation-order rounding - // (review fix round 6, N2: the atmospheric fragment's split - // ambient+point vs directional accumulation reassociates the - // sum relative to the plain pipeline's single varying — - // measured mean |Δ| 0.007 on the offline scene, not bit- - // identical). Hoisted out of the loop: a uniform branch, - // evaluated once per vertex regardless of light count. - bool shadowGatedOff = (uShadowTextureAndFlags.w & 1u) == 0u; + // Campaign OVERHAUL S5 #469: retail owns one authored outdoor + // directional-light channel (SkyDesc::GetLighting -> + // LScape::set_landscape_lighting -> Render::world_lights.sunlight). + // Keep the EXACT plain mesh_modern.vert direction in every shadow + // gate state: negate uLights[].dirAndRange.xyz and preserve its + // authored, unnormalized magnitude. The selected celestial source + // remains pack-only projection input in the fragment receiver and + // volumetric shaders; it never replaces base vertex lighting. 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 = shadowGatedOff - ? -uLights[i].dirAndRange.xyz - : normalize(uShadowLightDirectionAndSource.xyz); + vec3 Ldir = -uLights[i].dirAndRange.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 3dc26aa08096511f50ad930ff421bb98854a9efc..4600c0bfccaff01fb0862606fd2608042bb16bc2 100644 GIT binary patch literal 12712 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 R?{IWt`I|ZUZa6bL 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 diff --git a/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json b/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json index bd61c558..e8e3ddca 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": "bf470ee19d0ea5fd36f6ac96e307a71845e50d8872263cfa15d1c36fc6424732", + "sourceSha256": "b1e8e52b0b3dedec6c7dfd29ce3e70deb44d6b18197d6bd312e1a34e543f8337", "compiled": true }, { @@ -311,7 +311,7 @@ "stages": [ { "stage": "vert", - "sourceSha256": "42d1dbd0dbd950862e8672f79a3f429f771bae55fd2b5721c3e551a0ea9563b6", + "sourceSha256": "cd300c58b15401fa1b409b1df333b13c8aca7ff26c02615dd4811a39ffd79460", "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 0cc9e4d3e46f63af8dcd5458291a830c9d63e484..309f9f4af3d2977dae703423b64eba4799746a8b 100644 GIT binary patch literal 9228 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 diff --git a/src/AcDream.App/Rendering/Shaders/terrain_atmospheric.vert b/src/AcDream.App/Rendering/Shaders/terrain_atmospheric.vert index fe8021f1..9dc38cb2 100644 --- a/src/AcDream.App/Rendering/Shaders/terrain_atmospheric.vert +++ b/src/AcDream.App/Rendering/Shaders/terrain_atmospheric.vert @@ -142,20 +142,12 @@ void main() { vWorldNormal = normalize(aNormal); // Retail AdjustPlanes bake (terrain.vert:124-134 — identical math). - // Campaign VM VM6 review fix round 5 (F1 BLOCKER): when the shadow - // block's flag bit is clear (a gated-off frame), uShadowLightDirectionAndSource - // carries only a (0,0,1) NaN guard, NOT a real light direction. Fall - // back to the EXACT plain terrain_modern.vert expression - // (-uLights[0].dirAndRange.xyz, unnormalized — matched bit-for-bit) - // so a gated-off frame matches the plain pipeline to within float - // summation-order rounding (review fix round 6, N2: terrain's two - // separate varyings vs the plain pipeline's one reassociate the sum - // — measured mean |Δ| 0.007 on the offline scene, not bit-identical) - // — see the matching comment in mesh_atmospheric.vert's accumulateLights. - bool shadowGatedOff = (uShadowTextureAndFlags.w & 1u) == 0u; - vec3 surfaceToLight = shadowGatedOff - ? -uLights[0].dirAndRange.xyz - : normalize(uShadowLightDirectionAndSource.xyz); + // Campaign OVERHAUL S5 #469: SkyDesc::GetLighting authors retail's one + // outdoor light direction. Keep the EXACT plain terrain.vert expression + // in every shadow gate state: negate uLights[0].dirAndRange.xyz without + // normalizing it. The selected celestial source remains pack-only input + // to fragment shadow projection and atmospheric volumetrics. + vec3 surfaceToLight = -uLights[0].dirAndRange.xyz; vec3 sunCol = uLights[0].colorAndIntensity.xyz * uLights[0].colorAndIntensity.w; float L = max(dot(vWorldNormal, surfaceToLight), MIN_FACTOR); // Preserve retail's authored lighting values, but keep the outdoor diff --git a/tests/AcDream.App.Tests/Rendering/Gpu/Vk/AtmosphericLightingDirectionSpirvTests.cs b/tests/AcDream.App.Tests/Rendering/Gpu/Vk/AtmosphericLightingDirectionSpirvTests.cs new file mode 100644 index 00000000..9c9d7f0c --- /dev/null +++ b/tests/AcDream.App.Tests/Rendering/Gpu/Vk/AtmosphericLightingDirectionSpirvTests.cs @@ -0,0 +1,230 @@ +using System.Security.Cryptography; +using System.Text; + +namespace AcDream.App.Tests.Rendering.Gpu.Vk; + +/// +/// S5 issue #469: inspect the committed production modules so source comments +/// or an unused lookalike expression cannot satisfy the authored-light proof. +/// +public sealed class AtmosphericLightingDirectionSpirvTests +{ + private const ushort OpExtInst = 12; + private const ushort OpConstant = 43; + private const ushort OpVariable = 59; + private const ushort OpLoad = 61; + private const ushort OpAccessChain = 65; + private const ushort OpVectorShuffle = 79; + private const ushort OpDecorate = 71; + private const ushort OpFNegate = 127; + private const ushort OpDot = 148; + + private const uint DecorationBinding = 33; + private const uint DecorationDescriptorSet = 34; + private const uint SceneLightingSet = 1; + private const uint SceneLightingBinding = 1; + private const uint PackUniformSet = 3; + private const uint DirectionalShadowBinding = 6; + + [Theory] + [InlineData("mesh_atmospheric.vert.spv", false)] + [InlineData("terrain_atmospheric.vert.spv", true)] + public void ProductionVertexModule_DotsTheNegatedAuthoredDirectionWithoutNormalization( + string fileName, + bool terrain) + { + Spirv module = Spirv.Read(Shader(fileName)); + + Assert.Empty(module.RootVariables(PackUniformSet, DirectionalShadowBinding)); + uint sceneLighting = Assert.Single(module.RootVariables(SceneLightingSet, SceneLightingBinding)); + Instruction direction = Assert.Single(module.Instructions, instruction => + instruction.OpCode == OpAccessChain + && instruction.Operands[0] == sceneLighting + && IsAuthoredDirectionPath(module, instruction, terrain)); + Instruction loaded = Assert.Single(module.WithOperand(OpLoad, direction.ResultId)); + Instruction xyz = Assert.Single(module.WithOperand(OpVectorShuffle, loaded.ResultId)); + Instruction negated = Assert.Single(module.WithOperand(OpFNegate, xyz.ResultId)); + Instruction dot = Assert.Single(module.WithOperand(OpDot, negated.ResultId)); + + Assert.Equal(negated.ResultId, dot.Operands[1]); + } + + [Theory] + [InlineData("mesh_atmospheric.frag.spv")] + [InlineData("terrain_atmospheric.frag.spv")] + [InlineData("atmospheric_volumetric.frag.spv")] + public void ProductionShadowAndVolumetricModules_StillNormalizeTheCelestialProjectionDirection( + string fileName) + { + Spirv module = Spirv.Read(Shader(fileName)); + uint shadowBlock = Assert.Single(module.RootVariables(PackUniformSet, DirectionalShadowBinding)); + Instruction direction = Assert.Single(module.Instructions, instruction => + instruction.OpCode == OpAccessChain + && instruction.Operands[0] == shadowBlock + && instruction.Operands.Length == 2 + && module.Constant(instruction.Operands[1]) == 5u); + Instruction loaded = Assert.Single(module.WithOperand(OpLoad, direction.ResultId)); + Instruction xyz = Assert.Single(module.WithOperand(OpVectorShuffle, loaded.ResultId)); + + Assert.Contains( + module.Instructions, + instruction => instruction.OpCode == OpExtInst + && instruction.Operands.Contains(xyz.ResultId)); + } + + [Fact] + public void ParentPlainShadersAndReceiverSelectionSourcesRemainByteExact() + { + AssertNormalizedTextHash( + "d5af0cbe995bcd10acea92ac7140363f0941105452a89039a7b54c5217c505a2", + "src", "AcDream.App", "Rendering", "Shaders", "mesh_modern.vert"); + AssertBinaryHash( + "6194193764616bc88bf967d1fdc2bea5f6f2d1ec2ef94fd492922137503f56e4", + "src", "AcDream.App", "Rendering", "Shaders", "spv", "mesh_modern.vert.spv"); + AssertNormalizedTextHash( + "b80c201771b6adf3a13b5564492c5188f53a59e61b4d730fb5037ae322b0dde0", + "src", "AcDream.App", "Rendering", "Shaders", "mesh_modern.frag"); + AssertBinaryHash( + "bd47fe8a33e0f1d025fe48fd95b034ba6fe591a6d20e88e636c86238d8773db1", + "src", "AcDream.App", "Rendering", "Shaders", "spv", "mesh_modern.frag.spv"); + AssertNormalizedTextHash( + "faf6855222cb2b09da6697c93a93f35cbc57c27bbc124055a6d235c72ef1a23a", + "src", "AcDream.App", "Rendering", "Shaders", "terrain_modern.vert"); + AssertBinaryHash( + "8a73d89ef0e51e550327b9ff8c24857e309103b1d491030cf0d4d8594b45068c", + "src", "AcDream.App", "Rendering", "Shaders", "spv", "terrain_modern.vert.spv"); + AssertNormalizedTextHash( + "a9f0ad2e679b68ee2de6c4877202f6f5e5089d0f7368fcedd6048891c6848915", + "src", "AcDream.App", "Rendering", "Shaders", "terrain_modern.frag"); + AssertBinaryHash( + "7b3cdb01b837ed77ee20559a81c1ce5c9d5395300efcc072560ab0be3c5a1af9", + "src", "AcDream.App", "Rendering", "Shaders", "spv", "terrain_modern.frag.spv"); + AssertNormalizedTextHash( + "58c54e02bc0ba1d1086f05ef211e58eb6ba4d28d9a6be9ce3027509fee93ccf0", + "src", "AcDream.App", "Rendering", "Wb", "WbDrawDispatcher.DirectionalShadowReceivers.cs"); + AssertNormalizedTextHash( + "ef33fb37b019e6b32932fb501ed87721dab496611c580cb046b8f1e80249efe2", + "src", "AcDream.App", "Rendering", "TerrainModernRenderer.DirectionalShadowReceivers.cs"); + AssertNormalizedTextHash( + "55c6512f164376877c8bf28bff7487b7a2ce18ec963798eaeaf795c3a043c6b4", + "src", "AcDream.App", "Rendering", "Packs", "RenderPackReceiverPipelineCoordinator.cs"); + AssertNormalizedTextHash( + "34186c77d66bd22aaeff439b31a0ae0a7af8d611ec7d4a3429fc40705034e028", + "src", "AcDream.App", "Rendering", "DirectionalShadowReceiver.cs"); + } + + private static bool IsAuthoredDirectionPath( + Spirv module, + in Instruction instruction, + bool terrain) + { + if (instruction.Operands.Length != 4 + || module.Constant(instruction.Operands[1]) != 0u + || module.Constant(instruction.Operands[3]) != 1u) + return false; + + return !terrain || module.Constant(instruction.Operands[2]) == 0u; + } + + private static void AssertNormalizedTextHash(string expected, params string[] relativePath) + { + string text = File.ReadAllText(Path.Combine([RepositoryRoot(), .. relativePath])) + .Replace("\r\n", "\n", StringComparison.Ordinal); + Assert.Equal(expected, Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(text))).ToLowerInvariant()); + } + + private static void AssertBinaryHash(string expected, params string[] relativePath) => + Assert.Equal( + expected, + Convert.ToHexString(SHA256.HashData(File.ReadAllBytes( + Path.Combine([RepositoryRoot(), .. relativePath])))).ToLowerInvariant()); + + private static string Shader(string fileName) => Path.Combine( + RepositoryRoot(), "src", "AcDream.App", "Rendering", "Shaders", "spv", fileName); + + private readonly record struct Instruction(ushort OpCode, uint ResultId, uint[] Operands); + + private sealed class Spirv + { + private readonly Dictionary> _decorations = []; + private readonly Dictionary _constants = []; + + private Spirv() + { + } + + internal Dictionary Variables { get; } = []; + internal List Instructions { get; } = []; + + internal static Spirv Read(string path) + { + byte[] bytes = File.ReadAllBytes(path); + Assert.True(bytes.Length >= 20 && bytes.Length % sizeof(uint) == 0, $"{path} is not SPIR-V."); + uint[] words = new uint[bytes.Length / sizeof(uint)]; + Buffer.BlockCopy(bytes, 0, words, 0, bytes.Length); + Assert.Equal(0x07230203u, words[0]); + + var module = new Spirv(); + for (int index = 5; index < words.Length;) + { + int wordCount = checked((int)(words[index] >> 16)); + ushort opCode = checked((ushort)(words[index] & 0xFFFFu)); + Assert.True(wordCount > 0 && index + wordCount <= words.Length); + + if (opCode == OpDecorate && wordCount >= 4) + { + uint target = words[index + 1]; + if (!module._decorations.TryGetValue(target, out Dictionary? values)) + module._decorations[target] = values = []; + values[words[index + 2]] = words[index + 3]; + } + else if (opCode == OpConstant && wordCount >= 4) + { + module._constants[words[index + 2]] = words[index + 3]; + } + else if (opCode == OpVariable && wordCount >= 4) + { + module.Variables[words[index + 2]] = words[index + 3]; + } + + if (HasResult(opCode)) + { + module.Instructions.Add(new Instruction( + opCode, + words[index + 2], + words.AsSpan(index + 3, wordCount - 3).ToArray())); + } + index += wordCount; + } + return module; + } + + internal uint? Constant(uint id) => _constants.TryGetValue(id, out uint value) ? value : null; + + internal IEnumerable RootVariables(uint set, uint binding) => Variables.Keys + .Where(id => Decoration(id, DecorationDescriptorSet) == set) + .Where(id => Decoration(id, DecorationBinding) == binding); + + internal IEnumerable WithOperand(ushort opCode, uint operand) => + Instructions.Where(instruction => instruction.OpCode == opCode + && instruction.Operands.Contains(operand)); + + private uint? Decoration(uint id, uint decoration) => + _decorations.TryGetValue(id, out Dictionary? values) + && values.TryGetValue(decoration, out uint value) + ? value + : null; + + private static bool HasResult(ushort opCode) => opCode is + OpExtInst or OpLoad or OpAccessChain or OpVectorShuffle or OpFNegate or OpDot; + } + + private static string RepositoryRoot() + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "AcDream.slnx"))) + directory = directory.Parent; + return directory?.FullName + ?? throw new InvalidOperationException("Could not locate the repository root."); + } +} diff --git a/tests/AcDream.App.Tests/Rendering/Gpu/Vk/MeshModernSharedIndexOffscreenTests.AtmosphericLighting.cs b/tests/AcDream.App.Tests/Rendering/Gpu/Vk/MeshModernSharedIndexOffscreenTests.AtmosphericLighting.cs new file mode 100644 index 00000000..485e8ca8 --- /dev/null +++ b/tests/AcDream.App.Tests/Rendering/Gpu/Vk/MeshModernSharedIndexOffscreenTests.AtmosphericLighting.cs @@ -0,0 +1,310 @@ +using System.Numerics; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using AcDream.App.Rendering; +using AcDream.App.Rendering.Gpu; +using AcDream.App.Rendering.Gpu.Vk; +using AcDream.App.Rendering.Packs; +using AcDream.Core.Lighting; + +namespace AcDream.App.Tests.Rendering.Gpu.Vk; + +public sealed unsafe partial class MeshModernSharedIndexOffscreenTests +{ + [Trait("Lane", "Vulkan")] + [Fact] + public void CommittedProductionAtmosphericReceivers_KeepAuthoredLightAcrossShadowGate() + { + lock (VulkanLock) + { + string shaderDirectory = Path.Combine( + RepositoryRoot(), "src", "AcDream.App", "Rendering", "Shaders", "spv"); + using var host = HeadlessVulkanHost.Create(shaderDirectory); + + (Pixel meshOff, Pixel meshOn) = RenderMeshLightingPair(host); + (Pixel terrainOff, Pixel terrainOn) = RenderTerrainLightingPair(host); + + Assert.Equal(meshOff, meshOn); + Assert.Equal(terrainOff, terrainOn); + AssertAuthoredHalfIntensity(meshOff, "mesh"); + AssertAuthoredHalfIntensity(terrainOff, "terrain"); + } + } + + [Fact] + public void AtmosphericLightingPixelWitness_IsOwnedByTheDedicatedVulkanLane() + { + string source = File.ReadAllText(Path.Combine( + RepositoryRoot(), "tests", "AcDream.App.Tests", "Rendering", "Gpu", "Vk", + "MeshModernSharedIndexOffscreenTests.AtmosphericLighting.cs")); + Assert.Matches( + new Regex( + @"\[Trait\(""Lane"", ""Vulkan""\)\]\s*" + + @"\[Fact\]\s*public void CommittedProductionAtmosphericReceivers_KeepAuthoredLightAcrossShadowGate", + RegexOptions.Singleline), + source); + Assert.Equal(1, Count(source, "[Trait(\"Lane\", \"Vulkan\")]")); + } + + private static (Pixel Disabled, Pixel Enabled) RenderMeshLightingPair(HeadlessVulkanHost host) + { + VulkanGpuDevice device = host.Device; + using IGpuBuffer vertices = device.CreateBuffer(new GpuBufferDescription( + "s5-469-mesh-vertices", + 4 * Marshal.SizeOf(), + GpuBufferUsage.Vertex | GpuBufferUsage.TransferDestination, + GpuMemoryResidency.DeviceLocal)); + using IGpuBuffer indices = device.CreateBuffer(new GpuBufferDescription( + "s5-469-mesh-indices", + 6 * sizeof(ushort), + GpuBufferUsage.Index | GpuBufferUsage.TransferDestination, + GpuMemoryResidency.DeviceLocal)); + Vertex[] vertexData = + [ + new(new Vector3(-0.32f, -0.32f, 0f), Vector3.UnitZ, Vector2.Zero), + new(new Vector3( 0.32f, -0.32f, 0f), Vector3.UnitZ, Vector2.UnitX), + new(new Vector3( 0.32f, 0.32f, 0f), Vector3.UnitZ, Vector2.One), + new(new Vector3(-0.32f, 0.32f, 0f), Vector3.UnitZ, Vector2.UnitY), + ]; + vertices.Upload(0, MemoryMarshal.AsBytes(vertexData)); + indices.Upload(0, MemoryMarshal.AsBytes([0, 1, 2, 2, 3, 0])); + + using IGpuRenderTarget target = CreateTarget(device, "s5-469-mesh-offscreen"); + using IGpuPipeline pipeline = device.CreatePipeline(new GpuPipelineDescription + { + Name = "s5-469-mesh-atmospheric", + Shaders = new GpuShaderSet("mesh_atmospheric"), + VertexLayout = GpuVertexLayout.WorldMesh, + Topology = GpuPrimitiveTopology.TriangleList, + Blend = GpuBlendMode.None, + Depth = GpuDepthState.Disabled, + Cull = GpuCullMode.None, + UsesRenderPackShaderAbi = true, + SampleCount = 1, + }); + Assert.False(pipeline.Description.Shaders.HasEmbeddedSpirv); + Assert.Equal("mesh_atmospheric", pipeline.Description.Shaders.Name); + + using (IGpuFrame frame = device.BeginFrame()) + { + using IGpuPassEncoder encoder = BeginPass(frame, target, "s5-469-mesh-lighting"); + encoder.BindPipeline(pipeline); + encoder.SetPushConstants(GpuPushConstants.Default); + + BindStorage(frame, encoder, GpuBindingModel.StorageInstances, + [ + Matrix4x4.CreateTranslation(-0.48f, 0f, 0f), + Matrix4x4.CreateTranslation( 0.48f, 0f, 0f), + ]); + BindStorage(frame, encoder, GpuBindingModel.StorageBatches, + [new BatchData(device.DefaultTextureSlot.Index, 1f, 0u, 0u)]); + BindStorage(frame, encoder, GpuBindingModel.StorageClipSlots, [0u, 0u]); + BindStorage(frame, encoder, GpuBindingModel.StorageGlobalLights, [GlobalLight.Zero]); + BindStorage(frame, encoder, GpuBindingModel.StorageInstanceLightSets, + Enumerable.Repeat(-1, 16).ToArray()); + BindStorage(frame, encoder, GpuBindingModel.StorageInstanceIndoor, [0u, 0u]); + BindStorage(frame, encoder, GpuBindingModel.StorageInstanceAlpha, [1f, 1f]); + BindStorage(frame, encoder, GpuBindingModel.StorageInstanceSelectionLighting, + [new Vector2(0f, 1f), new Vector2(0f, 1f)]); + BindStorage(frame, encoder, GpuBindingModel.StorageInstanceDetailCategory, [0u, 0u]); + BindUniform(frame, encoder, GpuBindingModel.UniformSceneLighting, AuthoredHalfLight()); + BindUniform(frame, encoder, GpuBindingModel.UniformAtmosphericFrame, default(AtmosphericFrameUniforms)); + + encoder.BindVertexBuffer(0, vertices, 0); + encoder.BindIndexBuffer(indices, 0, GpuIndexType.UInt16); + BindUniform(frame, encoder, GpuBindingModel.UniformDirectionalShadow, + ShadowUniforms(device.DefaultTextureSlot, enabled: false, -Vector3.UnitY)); + encoder.DrawIndexed(6, 1, 0, 0, 0); + BindUniform(frame, encoder, GpuBindingModel.UniformDirectionalShadow, + ShadowUniforms(device.DefaultTextureSlot, enabled: true, Vector3.UnitX)); + encoder.DrawIndexed(6, 1, 0, 0, 1); + } + + device.WaitIdle(); + return ReadLightingPair(host, target); + } + + private static (Pixel Disabled, Pixel Enabled) RenderTerrainLightingPair(HeadlessVulkanHost host) + { + VulkanGpuDevice device = host.Device; + TerrainVertex[] terrain = + [ + .. TerrainQuad(-0.48f), + .. TerrainQuad(0.48f), + ]; + using IGpuBuffer vertices = device.CreateBuffer(new GpuBufferDescription( + "s5-469-terrain-vertices", + terrain.Length * Marshal.SizeOf(), + GpuBufferUsage.Vertex | GpuBufferUsage.TransferDestination, + GpuMemoryResidency.DeviceLocal)); + vertices.Upload(0, MemoryMarshal.AsBytes(terrain)); + + using IGpuRenderTarget target = CreateTarget(device, "s5-469-terrain-offscreen"); + using IGpuPipeline pipeline = device.CreatePipeline(new GpuPipelineDescription + { + Name = "s5-469-terrain-atmospheric", + Shaders = new GpuShaderSet("terrain_atmospheric"), + VertexLayout = TerrainModernRenderer.TerrainVertexLayout, + Topology = GpuPrimitiveTopology.TriangleList, + Blend = GpuBlendMode.None, + Depth = GpuDepthState.Disabled, + Cull = GpuCullMode.None, + UsesRenderPackShaderAbi = true, + SampleCount = 1, + }); + Assert.False(pipeline.Description.Shaders.HasEmbeddedSpirv); + Assert.Equal("terrain_atmospheric", pipeline.Description.Shaders.Name); + + using (IGpuFrame frame = device.BeginFrame()) + { + using IGpuPassEncoder encoder = BeginPass(frame, target, "s5-469-terrain-lighting"); + encoder.BindPipeline(pipeline); + GpuPushConstants constants = GpuPushConstants.Default; + constants.TextureIndexA = device.DefaultTextureSlot.Index; + constants.TextureIndexB = device.DefaultTextureSlot.Index; + encoder.SetPushConstants(constants); + BindUniform(frame, encoder, GpuBindingModel.UniformSceneLighting, AuthoredHalfLight()); + BindTerrainTiling(frame, encoder); + encoder.BindVertexBuffer(0, vertices, 0); + + BindUniform(frame, encoder, GpuBindingModel.UniformDirectionalShadow, + ShadowUniforms(device.DefaultTextureSlot, enabled: false, -Vector3.UnitY)); + encoder.Draw(6, 1, 0, 0); + BindUniform(frame, encoder, GpuBindingModel.UniformDirectionalShadow, + ShadowUniforms(device.DefaultTextureSlot, enabled: true, Vector3.UnitX)); + encoder.Draw(6, 1, 6, 0); + } + + device.WaitIdle(); + return ReadLightingPair(host, target); + } + + private static IGpuRenderTarget CreateTarget(VulkanGpuDevice device, string name) => + device.CreateRenderTarget(new GpuRenderTargetDescription( + name, + Extent, + Extent, + GpuTextureFormat.Rgba8UnormRenderTarget, + DepthFormat: null, + SampleCount: 1)); + + private static IGpuPassEncoder BeginPass(IGpuFrame frame, IGpuRenderTarget target, string name) => + frame.BeginPass(new GpuPassDescription + { + Name = name, + Color = new GpuColorAttachment( + target, + GpuLoadOp.Clear, + GpuStoreOp.Store, + new Vector4(0f, 0f, 0f, 1f)), + Depth = null, + SampleCount = 1, + }); + + private static SceneLightingUbo AuthoredHalfLight() => new() + { + Light0 = new UboLight + { + PosAndKind = new Vector4(0f, 0f, 0f, 0f), + DirAndRange = new Vector4(0f, 0f, -0.5f, 0f), + ColorAndIntensity = Vector4.One, + ConeAngleEtc = Vector4.Zero, + }, + CellAmbient = new Vector4(0f, 0f, 0f, 1f), + }; + + private static DirectionalShadowUniforms ShadowUniforms( + GpuTextureSlot texture, + bool enabled, + Vector3 celestialSurfaceToLight) => new( + Matrix4x4.Identity, + Matrix4x4.Identity, + Matrix4x4.Identity, + Matrix4x4.Identity, + Vector4.Zero, + new Vector4(0f, 1f, 0f, 1f), + Vector4.Zero, + new UInt4(texture.Index, 0u, 1u, enabled ? 1u : 0u), + new Vector4(celestialSurfaceToLight, 1f)); + + private static void BindTerrainTiling(IGpuFrame frame, IGpuPassEncoder encoder) + { + GpuRingAllocation allocation = frame.AllocateRing( + TerrainTextureTilingTable.UniformBufferBytes, + GpuRingUsage.Uniform); + allocation.Data.Clear(); + allocation.AsSpan()[0] = 1f; + encoder.BindUniformBuffer( + GpuBindingModel.UniformTerrainTiling, + allocation.Buffer, + allocation.OffsetBytes, + (uint)allocation.Data.Length); + } + + private static TerrainVertex[] TerrainQuad(float x) => + [ + new(new Vector3(x - 0.32f, -0.32f, 0.5f)), + new(new Vector3(x + 0.32f, -0.32f, 0.5f)), + new(new Vector3(x + 0.32f, 0.32f, 0.5f)), + new(new Vector3(x - 0.32f, -0.32f, 0.5f)), + new(new Vector3(x + 0.32f, 0.32f, 0.5f)), + new(new Vector3(x - 0.32f, 0.32f, 0.5f)), + ]; + + private static (Pixel Disabled, Pixel Enabled) ReadLightingPair( + HeadlessVulkanHost host, + IGpuRenderTarget target) + { + VulkanGpuRenderTarget vkTarget = Assert.IsType(target); + byte[] pixels = ReadBack( + host.Vk, + host.PhysicalDevice, + host.LogicalDevice, + host.Queue, + host.QueueFamily, + vkTarget.ColorResult.Image); + return (PixelAt(pixels, 16, 32), PixelAt(pixels, 48, 32)); + } + + private static Pixel PixelAt(byte[] pixels, int x, int y) + { + int offset = ((y * Extent) + x) * 4; + return new Pixel(pixels[offset], pixels[offset + 1], pixels[offset + 2], pixels[offset + 3]); + } + + private static void AssertAuthoredHalfIntensity(Pixel pixel, string receiver) + { + Assert.InRange(pixel.R, (byte)126, (byte)129); + Assert.InRange(pixel.G, (byte)126, (byte)129); + Assert.InRange(pixel.B, (byte)126, (byte)129); + Assert.True(pixel.A >= 253, $"{receiver} alpha was {pixel.A}, expected opaque."); + } + + private static int Count(string text, string token) + { + int count = 0; + for (int index = 0; (index = text.IndexOf(token, index, StringComparison.Ordinal)) >= 0;) + { + count++; + index += token.Length; + } + return count; + } + + [StructLayout(LayoutKind.Sequential, Pack = 4)] + private readonly record struct TerrainVertex( + Vector3 Position, + Vector3 Normal, + uint Packed0, + uint Packed1, + uint Packed2, + uint Packed3) + { + internal TerrainVertex(Vector3 position) + : this(position, Vector3.UnitZ, 0xFFFF_FF00u, uint.MaxValue, uint.MaxValue, 0u) + { + } + } + + private readonly record struct Pixel(byte R, byte G, byte B, byte A); +} diff --git a/tests/AcDream.App.Tests/Rendering/Gpu/Vk/MeshModernSharedIndexOffscreenTests.cs b/tests/AcDream.App.Tests/Rendering/Gpu/Vk/MeshModernSharedIndexOffscreenTests.cs index 3837cb6f..9d6f2761 100644 --- a/tests/AcDream.App.Tests/Rendering/Gpu/Vk/MeshModernSharedIndexOffscreenTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Gpu/Vk/MeshModernSharedIndexOffscreenTests.cs @@ -13,7 +13,7 @@ namespace AcDream.App.Tests.Rendering.Gpu.Vk; /// mesh_modern SPIR-V pair, binds a nonzero shared-transform prefix and local /// sidecars, submits an actual offscreen draw, and the test reads its pixels. /// -public sealed unsafe class MeshModernSharedIndexOffscreenTests +public sealed unsafe partial class MeshModernSharedIndexOffscreenTests { private const int Extent = 64; private const uint Prefix = 2; diff --git a/tests/AcDream.App.Tests/Rendering/Packs/AtmosphericPostProcessGraphTests.cs b/tests/AcDream.App.Tests/Rendering/Packs/AtmosphericPostProcessGraphTests.cs index 608d3c04..895e81e0 100644 --- a/tests/AcDream.App.Tests/Rendering/Packs/AtmosphericPostProcessGraphTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Packs/AtmosphericPostProcessGraphTests.cs @@ -1224,26 +1224,13 @@ public sealed class AtmosphericPostProcessGraphTests // ── Campaign VM VM6: foliage wind ──────────────────────────────────── [Fact] - public void ReceiverVertexShadersFallBackToThePlainLightDirectionWhenTheShadowFlagIsClear() + public void ReceiverVertexShadersAlwaysUseTheExactUnnormalizedPlainLightDirection() { - // Campaign VM VM6 review fix round 5 (F1 BLOCKER, test T3), hardened - // round 6 (N1): the original version of this test asserted only - // that the plain-pipeline direction SUBSTRING appeared somewhere - // in the receiver file — but the round-5 fix's own explanatory - // comments ALSO quote that exact substring in prose ("(-uLights[i] - // .dirAndRange.xyz, ... matched bit-for-bit)"), so the check was - // satisfied by the COMMENT even with the CODE mutated. Mutation- - // tested locally against this rewritten test: (1) swapping the - // ternary's true/false operands — PASSED the old substring-only - // test, FAILS this one; (2) deleting the fallback entirely - // (always taking the shadow-block direction, i.e. collapsing the - // ternary to just its false arm) — PASSED the old test, FAILS - // this one. Comment lines are stripped before matching, and the - // assertion is the exact branch SHAPE — the gated-off condition, - // then the plain uLights expression on the TRUE arm, then - // uShadowLightDirectionAndSource on the FALSE arm — via an - // ordered regex, not a substring search that a comment can also - // satisfy. + // Campaign OVERHAUL S5 #469: the atmospheric pack may direct + // shadow projection at a selected celestial body, but both receiver + // vertex shaders keep retail's one authored, unnormalized base-light + // direction in every gate state. Strip comments so prose cannot + // satisfy any of these production-expression checks. string shaderRoot = Path.Combine( RepositoryRoot(), "src", @@ -1259,9 +1246,12 @@ public sealed class AtmosphericPostProcessGraphTests string terrainModern = StripLineComments( 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); + Assert.DoesNotContain("shadowGatedOff", meshAtmospheric, StringComparison.Ordinal); + Assert.DoesNotContain("shadowGatedOff", terrainAtmospheric, StringComparison.Ordinal); + Assert.DoesNotContain("uShadowTextureAndFlags", meshAtmospheric, StringComparison.Ordinal); + Assert.DoesNotContain("uShadowTextureAndFlags", terrainAtmospheric, StringComparison.Ordinal); + Assert.DoesNotContain("uShadowLightDirectionAndSource", meshAtmospheric, StringComparison.Ordinal); + Assert.DoesNotContain("uShadowLightDirectionAndSource", terrainAtmospheric, StringComparison.Ordinal); // mesh: both files negate the per-light uniform inline (no // intermediate variable), so the literal expression matches @@ -1271,10 +1261,9 @@ public sealed class AtmosphericPostProcessGraphTests 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. + // terrain_modern.vert reads the same uniform into `sunDir` and then + // negates it; the atmospheric receiver spells that identical value + // inline so its split directional varying remains unchanged. const string terrainLightUniform = "uLights[0].dirAndRange.xyz"; Assert.Contains(terrainLightUniform, terrainModern, StringComparison.Ordinal); Assert.Contains( @@ -1282,24 +1271,19 @@ public sealed class AtmosphericPostProcessGraphTests terrainAtmospheric, StringComparison.Ordinal); - // Branch-shape guard (N1): fails on a swapped ternary or a - // deleted fallback even though the substrings above would still - // be present somewhere in the (comment-stripped) file. - var meshBranchShape = new Regex( - @"shadowGatedOff\s*\?\s*-uLights\[i\]\.dirAndRange\.xyz\s*:\s*normalize\(uShadowLightDirectionAndSource\.xyz\)", + var meshDirectionShape = new Regex( + @"vec3\s+Ldir\s*=\s*-uLights\[i\]\.dirAndRange\.xyz\s*;\s*float\s+ndl\s*=\s*max\(0\.0,\s*dot\(N,\s*Ldir\)\)", RegexOptions.Singleline); Assert.True( - meshBranchShape.IsMatch(meshAtmospheric), - "mesh_atmospheric.vert must branch exactly: " - + "shadowGatedOff ? -uLights[i].dirAndRange.xyz : normalize(uShadowLightDirectionAndSource.xyz)"); + meshDirectionShape.IsMatch(meshAtmospheric), + "mesh_atmospheric.vert must feed the exact unnormalized -uLights[i] direction directly to N dot L."); - var terrainBranchShape = new Regex( - @"shadowGatedOff\s*\?\s*-uLights\[0\]\.dirAndRange\.xyz\s*:\s*normalize\(uShadowLightDirectionAndSource\.xyz\)", + var terrainDirectionShape = new Regex( + @"vec3\s+surfaceToLight\s*=\s*-uLights\[0\]\.dirAndRange\.xyz\s*;\s*vec3\s+sunCol[\s\S]*?float\s+L\s*=\s*max\(dot\(vWorldNormal,\s*surfaceToLight\),\s*MIN_FACTOR\)", RegexOptions.Singleline); Assert.True( - terrainBranchShape.IsMatch(terrainAtmospheric), - "terrain_atmospheric.vert must branch exactly: " - + "shadowGatedOff ? -uLights[0].dirAndRange.xyz : normalize(uShadowLightDirectionAndSource.xyz)"); + terrainDirectionShape.IsMatch(terrainAtmospheric), + "terrain_atmospheric.vert must feed the exact unnormalized -uLights[0] direction directly to N dot L."); } /// @@ -1495,6 +1479,12 @@ public sealed class AtmosphericPostProcessGraphTests StringComparison.Ordinal); Assert.Contains("pack-off does not build, select, upload, or draw shadow work", ia24, StringComparison.Ordinal); + Assert.Contains("authored, unnormalized `uLights` direction remains the sole base-light direction", ia24, + StringComparison.Ordinal); + Assert.Contains("selected celestial source drives only the opt-in shadow and volumetric projection", ia24, + StringComparison.Ordinal); + Assert.Contains("light/shadow-angle separation", ia24, StringComparison.Ordinal); + Assert.DoesNotContain("same celestial source instead", ia24, StringComparison.Ordinal); static void AssertGraphUsesTypedSelection(string source, string methodName) {