acdream/src/AcDream.App/Rendering/Packs/AtmosphericFrameInputs.cs
Erik 0930c35d1d 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>
2026-08-23 00:30:02 +02:00

241 lines
8 KiB
C#

using System.Numerics;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
using AcDream.Core.World;
using AcDream.Plugin.Abstractions.Rendering;
namespace AcDream.App.Rendering.Packs;
/// <summary>
/// Immutable authored atmosphere and exact camera projection captured from the
/// normal world frame. This value owns no gameplay or renderer objects and is
/// valid after the wrapped world renderer returns.
/// </summary>
internal readonly record struct AtmosphericFrameInputs(
Vector2 SunScreenUv,
bool SunIsOnScreen,
float SunElevationDegrees,
Vector3 SunColor,
Vector3 SunDirection,
float SunDirectionalBrightness,
Matrix4x4 InverseViewProjection,
int ActiveDayGroup,
WeatherKind Weather,
float WeatherIntensity,
double DeltaSeconds,
int ViewportWidth,
int ViewportHeight,
bool IsOutdoor);
internal interface IAtmosphericWorldFrameSink
{
void Publish(
in RenderFrameFoundation foundation,
in WorldRenderFrame world,
int activeDayGroup);
}
/// <summary>
/// One-frame handoff between <see cref="WorldSceneRenderer"/>, which owns the
/// canonical camera build, and the post graph. Reset happens before the world
/// pass so an intentionally skipped world can never reuse a prior camera.
/// </summary>
internal sealed class AtmosphericFrameInputState : IAtmosphericWorldFrameSink
{
private RenderFrameInput _host;
private RenderFrameFoundation _foundation;
private AtmosphericFrameInputs _current;
private bool _published;
internal void BeginFrame(
in RenderFrameInput host,
in RenderFrameFoundation foundation)
{
_host = host;
_foundation = foundation;
_current = default;
_published = false;
}
public void Publish(
in RenderFrameFoundation foundation,
in WorldRenderFrame world,
int activeDayGroup)
{
Vector3 direction = SkyStateProvider.SunDirectionFromKeyframe(foundation.Sky);
Vector3 sunPoint = world.Camera.Position + (direction * 10_000f);
Vector4 clip = Vector4.Transform(
new Vector4(sunPoint, 1f),
world.Camera.ViewProjection);
bool finite = float.IsFinite(clip.X)
&& float.IsFinite(clip.Y)
&& float.IsFinite(clip.W)
&& clip.W > 1e-5f;
Vector2 uv = finite
? new Vector2(
(clip.X / clip.W * 0.5f) + 0.5f,
0.5f - (clip.Y / clip.W * 0.5f))
: new Vector2(-1f, -1f);
bool onScreen = finite
&& uv.X >= 0f && uv.X <= 1f
&& uv.Y >= 0f && uv.Y <= 1f;
Matrix4x4 inverseViewProjection = Matrix4x4.Invert(
world.Camera.ViewProjection,
out Matrix4x4 inverse)
? inverse
: Matrix4x4.Identity;
_current = new AtmosphericFrameInputs(
uv,
onScreen,
foundation.Sky.SunPitchDeg,
foundation.Sky.SunColor,
direction,
foundation.Sky.DirBright,
inverseViewProjection,
activeDayGroup,
foundation.Atmosphere.Kind,
Math.Clamp(foundation.Atmosphere.Intensity, 0f, 1f),
_host.DeltaSeconds,
_host.ViewportWidth,
_host.ViewportHeight,
IsOutdoor: world.Roots.RenderSky && !world.Roots.CameraInsideCell);
_published = true;
}
internal AtmosphericFrameInputs Snapshot()
{
if (_published)
return _current;
// A portal/login frame deliberately skipped the normal world. Preserve
// its authored colour inputs but suppress every directional effect.
return new AtmosphericFrameInputs(
new Vector2(-1f, -1f),
SunIsOnScreen: false,
_foundation.Sky.SunPitchDeg,
_foundation.Sky.SunColor,
SkyStateProvider.SunDirectionFromKeyframe(_foundation.Sky),
_foundation.Sky.DirBright,
Matrix4x4.Identity,
-1,
_foundation.Atmosphere.Kind,
Math.Clamp(_foundation.Atmosphere.Intensity, 0f, 1f),
_host.DeltaSeconds,
_host.ViewportWidth,
_host.ViewportHeight,
IsOutdoor: false);
}
}
/// <summary>
/// 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 <c>foliage_wind.glsl</c> reads; every earlier member
/// keeps its ABI v1 offset. See <see cref="RenderPackShaderAbi.AtmosphericFrameSizeBytesV1"/>
/// and <c>atmospheric_common.glsl</c> for the byte-level contract both
/// backends and the SPIR-V validator agree on.
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 4)]
internal readonly struct AtmosphericFrameUniforms
{
internal const int SizeInBytes = 192;
internal AtmosphericFrameUniforms(
Vector4 sunScreen,
Vector4 sunColor,
Vector4 viewport,
Vector4 weather,
Vector4 sunDirection,
Vector4 policy,
Matrix4x4 inverseViewProjection,
Vector4 clockWind,
Vector4 windAmplitude)
{
SunScreen = sunScreen;
SunColor = sunColor;
Viewport = viewport;
Weather = weather;
SunDirection = sunDirection;
Policy = policy;
InverseViewProjection = inverseViewProjection;
ClockWind = clockWind;
WindAmplitude = windAmplitude;
}
internal readonly Vector4 SunScreen;
internal readonly Vector4 SunColor;
internal readonly Vector4 Viewport;
internal readonly Vector4 Weather;
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;
}
/// <summary>
/// Shader ABI SSOT for opt-in set 3 binding 7. Passes assign meanings to four std140
/// vec4 values without changing the shared descriptor layout.
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 4)]
internal readonly struct AtmosphericPackPassUniforms
{
internal const int SizeInBytes = 64;
internal AtmosphericPackPassUniforms(
Vector4 params0,
Vector4 params1,
Vector4 params2,
Vector4 params3)
{
Params0 = params0;
Params1 = params1;
Params2 = params2;
Params3 = params3;
}
internal readonly Vector4 Params0;
internal readonly Vector4 Params1;
internal readonly Vector4 Params2;
internal readonly Vector4 Params3;
internal static AtmosphericPackPassUniforms From(Vector4 params0) =>
new(params0, Vector4.Zero, Vector4.Zero, Vector4.Zero);
}
/// <summary>
/// Shader ABI SSOT for opt-in set 3 binding 8. API v1 exposes 64 scalar values in
/// descriptor declaration order, physically grouped as sixteen std140 vec4s.
/// </summary>
[InlineArray(RenderPackShaderAbi.PackSettingScalarCapacity)]
internal struct PackSettingsUniforms
{
internal const int SizeInBytes = RenderPackShaderAbi.PackSettingsSizeBytes;
private float _element0;
internal static PackSettingsUniforms Create(
RenderPackDescriptor descriptor,
RenderQualityPreset preset,
IReadOnlyDictionary<string, string>? userSettingOverrides = null)
{
var result = new PackSettingsUniforms();
int count = Math.Min(
descriptor.Settings.Count,
RenderPackShaderAbi.PackSettingScalarCapacity);
for (int i = 0; i < count; i++)
{
RenderSettingDeclaration setting = descriptor.Settings[i];
string value = RenderPackSettingResolution.Resolve(
setting,
preset,
userSettingOverrides);
result[i] = RenderPackSettingValueCodec.TryEncode(setting, value, out float encoded)
? encoded
: 0f;
}
return result;
}
}