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 a5a50473..23556c7d 100644 Binary files a/src/AcDream.App/Rendering/Shaders/spv/atmospheric_sun_occlusion.frag.spv and b/src/AcDream.App/Rendering/Shaders/spv/atmospheric_sun_occlusion.frag.spv differ 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 1dcc705f..50a3e79d 100644 Binary files a/src/AcDream.App/Rendering/Shaders/spv/atmospheric_sun_rays.frag.spv and b/src/AcDream.App/Rendering/Shaders/spv/atmospheric_sun_rays.frag.spv differ 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 5ac257b8..80558d06 100644 Binary files a/src/AcDream.App/Rendering/Shaders/spv/atmospheric_volumetric.frag.spv and b/src/AcDream.App/Rendering/Shaders/spv/atmospheric_volumetric.frag.spv differ 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);