feat(render): shader ABI v2 - AtmosphericFrame gains clock/wind blocks; caster pass binds it (Campaign VM VM6a)

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 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-23 00:30:02 +02:00
parent a0157693ec
commit 0930c35d1d
17 changed files with 340 additions and 32 deletions

View file

@ -9,6 +9,25 @@ using DatReaderWriter.Enums;
namespace AcDream.App.Rendering;
/// <summary>
/// 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.
/// </summary>
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);