using System.Numerics;
using AcDream.App.Rendering.Gpu;
namespace AcDream.App.Rendering;
///
/// The ordinary, non-ref view of the directional-shadow allocation produced for
/// one frame. The serial prevents a ring slice from leaking into a later frame.
///
internal readonly record struct DirectionalShadowFrameBinding(
long FrameSerial,
bool Enabled,
IGpuBuffer? Buffer,
uint OffsetBytes,
uint SizeBytes,
GpuTextureSlot TextureSlot,
int CascadeCount,
// Campaign VM VM6 review fix round (A3): the SAME AtmosphericFrame
// binding the caster pass bound this frame (see
// DirectionalSunShadowRenderer.RenderPrepared's atmosphericFrame
// parameter), carried on this existing per-frame seam so the world
// receiver pass (mesh_atmospheric.vert, which reads
// uAtmosphereClockWind/uAtmosphereWindAmplitude) binds set 3/binding 5
// itself instead of relying on whatever the caster pass happened to
// leave bound earlier in the frame. Unbound (default) is valid — it
// just means no caster ran this frame (or the source never supplied
// one, e.g. a declared pack); BindDirectionalShadowReceiver skips
// binding 5 in that case exactly like the caster side does.
AtmosphericFrameBufferBinding AtmosphericFrame = default)
{
internal static DirectionalShadowFrameBinding Disabled => default;
///
/// Campaign VM VM6 review fix round 4 (item 1): "there IS a real,
/// current-frame ring allocation here" — independent of whether the
/// shadow content it carries is Enabled. A directional-shadow-gated-off
/// frame (indoors, portal cover, night, sun-shadow-strength 0, ...)
/// still publishes a DISABLED-content block via
/// PublishDisabledReceiverBinding when an AtmosphericFrame binding is
/// available, specifically so the world receiver pipeline (which reads
/// wind from set 3/binding 5, not the shadow block) keeps running
/// regardless of shadow gating. This is the predicate consumers use to
/// decide "is there something here to bind" — see
/// BindDirectionalShadowReceiver and TerrainModernRenderer's own
/// receiver-pipeline selection.
///
internal bool IsBindableFor(IGpuFrame frame) =>
Buffer is not null
&& FrameSerial == frame.Serial
&& SizeBytes == DirectionalShadowUniforms.SizeInBytes;
///
/// "There is a real, current-frame, ENABLED shadow map here" — the
/// stricter predicate consumers that need actual shadow content (texture
/// slot, cascade count) must use, e.g. VolumetricShaftRenderer's own
/// gate. A disabled block published by PublishDisabledReceiverBinding
/// deliberately fails this (TextureSlot.Unassigned, CascadeCount 0) even
/// though it passes IsBindableFor.
///
internal bool IsValidFor(IGpuFrame frame) =>
IsBindableFor(frame)
&& Enabled
&& TextureSlot.IsAssigned
&& CascadeCount is >= 2 and <= 4;
}
///
/// Receiver-side seam. A pack runtime may publish this source at a stable frame
/// boundary without exposing the producer's target or ref-struct allocation.
///
internal interface IDirectionalShadowReceiverSource
{
DirectionalShadowPipelineShaders PipelineShaders { get; }
///
/// Campaign VM VM6 review fix round 4 (item 2): returns TRUE whenever
/// is bindable for
/// () — NOT
/// whether shadows are Enabled this frame. This deliberately decouples
/// world-receiver pipeline selection (which needs binding 5's wind
/// data, unconditionally, from any bound AtmosphericFrame) from the
/// directional-shadow gate (indoors, portal cover, night, strength 0,
/// ...), which used to also silently disable foliage wind because it
/// left the world pass on the plain (no-wind) pipeline. A caller that
/// needs actual shadow CONTENT — texture slot, cascade count — must
/// additionally check
/// on the returned binding (e.g. VolumetricShaftRenderer's own gate).
///
bool TryGetCurrentFrameBinding(
IGpuFrame frame,
out DirectionalShadowFrameBinding binding);
}
internal readonly record struct DirectionalShadowPipelineShaders(
GpuShaderSet TerrainCaster,
GpuShaderSet WorldOpaqueCaster,
GpuShaderSet WorldAlphaCutoutCaster,
GpuShaderSet TerrainReceiver,
GpuShaderSet WorldReceiver)
{
internal DirectionalShadowMultiviewPipelineShaders? MultiviewCasters { get; init; }
internal static DirectionalShadowPipelineShaders Local { get; } = new(
new GpuShaderSet("directional_shadow_terrain"),
new GpuShaderSet("directional_shadow_world_opaque"),
new GpuShaderSet("directional_shadow_world_cutout"),
new GpuShaderSet("terrain_atmospheric"),
new GpuShaderSet("mesh_atmospheric"))
{
MultiviewCasters = new DirectionalShadowMultiviewPipelineShaders(
new GpuShaderSet("directional_shadow_terrain_multiview"),
new GpuShaderSet("directional_shadow_world_opaque_multiview"),
new GpuShaderSet("directional_shadow_world_cutout_multiview")),
};
}
internal readonly record struct DirectionalShadowMultiviewPipelineShaders(
GpuShaderSet TerrainCaster,
GpuShaderSet WorldOpaqueCaster,
GpuShaderSet WorldAlphaCutoutCaster);
internal readonly record struct DirectionalShadowCascadeBlend(
int PrimaryCascade,
int SecondaryCascade,
float SecondaryWeight,
bool WithinShadowReach);
/// CPU mirror of receiver-only cascade and world-metre bias policy.
internal static class DirectionalShadowReceiverPolicy
{
internal const string AtmosphericWorldPassName = "atmospheric-world-hdr";
internal static bool ShouldSelectReceiverPipeline(
string passName,
bool sourcePresent,
bool bindingValid) =>
sourcePresent
&& bindingValid
&& string.Equals(
passName,
AtmosphericWorldPassName,
StringComparison.Ordinal);
internal static DirectionalShadowCascadeBlend SelectCascade(
float cameraDistanceMeters,
Vector4 splitFarMeters,
int cascadeCount,
float blendWidthMeters)
{
if (!float.IsFinite(cameraDistanceMeters) || cameraDistanceMeters < 0f)
throw new ArgumentOutOfRangeException(nameof(cameraDistanceMeters));
if (cascadeCount is < 2 or > 4)
throw new ArgumentOutOfRangeException(nameof(cascadeCount));
if (!float.IsFinite(blendWidthMeters) || blendWidthMeters < 0f)
throw new ArgumentOutOfRangeException(nameof(blendWidthMeters));
Span splits = stackalloc float[4]
{
splitFarMeters.X,
splitFarMeters.Y,
splitFarMeters.Z,
splitFarMeters.W,
};
for (int i = 0; i < cascadeCount; i++)
{
if (!float.IsFinite(splits[i])
|| splits[i] <= 0f
|| (i > 0 && splits[i] < splits[i - 1]))
{
throw new ArgumentException(
"Directional-shadow split distances must be finite, positive, and monotonic.",
nameof(splitFarMeters));
}
}
int primary = 0;
while (primary < cascadeCount && cameraDistanceMeters > splits[primary])
primary++;
if (primary == cascadeCount)
return new DirectionalShadowCascadeBlend(cascadeCount - 1, cascadeCount - 1, 0f, false);
if (primary == cascadeCount - 1 || blendWidthMeters <= 0f)
return new DirectionalShadowCascadeBlend(primary, primary, 0f, true);
float blendStart = MathF.Max(0f, splits[primary] - blendWidthMeters);
float t = Math.Clamp(
(cameraDistanceMeters - blendStart) / MathF.Max(blendWidthMeters, 1e-6f),
0f,
1f);
float smooth = t * t * (3f - 2f * t);
return new DirectionalShadowCascadeBlend(primary, primary + 1, smooth, true);
}
internal static float ReceiverBiasMeters(
in DirectionalShadowWorldBias bias,
float normalDotSurfaceToLight) =>
bias.ConstantDepthMeters
+ bias.SlopeDepthMeters * (1f - Math.Clamp(normalDotSurfaceToLight, 0f, 1f));
internal static bool ShouldSample(
bool bindingEnabled,
bool indoor,
bool hasSelectedCelestialDirectionalLight) =>
bindingEnabled && !indoor && hasSelectedCelestialDirectionalLight;
}