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; }