From 0930c35d1db79c171c65475f2336001eb4fd3f71 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 23 Aug 2026 00:30:02 +0200 Subject: [PATCH] feat(render): shader ABI v2 - AtmosphericFrame gains clock/wind blocks; caster pass binds it (Campaign VM VM6a) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AtmosphericFrame (set 3/binding 5) grows additively from 160 to 192 bytes: two appended vec4 members, uAtmosphereClockWind and uAtmosphereWindAmplitude, carry the foliage-wind clock/weather and amplitude inputs VM6b's shader displacement will read. RenderPackShaderAbi renames the old constant to AtmosphericFrameSizeBytesV1 (160), adds AtmosphericFrameSizeBytesV2 (192), keeps AtmosphericFrameSizeBytes pointing at the current (v2) size, and adds ShaderAbiVersion = 2. RenderPackSpirvValidator.ValidateAtmosphericFrame accepts either the v1 (seven-member, 160-byte) or v2 (nine-member, 192-byte) shape and rejects anything else naming both — this is why the frozen external sample packs under samples/*/Shaders/*.spv, whose GLSL sources are not in this tree, need no rebuild: a v1 shader bound to the 192-byte buffer still reads correctly, since a bound range only needs to be >= the block's own declared size. DirectionalSunShadowRenderer's caster pass now binds AtmosphericFrame too (both the multiview and per-cascade sites), through a new AtmosphericFrameBufferBinding the graph owns and supplies via DirectionalSunShadowRenderInput. AtmosphericPostProcessGraph.RenderDirectionalShadows builds its own 192-byte ring allocation for this, separate from the world receiver's frame block, because the caster pass runs before RenderPostProcess constructs that block within the same frame. The four world caster pipeline variants (opaque/cutout, base/multiview) are now allowed to declare binding 5 in the validator; terrain casters are untouched. This commit is plumbing only: the two new members are always written but never read by any shader yet (zero placeholders), so pack-on and pack-off output are both pixel-identical to before. VM6b wires the real weather-driven values and the shader-side displacement. App hermetic filter: 5972/5974 (2 pre-existing failures unrelated to this change, confirmed against the unmodified baseline). Core.Tests hermetic: 4697/4697. RenderPackValidator.Tests: 30/30. VulkanShaderManifestTests (retail oracle set): 7/7, byte-identical. Co-Authored-By: Claude Sonnet 5 --- docs/render-packs/README.md | 9 +++ docs/render-packs/semantic-bindings-v1.md | 34 +++++++- .../Rendering/DirectionalSunShadowRenderer.cs | 53 +++++++++++- .../Rendering/Packs/AtmosphericFrameInputs.cs | 20 ++++- .../Packs/AtmosphericPostProcessGraph.cs | 50 +++++++++++- .../DeclaredFullscreenRenderPackGraph.cs | 9 ++- .../Packs/VolumetricShaftRenderer.cs | 6 +- .../Rendering/Shaders/atmospheric_common.glsl | 26 +++++- .../spv/atmospheric_sun_occlusion.frag.spv | Bin 1744 -> 1792 bytes .../Shaders/spv/atmospheric_sun_rays.frag.spv | Bin 5296 -> 5344 bytes .../spv/atmospheric_volumetric.frag.spv | Bin 6528 -> 6576 bytes .../Shaders/spv/shaders.manifest.json | 12 +-- .../Rendering/RenderPackShaderAbi.cs | 20 ++++- .../Rendering/RenderPackSpirvValidator.cs | 34 ++++++-- .../Packs/AtmosphericShaderAbiTests.cs | 12 ++- .../Packs/RenderPackSpirvValidatorTests.cs | 76 ++++++++++++++++++ .../RenderPackValidatorCommandTests.cs | 11 ++- 17 files changed, 340 insertions(+), 32 deletions(-) diff --git a/docs/render-packs/README.md b/docs/render-packs/README.md index 5e9e4263..498dd36f 100644 --- a/docs/render-packs/README.md +++ b/docs/render-packs/README.md @@ -209,6 +209,15 @@ optional after a pass unconditionally declares them. these graphics declarations. Additive enum/record support stays compatible; a breaking contract requires a new render-pack API version and explicit compatibility path. +- `RenderPackShaderAbi.ShaderAbiVersion` separately versions the numeric + SPIR-V interface (set/binding numbers and std140 block layouts) declarations + are validated against — distinct from `RenderPackApi`/`PluginApi`. Campaign + VM VM6 shipped v2: `AtmosphericFrame` (set 3, binding 5) grew additively + from 160 to 192 bytes (see `docs/render-packs/semantic-bindings-v1.md`'s + "ABI v2 (additive)" section). `RenderPackSpirvValidator` accepts both the + v1 and v2 shapes, so shader assets compiled before a version bump — the + external sample packs among them — never need a rebuild for an additive + change. - Persisted identity is pack ID + pack version + preset ID, never list index. User-authored setting strings are keyed by the same stable pack identity and stable setting ID, never declaration or menu index. diff --git a/docs/render-packs/semantic-bindings-v1.md b/docs/render-packs/semantic-bindings-v1.md index 4889e805..efd8707a 100644 --- a/docs/render-packs/semantic-bindings-v1.md +++ b/docs/render-packs/semantic-bindings-v1.md @@ -87,7 +87,7 @@ checked-in shared render-pack GLSL includes are the byte-offset SSOT; authors include those definitions rather than maintaining a private copy. The tables below state the same values for review and tool diagnostics. -### `AtmosphericFrame` — set 3, binding 5, 160 bytes +### `AtmosphericFrame` — set 3, binding 5, 192 bytes (ABI v2; see below) ```glsl layout(std140, set = 3, binding = 5) uniform AtmosphericFrame { @@ -134,6 +134,38 @@ curve without an exact 0-degree point must make its first positive point zero. from scene depth and the normalized viewport coordinates. Matrix convention and depth range match the shared push-block `viewProjection`. +### ABI v2 (additive) — Campaign VM VM6 + +`AtmosphericFrame` grew from 160 to 192 bytes by appending two members after +`uAtmosphereInverseViewProjection`. Nothing above this line moved or changed +meaning: + +```glsl +layout(std140, set = 3, binding = 5) uniform AtmosphericFrame { + // ... the seven v1 members, unchanged ... + vec4 uAtmosphereClockWind; // @160: elapsed seconds, wind mean [0..1], wind gust [0..1], wind direction radians + vec4 uAtmosphereWindAmplitude; // @176: lean amplitude m, branch amplitude m, flutter amplitude m, max canopy height m +}; +``` + +`uAtmosphereClockWind`/`uAtmosphereWindAmplitude` feed the shared +`foliage_wind.glsl` include, which `mesh_atmospheric.vert` and the four +`directional_shadow_world_*` (opaque/cutout, base and multiview) caster +vertex shaders call identically so a displaced leaf's shadow moves with it. +No other pass reads these members. + +**Compatibility rule:** the host always allocates and binds the full 192-byte +v2 block (`RenderPackShaderAbi.AtmosphericFrameSizeBytes`), but a v1 shader — +one compiled before this campaign, declaring only the original seven members +— binds and reads correctly against it: a bound range only needs to be at +least as large as the block's declared size, so the shader simply never sees +the appended bytes. `RenderPackSpirvValidator.ValidateAtmosphericFrame` +accepts either the v1 shape (seven members, 160 bytes) or the v2 shape (nine +members, 192 bytes); any other member count is rejected naming both. This is +why the external sample packs under `samples/*/Shaders/*.spv` — whose GLSL +sources are not in this tree and are never recompiled — needed no rebuild for +this change. + ### Colour space The main-world colour target, `uAtmosphereSunColor` (sun-ray input), and any diff --git a/src/AcDream.App/Rendering/DirectionalSunShadowRenderer.cs b/src/AcDream.App/Rendering/DirectionalSunShadowRenderer.cs index 2d6966d7..accb930b 100644 --- a/src/AcDream.App/Rendering/DirectionalSunShadowRenderer.cs +++ b/src/AcDream.App/Rendering/DirectionalSunShadowRenderer.cs @@ -9,6 +9,25 @@ using DatReaderWriter.Enums; namespace AcDream.App.Rendering; +/// +/// Campaign VM VM6: the caster's own set 3/binding 5 AtmosphericFrame slice +/// (192 bytes, ABI v2 — see RenderPackShaderAbi.AtmosphericFrameSizeBytes), +/// owned and allocated by the graph exactly like the world receiver's frame +/// block, so the caster can read the identical foliage-wind clock/amplitude +/// inputs through the shared foliage_wind.glsl include. Unbound (the +/// default) when the caller has no atmospheric-frame data to offer — the +/// declared-pack graph does not build one, and the renderer simply skips +/// binding 5 for that caller rather than manufacturing a buffer nobody asked +/// for. +/// +internal readonly record struct AtmosphericFrameBufferBinding( + IGpuBuffer? Buffer, + uint OffsetBytes, + uint SizeBytes) +{ + internal bool IsBound => Buffer is not null; +} + internal readonly record struct DirectionalSunShadowRenderInput( DirectionalShadowEnvironmentInput Environment, Matrix4x4 CameraView, @@ -18,7 +37,8 @@ internal readonly record struct DirectionalSunShadowRenderInput( float CasterDepthPaddingMeters = 48f, float ResidentMaximumReachMeters = float.PositiveInfinity, bool MeasureGpuTimers = true, - bool MeasureCpuStages = false); + bool MeasureCpuStages = false, + AtmosphericFrameBufferBinding AtmosphericFrame = default); internal readonly record struct DirectionalSunShadowCpuStageTicks( long EnvironmentGateTicks, @@ -417,7 +437,8 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS 0L, 0L, 0L), - transformChurn); + transformChurn, + input.AtmosphericFrame); } catch { @@ -489,7 +510,8 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS bool measureGpuTimers = true, bool measureCpuStages = false, DirectionalSunShadowCpuStageTicks cpuStages = default, - DirectionalShadowTransformChurnDiagnostics transformChurn = default) + DirectionalShadowTransformChurnDiagnostics transformChurn = default, + AtmosphericFrameBufferBinding atmosphericFrame = default) { ObjectDisposedException.ThrowIf(_disposed, this); ArgumentNullException.ThrowIfNull(frame); @@ -566,6 +588,21 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS uniformAllocation.Buffer, uniformAllocation.OffsetBytes, DirectionalShadowUniforms.SizeInBytes); + // Campaign VM VM6: the caster pass reads foliage-wind clock/ + // amplitude inputs through the same shared AtmosphericFrame + // block the world receiver binds (set 3/binding 5), so a + // displaced leaf's shadow moves with it by construction. Bound + // only when the caller supplied one — declared (non-built-in) + // packs leave this unbound and their caster shaders, which never + // declare binding 5, are unaffected. + if (atmosphericFrame.IsBound) + { + encoder.BindUniformBuffer( + GpuBindingModel.UniformAtmosphericFrame, + atmosphericFrame.Buffer!, + atmosphericFrame.OffsetBytes, + atmosphericFrame.SizeBytes); + } DrawTerrain(encoder, uploads, terrainDraws, terrainGeometry, 0, _terrainMultiviewPipeline); DrawWorld(encoder, uploads, worldDraws, worldGeometry, 0, @@ -586,6 +623,16 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS uniformAllocation.Buffer, uniformAllocation.OffsetBytes, DirectionalShadowUniforms.SizeInBytes); + // Campaign VM VM6 — see the matching comment in the multiview + // branch above. + if (atmosphericFrame.IsBound) + { + encoder.BindUniformBuffer( + GpuBindingModel.UniformAtmosphericFrame, + atmosphericFrame.Buffer!, + atmosphericFrame.OffsetBytes, + atmosphericFrame.SizeBytes); + } DrawTerrain(encoder, uploads, terrainDraws, terrainGeometry, cascadeIndex); DrawWorld(encoder, uploads, worldDraws, worldGeometry, cascadeIndex); diff --git a/src/AcDream.App/Rendering/Packs/AtmosphericFrameInputs.cs b/src/AcDream.App/Rendering/Packs/AtmosphericFrameInputs.cs index e61ef0f7..abf8cfa0 100644 --- a/src/AcDream.App/Rendering/Packs/AtmosphericFrameInputs.cs +++ b/src/AcDream.App/Rendering/Packs/AtmosphericFrameInputs.cs @@ -129,13 +129,18 @@ internal sealed class AtmosphericFrameInputState : IAtmosphericWorldFrameSink } /// -/// Shader ABI SSOT for opt-in set 3 binding 5. Six std140 vec4 values followed by one -/// mat4, 160 bytes. +/// Shader ABI SSOT for opt-in set 3 binding 5. ABI v2 (Campaign VM VM6): +/// six std140 vec4 values, one mat4, then two more std140 vec4 values — +/// 192 bytes. The two appended members carry the foliage-wind clock/weather +/// and amplitude inputs foliage_wind.glsl reads; every earlier member +/// keeps its ABI v1 offset. See +/// and atmospheric_common.glsl for the byte-level contract both +/// backends and the SPIR-V validator agree on. /// [StructLayout(LayoutKind.Sequential, Pack = 4)] internal readonly struct AtmosphericFrameUniforms { - internal const int SizeInBytes = 160; + internal const int SizeInBytes = 192; internal AtmosphericFrameUniforms( Vector4 sunScreen, @@ -144,7 +149,9 @@ internal readonly struct AtmosphericFrameUniforms Vector4 weather, Vector4 sunDirection, Vector4 policy, - Matrix4x4 inverseViewProjection) + Matrix4x4 inverseViewProjection, + Vector4 clockWind, + Vector4 windAmplitude) { SunScreen = sunScreen; SunColor = sunColor; @@ -153,6 +160,8 @@ internal readonly struct AtmosphericFrameUniforms SunDirection = sunDirection; Policy = policy; InverseViewProjection = inverseViewProjection; + ClockWind = clockWind; + WindAmplitude = windAmplitude; } internal readonly Vector4 SunScreen; @@ -162,6 +171,9 @@ internal readonly struct AtmosphericFrameUniforms internal readonly Vector4 SunDirection; internal readonly Vector4 Policy; internal readonly Matrix4x4 InverseViewProjection; + // Campaign VM VM6 ABI v2 additions — offsets 160/176. + internal readonly Vector4 ClockWind; + internal readonly Vector4 WindAmplitude; } /// diff --git a/src/AcDream.App/Rendering/Packs/AtmosphericPostProcessGraph.cs b/src/AcDream.App/Rendering/Packs/AtmosphericPostProcessGraph.cs index 9c86b6af..22e1d59d 100644 --- a/src/AcDream.App/Rendering/Packs/AtmosphericPostProcessGraph.cs +++ b/src/AcDream.App/Rendering/Packs/AtmosphericPostProcessGraph.cs @@ -323,6 +323,8 @@ internal sealed class AtmosphericPostProcessGraph : * _shadowStrength, 0f, 1f)); + AtmosphericFrameBufferBinding shadowAtmosphericFrame = + BuildShadowAtmosphericFrameBinding(frame); var input = new DirectionalSunShadowRenderInput( environment, world.Camera.Camera.View, @@ -333,7 +335,8 @@ internal sealed class AtmosphericPostProcessGraph : MeasureGpuTimers: AtmosphericGpuTimerSampling.ShouldMeasure( Preset.Semantic, frame.Serial), - MeasureCpuStages: measureCpuStages); + MeasureCpuStages: measureCpuStages, + AtmosphericFrame: shadowAtmosphericFrame); long environmentFinished = measureCpuStages ? Stopwatch.GetTimestamp() : 0L; _lastShadowCasterCount = _shadowCasters.Stats.Accepted; _lastShadowClassificationCalls = _shadowCasters.Stats.TopologyRebuilt ? 1 : 0; @@ -368,6 +371,43 @@ internal sealed class AtmosphericPostProcessGraph : return _lastShadowDiagnostics; } + /// + /// Campaign VM VM6: the caster pass runs before + /// builds the receiver's frame block (shadows render during the prepared- + /// world phase; post-process runs after), so it cannot share that + /// allocation. This graph owns a second, independent 192-byte ABI v2 + /// ring slice for the caster instead — see the D2 binding sites in + /// . Only the + /// two appended VM6 members carry real content; the caster shaders never + /// read the other seven (sun/weather/policy/inverse-view-projection are + /// receiver-only concerns), so this method leaves them zeroed rather + /// than duplicating 's computation. + /// + private AtmosphericFrameBufferBinding BuildShadowAtmosphericFrameBinding( + IGpuFrame frame) + { + GpuRingAllocation allocation = frame.AllocateRing( + AtmosphericFrameUniforms.SizeInBytes, + GpuRingUsage.Uniform); + var uniforms = new AtmosphericFrameUniforms( + Vector4.Zero, + Vector4.Zero, + Vector4.Zero, + Vector4.Zero, + Vector4.Zero, + Vector4.Zero, + Matrix4x4.Identity, + // VM6a: ABI v2 plumbing only. VM6b fills the real weather-driven + // wind block here; no caster shader reads it yet. + Vector4.Zero, + Vector4.Zero); + MemoryMarshal.Write(allocation.Data, in uniforms); + return new AtmosphericFrameBufferBinding( + allocation.Buffer, + allocation.OffsetBytes, + (uint)AtmosphericFrameUniforms.SizeInBytes); + } + public IGpuRenderTarget PrepareWorldTarget( int width, int height, @@ -485,7 +525,13 @@ internal sealed class AtmosphericPostProcessGraph : dayGroupPolicy, shadowElevationPolicy, volumetricElevationPolicy), - inputs.InverseViewProjection); + inputs.InverseViewProjection, + // Campaign VM VM6a: ABI v2 plumbing only — written here so the + // 192-byte block is always fully populated, but mesh_atmospheric.vert + // does not read these two members yet (VM6b wires the real + // weather-driven values and the shader read together). + Vector4.Zero, + Vector4.Zero); GpuRingAllocation frameBlock; GpuRingAllocation settingsBlock; GpuRingAllocation fusedSunPassBlock = default; diff --git a/src/AcDream.App/Rendering/Packs/DeclaredFullscreenRenderPackGraph.cs b/src/AcDream.App/Rendering/Packs/DeclaredFullscreenRenderPackGraph.cs index 0f005236..248cf90e 100644 --- a/src/AcDream.App/Rendering/Packs/DeclaredFullscreenRenderPackGraph.cs +++ b/src/AcDream.App/Rendering/Packs/DeclaredFullscreenRenderPackGraph.cs @@ -319,7 +319,14 @@ internal class DeclaredFullscreenRenderPackGraph : dayGroupPolicy, shadowElevationPolicy, volumetricElevationPolicy), - inputs.InverseViewProjection); + inputs.InverseViewProjection, + // Campaign VM VM6 ABI v2 additions. Foliage wind is a built-in- + // pack feature (BuiltInAtmosphericRenderPack.Settings / + // AtmosphericPostProcessGraph) — declared (third-party) packs + // write zero here and their shaders, compiled before VM6, never + // declare binding 5's appended members. + Vector4.Zero, + Vector4.Zero); GpuRingAllocation frameBlock = frame.AllocateRing(AtmosphericFrameUniforms.SizeInBytes, GpuRingUsage.Uniform); MemoryMarshal.Write(frameBlock.Data, in frameValues); GpuRingAllocation settingsBlock = frame.AllocateRing(PackSettingsUniforms.SizeInBytes, GpuRingUsage.Uniform); diff --git a/src/AcDream.App/Rendering/Packs/VolumetricShaftRenderer.cs b/src/AcDream.App/Rendering/Packs/VolumetricShaftRenderer.cs index da67b20d..d52f56a7 100644 --- a/src/AcDream.App/Rendering/Packs/VolumetricShaftRenderer.cs +++ b/src/AcDream.App/Rendering/Packs/VolumetricShaftRenderer.cs @@ -374,7 +374,11 @@ internal sealed class VolumetricShaftRenderer : IDisposable RenderPackAtmospherePolicyEvaluation.VolumetricShaft( _atmospherePolicy.VolumetricShaftSunElevationResponse, inputs.SunElevationDegrees)), - inputs.InverseViewProjection); + inputs.InverseViewProjection, + // Campaign VM VM6 ABI v2 additions — the volumetric-shaft pass does + // not read foliage wind. + Vector4.Zero, + Vector4.Zero); private VolumetricShaftDiagnostics Disabled(VolumetricShaftGateReason reason) => new( reason, diff --git a/src/AcDream.App/Rendering/Shaders/atmospheric_common.glsl b/src/AcDream.App/Rendering/Shaders/atmospheric_common.glsl index 77d70cc5..3bd00f5d 100644 --- a/src/AcDream.App/Rendering/Shaders/atmospheric_common.glsl +++ b/src/AcDream.App/Rendering/Shaders/atmospheric_common.glsl @@ -1,10 +1,23 @@ #ifndef ACDREAM_ATMOSPHERIC_COMMON_GLSL #define ACDREAM_ATMOSPHERIC_COMMON_GLSL -// Render-pack shader ABI v1. These declarations are the byte-level SSOT for -// AtmosphericFrameUniforms (set 3/binding 5, 160 bytes) and -// AtmosphericPackPassUniforms (set 3/binding 7). Binding 6 is intentionally -// reserved for directional-shadow data in directional_shadow_common.glsl. +// Render-pack shader ABI v2 (Campaign VM VM6, additive over v1). These +// declarations are the byte-level SSOT for AtmosphericFrameUniforms (set +// 3/binding 5) and AtmosphericPackPassUniforms (set 3/binding 7). Binding 6 +// is intentionally reserved for directional-shadow data in +// directional_shadow_common.glsl. +// +// v1 was seven members / 160 bytes (everything through +// uAtmosphereInverseViewProjection). v2 appends two vec4 members at offsets +// 160/176 for a total of 192 bytes — foliage-wind clock/weather and +// amplitude inputs. The append is additive-only: a v1 module bound to the +// (now 192-byte) buffer reads only its declared 160-byte prefix, which is +// valid Vulkan (the bound range only needs to be >= the block's declared +// size), so shader assets compiled before VM6 — including the frozen +// external sample packs under samples/*/Shaders/*.spv, whose GLSL sources +// are not in this tree and are never recompiled — remain valid without a +// rebuild. RenderPackSpirvValidator.ValidateAtmosphericFrame accepts EITHER +// shape. layout(std140, ACDREAM_PACK_UBO_SET binding = 5) uniform AtmosphericFrame { vec4 uAtmosphereSunScreen; // 0: uv.xy, ray strength, elevation degrees vec4 uAtmosphereSunColor; // 16: authored display-space rgb (retail has no linear pipeline), policy multiplier @@ -13,6 +26,11 @@ layout(std140, ACDREAM_PACK_UBO_SET binding = 5) uniform AtmosphericFrame { vec4 uAtmosphereSunDirection; // 64: surface-to-sun xyz, authored brightness vec4 uAtmospherePolicy; // 80: day group, group factor, shadow/shaft elevation factors mat4 uAtmosphereInverseViewProjection; // 96: screen/depth to world + // Campaign VM VM6 (2026-08-22): ABI v2 additions. Read by + // foliage_wind.glsl only — every pre-VM6 pass leaves these declared but + // unread. + vec4 uAtmosphereClockWind; // 160: elapsed seconds, wind mean [0..1], wind gust [0..1], wind direction radians + vec4 uAtmosphereWindAmplitude; // 176: lean amplitude m, branch amplitude m, flutter amplitude m, max canopy height m }; layout(std140, ACDREAM_PACK_UBO_SET binding = 7) uniform PackPass { diff --git a/src/AcDream.App/Rendering/Shaders/spv/atmospheric_sun_occlusion.frag.spv b/src/AcDream.App/Rendering/Shaders/spv/atmospheric_sun_occlusion.frag.spv index a5a5047364f122520cafc936aacfdabc340f66b4..23556c7d7b5d1056259dede60a9ee9216fce274a 100644 GIT binary patch delta 77 zcmcb>+rYP>fl1Yaft5j@fq{V?h?RkO0T9E)IG|!1HrF!UWn|`N(4U;hB0jl*g@uzJ ND8&cDn=4tunE=Fe3xogw delta 23 fcmZqRyTH4lfoby!rdy1RoRgbb6gRJ731I>NX2J*5 diff --git a/src/AcDream.App/Rendering/Shaders/spv/atmospheric_sun_rays.frag.spv b/src/AcDream.App/Rendering/Shaders/spv/atmospheric_sun_rays.frag.spv index 1dcc705f51684bc5483185d771f12c2be7664a14..50a3e79d11fab8621209fab3ac1a52e285b41ebc 100644 GIT binary patch delta 67 zcmdm>`9O0+0F$Z*11p0s0|Ns)5Gw=m0w9KoaX`g3Z1!bxWM<@^+|R1WY5){8*u0jN GnI8aZ{0dnB delta 32 mcmaE$xj}P70Mq6QCOc+kP6pq}2U&$DUtnbcVvfZRSos08c?ydF diff --git a/src/AcDream.App/Rendering/Shaders/spv/atmospheric_volumetric.frag.spv b/src/AcDream.App/Rendering/Shaders/spv/atmospheric_volumetric.frag.spv index 5ac257b8bdeaa3791b510814ce252948b0c0ae11..80558d060416b6bf5f2a2ae61a72a575f8429f68 100644 GIT binary patch delta 77 zcmZoL-e9~Tgi+Olft5jvfq{V?h?RkO0T9E)IG|!1HU}~;WoG7P(3;H1Dm+<$l?8}7 OSVe)-qMJoovqb=|h6=m@ delta 23 ecmdmB++e&RgmH5X<0588&dKtuikm%HGeiJgu?Eio diff --git a/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json b/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json index aa31c7be..85f22c71 100644 --- a/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json +++ b/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json @@ -12,7 +12,7 @@ }, { "stage": "frag", - "sourceSha256": "de77256a5edde4f50c0d50544cb7e15e7d6b618636f2c529750b20e7a779cf65", + "sourceSha256": "ece56dc1c369316d7623f04795d93938fcc54756734bc49db01311fa96ec2dbf", "compiled": true } ] @@ -28,7 +28,7 @@ }, { "stage": "frag", - "sourceSha256": "f8f21269fdf4d994844a747842d68bbda5afcddb5dccccdcec11563450edba52", + "sourceSha256": "b13cd8712c6532fbe9c4661794ad9f424a27437b5f2fa14c0b76d38cf7e93523", "compiled": true } ] @@ -44,7 +44,7 @@ }, { "stage": "frag", - "sourceSha256": "dfc11d59f71d65efe650bcfa3434777cbcfe3358f5be38bf392f51b2bd2561b3", + "sourceSha256": "d28b82fc7ee8ca18d6911683ce904d59ecee749e9fd4bcc5af58ac048911a0b4", "compiled": true } ] @@ -60,7 +60,7 @@ }, { "stage": "frag", - "sourceSha256": "45d02b356ada9fb15052290188d0de85e666de94f7bec7dc34c4e405b31401ef", + "sourceSha256": "695e692cd5442be15d63a0b4f155abeaecf54674adae5f7cf25ef0bb892d18ee", "compiled": true } ] @@ -76,7 +76,7 @@ }, { "stage": "frag", - "sourceSha256": "9765a397328fe9a002a825ae806fcd1651645458e2909dc9ff96a1759c4690d9", + "sourceSha256": "6ae2a078177b516c8ee138467559e9d4bf8838ea53941709176ce4f6fa3803a0", "compiled": true } ] @@ -92,7 +92,7 @@ }, { "stage": "frag", - "sourceSha256": "93aeef7ce7555c9c184ee8d7e419c207b2fbed4d877788f016faf19832816b0d", + "sourceSha256": "739b397a270142a77c08e7edefde7b2f9992588452a9827b737a0512bbeab513", "compiled": true } ] diff --git a/src/AcDream.Plugin.Abstractions/Rendering/RenderPackShaderAbi.cs b/src/AcDream.Plugin.Abstractions/Rendering/RenderPackShaderAbi.cs index 71221a30..35683b88 100644 --- a/src/AcDream.Plugin.Abstractions/Rendering/RenderPackShaderAbi.cs +++ b/src/AcDream.Plugin.Abstractions/Rendering/RenderPackShaderAbi.cs @@ -7,9 +7,27 @@ namespace AcDream.Plugin.Abstractions.Rendering; /// public static class RenderPackShaderAbi { + /// + /// Campaign VM VM6: the render-pack shader ABI version this host writes + /// and binds. v2 is additive over v1 — see . + /// + public const int ShaderAbiVersion = 2; + public const int UniformDescriptorSet = 3; public const int AtmosphericFrameBinding = 5; - public const int AtmosphericFrameSizeBytes = 160; + + /// ABI v1 AtmosphericFrame size: seven vec4/mat4 members, 160 bytes. + public const int AtmosphericFrameSizeBytesV1 = 160; + + /// + /// ABI v2 AtmosphericFrame size: v1's 160 bytes plus two appended vec4 + /// members (foliage-wind clock/weather and amplitude), 192 bytes. + /// + public const int AtmosphericFrameSizeBytesV2 = 192; + + /// What the host allocates and binds. Always the current ABI version. + public const int AtmosphericFrameSizeBytes = AtmosphericFrameSizeBytesV2; + public const int DirectionalShadowBinding = 6; public const int DirectionalShadowSizeBytes = 336; public const int PackPassBinding = 7; diff --git a/src/AcDream.Plugin.Abstractions/Rendering/RenderPackSpirvValidator.cs b/src/AcDream.Plugin.Abstractions/Rendering/RenderPackSpirvValidator.cs index 0d371633..928af0ac 100644 --- a/src/AcDream.Plugin.Abstractions/Rendering/RenderPackSpirvValidator.cs +++ b/src/AcDream.Plugin.Abstractions/Rendering/RenderPackSpirvValidator.cs @@ -67,14 +67,20 @@ public static class RenderPackSpirvValidator new([], [], false, false, true, false, false), RenderPipelineVariantSemantic.TerrainMultiviewDirectionalShadowCaster => new([], [], false, false, true, false, false), + // Campaign VM VM6: these four world caster variants now bind + // AtmosphericFrame (set 3/binding 5) for foliage_wind.glsl's + // clock/wind inputs — the shadow must move with the leaf, and the + // shared include is what keeps caster and receiver from drifting + // apart. Terrain casters are untouched and keep AllowAtmosphericFrame + // false. RenderPipelineVariantSemantic.WorldOpaqueDirectionalShadowCaster => - new([0u], [], false, false, true, false, false), + new([0u], [], false, true, true, false, false), RenderPipelineVariantSemantic.WorldOpaqueMultiviewDirectionalShadowCaster => - new([0u], [], false, false, true, false, false), + new([0u], [], false, true, true, false, false), RenderPipelineVariantSemantic.WorldAlphaCutoutDirectionalShadowCaster => - new([0u, 1u], [], true, false, true, false, false), + new([0u, 1u], [], true, true, true, false, false), RenderPipelineVariantSemantic.WorldAlphaCutoutMultiviewDirectionalShadowCaster => - new([0u, 1u], [], true, false, true, false, false), + new([0u, 1u], [], true, true, true, false, false), RenderPipelineVariantSemantic.TerrainDirectionalShadowReceiver => new([], [1u, 2u, 3u], true, false, true, false, true), RenderPipelineVariantSemantic.WorldDirectionalShadowReceiver => @@ -247,8 +253,17 @@ public static class RenderPackSpirvValidator private static string? ValidateAtmosphericFrame(Module module, uint id, uint[] members) { - if (members.Length != 7) - return "AtmosphericFrame must contain exactly seven members and occupy 160 bytes"; + // Campaign VM VM6: ABI v2 is additive over v1 — a module declares + // EITHER the seven-member/160-byte v1 shape (frozen external sample + // packs, and every pass this campaign did not touch) OR the + // nine-member/192-byte v2 shape (the world receiver + directional- + // shadow caster passes VM6 gave foliage-wind inputs). Any other + // member count is rejected naming both accepted layouts. + if (members.Length != 7 && members.Length != 9) + { + return "AtmosphericFrame must match ABI v1 (seven members, 160 bytes) or " + + "ABI v2 (nine members, 192 bytes)"; + } for (int i = 0; i < 6; i++) { if (!module.IsFloatVector(members[i], 4) || module.MemberOffset(id, i) != (uint)(i * 16)) @@ -259,6 +274,13 @@ public static class RenderPackSpirvValidator || module.MemberDecoration(id, 6, Decoration.ColMajor) is null || module.MemberDecoration(id, 6, Decoration.MatrixStride) != 16) return "AtmosphericFrame inverse-view-projection layout does not match ABI v1"; + if (members.Length == 7) + return null; + + if (!module.IsFloatVector(members[7], 4) || module.MemberOffset(id, 7) != 160) + return "AtmosphericFrame clock/wind member does not match ABI v2"; + if (!module.IsFloatVector(members[8], 4) || module.MemberOffset(id, 8) != 176) + return "AtmosphericFrame wind-amplitude member does not match ABI v2"; return null; } diff --git a/tests/AcDream.App.Tests/Rendering/Packs/AtmosphericShaderAbiTests.cs b/tests/AcDream.App.Tests/Rendering/Packs/AtmosphericShaderAbiTests.cs index 09c3a384..3338ddce 100644 --- a/tests/AcDream.App.Tests/Rendering/Packs/AtmosphericShaderAbiTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Packs/AtmosphericShaderAbiTests.cs @@ -11,7 +11,7 @@ public sealed class AtmosphericShaderAbiTests [Fact] public void HostStructsMatchTheCheckedInStd140AtmosphericAbi() { - Assert.Equal(160, Marshal.SizeOf()); + Assert.Equal(192, Marshal.SizeOf()); Assert.Equal(0, Offset(nameof(AtmosphericFrameUniforms.SunScreen))); Assert.Equal(16, Offset(nameof(AtmosphericFrameUniforms.SunColor))); Assert.Equal(32, Offset(nameof(AtmosphericFrameUniforms.Viewport))); @@ -19,6 +19,14 @@ public sealed class AtmosphericShaderAbiTests Assert.Equal(64, Offset(nameof(AtmosphericFrameUniforms.SunDirection))); Assert.Equal(80, Offset(nameof(AtmosphericFrameUniforms.Policy))); Assert.Equal(96, Offset(nameof(AtmosphericFrameUniforms.InverseViewProjection))); + // Campaign VM VM6: ABI v2 appends the foliage-wind clock/weather and + // amplitude members at 160/176, growing the block from v1's 160 + // bytes to 192. + Assert.Equal(160, Offset(nameof(AtmosphericFrameUniforms.ClockWind))); + Assert.Equal(176, Offset(nameof(AtmosphericFrameUniforms.WindAmplitude))); + Assert.Equal(160, RenderPackShaderAbi.AtmosphericFrameSizeBytesV1); + Assert.Equal(192, RenderPackShaderAbi.AtmosphericFrameSizeBytesV2); + Assert.Equal(2, RenderPackShaderAbi.ShaderAbiVersion); Assert.Equal(64, Marshal.SizeOf()); Assert.Equal(0, Offset(nameof(AtmosphericPackPassUniforms.Params0))); @@ -71,6 +79,8 @@ public sealed class AtmosphericShaderAbiTests "uAtmosphereSunDirection", "uAtmospherePolicy", "uAtmosphereInverseViewProjection", + "uAtmosphereClockWind", + "uAtmosphereWindAmplitude", "binding = 7", "uPackParams0", "uPackParams1", diff --git a/tests/AcDream.App.Tests/Rendering/Packs/RenderPackSpirvValidatorTests.cs b/tests/AcDream.App.Tests/Rendering/Packs/RenderPackSpirvValidatorTests.cs index a62f1647..83c21c43 100644 --- a/tests/AcDream.App.Tests/Rendering/Packs/RenderPackSpirvValidatorTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Packs/RenderPackSpirvValidatorTests.cs @@ -195,6 +195,82 @@ public sealed class RenderPackSpirvValidatorTests Assert.Contains("AtmosphericFrame", result.Reason, StringComparison.Ordinal); } + // Campaign VM VM6: atmospheric_common.glsl's AtmosphericFrame block grew + // from ABI v1 (seven members, 160 bytes) to ABI v2 (nine members, 192 + // bytes) — additive only. Every compiled shader under this tree that + // includes the header (atmospheric_sun_occlusion.frag among them) is now + // v2; the frozen external sample packs under samples/*/Shaders/*.spv + // have no GLSL source in this tree and are never recompiled, so they + // stay v1 forever. The validator must accept both shapes and reject + // anything else, naming both. + + [Fact] + public void AtmosphericFrameAbiV2ModuleIsValid() + { + byte[] spirv = Shader("atmospheric_sun_occlusion.frag.spv"); + + RenderPackSpirvValidationResult result = RenderPackSpirvValidator.ValidatePassShader( + spirv, + RenderPackShaderStage.Fragment, + Pass(inputs: + [ + RenderSemanticInput.SceneDepth, + RenderSemanticInput.SunScreenPosition, + RenderSemanticInput.Weather, + ])); + + Assert.True(result.Success, result.Reason); + } + + [Fact] + public void AtmosphericFrameAbiV1ModuleIsStillValid() + { + byte[] v2 = Shader("atmospheric_sun_occlusion.frag.spv"); + byte[] v1 = RemoveLastBlockMember( + RemoveLastBlockMember( + v2, + RenderPackShaderAbi.UniformDescriptorSet, + RenderPackShaderAbi.AtmosphericFrameBinding), + RenderPackShaderAbi.UniformDescriptorSet, + RenderPackShaderAbi.AtmosphericFrameBinding); + + RenderPackSpirvValidationResult result = RenderPackSpirvValidator.ValidatePassShader( + v1, + RenderPackShaderStage.Fragment, + Pass(inputs: + [ + RenderSemanticInput.SceneDepth, + RenderSemanticInput.SunScreenPosition, + RenderSemanticInput.Weather, + ])); + + Assert.True(result.Success, result.Reason); + } + + [Fact] + public void AtmosphericFrameEightMemberModuleIsRejected() + { + byte[] v2 = Shader("atmospheric_sun_occlusion.frag.spv"); + byte[] eightMembers = RemoveLastBlockMember( + v2, + RenderPackShaderAbi.UniformDescriptorSet, + RenderPackShaderAbi.AtmosphericFrameBinding); + + RenderPackSpirvValidationResult result = RenderPackSpirvValidator.ValidatePassShader( + eightMembers, + RenderPackShaderStage.Fragment, + Pass(inputs: + [ + RenderSemanticInput.SceneDepth, + RenderSemanticInput.SunScreenPosition, + RenderSemanticInput.Weather, + ])); + + Assert.False(result.Success); + Assert.Contains("ABI v1", result.Reason, StringComparison.Ordinal); + Assert.Contains("ABI v2", result.Reason, StringComparison.Ordinal); + } + [Fact] public void DeclaredStageAndMainEntryPointAreEnforced() { diff --git a/tests/AcDream.RenderPackValidator.Tests/RenderPackValidatorCommandTests.cs b/tests/AcDream.RenderPackValidator.Tests/RenderPackValidatorCommandTests.cs index 0e8e813e..2052dc67 100644 --- a/tests/AcDream.RenderPackValidator.Tests/RenderPackValidatorCommandTests.cs +++ b/tests/AcDream.RenderPackValidator.Tests/RenderPackValidatorCommandTests.cs @@ -7,11 +7,18 @@ namespace AcDream.RenderPackValidator.Tests; public sealed class RenderPackValidatorCommandTests { [Fact] - public void PublicShaderAbiConstantsDescribeV1() + public void PublicShaderAbiConstantsDescribeTheCurrentAbi() { Assert.Equal(3, RenderPackShaderAbi.UniformDescriptorSet); Assert.Equal(5, RenderPackShaderAbi.AtmosphericFrameBinding); - Assert.Equal(160, RenderPackShaderAbi.AtmosphericFrameSizeBytes); + // Campaign VM VM6: the host always allocates/binds the current (v2) + // size; a v1-shaped shader — e.g. the frozen external sample packs + // this project validates — still reads correctly against it (a bound + // range only needs to be >= the block's own declared size). + Assert.Equal(160, RenderPackShaderAbi.AtmosphericFrameSizeBytesV1); + Assert.Equal(192, RenderPackShaderAbi.AtmosphericFrameSizeBytesV2); + Assert.Equal(192, RenderPackShaderAbi.AtmosphericFrameSizeBytes); + Assert.Equal(2, RenderPackShaderAbi.ShaderAbiVersion); Assert.Equal(6, RenderPackShaderAbi.DirectionalShadowBinding); Assert.Equal(336, RenderPackShaderAbi.DirectionalShadowSizeBytes); Assert.Equal(7, RenderPackShaderAbi.PackPassBinding);