using AcDream.App.Rendering.Gpu; namespace AcDream.App.Rendering.Wb; public sealed partial class WbDrawDispatcher { internal sealed class DirectionalShadowReceiverPipelineState : IDisposable { private readonly MeshPipelineSet _backbuffer; private readonly MeshPipelineSet _offscreen; internal DirectionalShadowReceiverPipelineState( IDirectionalShadowReceiverSource source, MeshPipelineSet backbuffer, MeshPipelineSet offscreen) { Source = source; _backbuffer = backbuffer; _offscreen = offscreen; } internal IDirectionalShadowReceiverSource Source { get; } internal MeshPipelineSet ForSampleCount(int sampleCount) => sampleCount > 1 ? _backbuffer : _offscreen; public void Dispose() { DisposeMeshPipelineSet(_backbuffer); if (!ReferenceEquals(_offscreen, _backbuffer)) DisposeMeshPipelineSet(_offscreen); } } private DirectionalShadowReceiverPipelineState? _directionalShadowReceiver; /// /// Builds both sample-count variants without publishing either. A different /// pack always gets pipelines compiled from that candidate's shader blobs; /// no prior pack pipeline is reused by shader name or nullable caching. /// internal DirectionalShadowReceiverPipelineState? PrepareDirectionalShadowReceiver( IDirectionalShadowReceiverSource? source, int sampleCount) { if (source is null) return null; IGpuDevice device = _device ?? throw new InvalidOperationException("Directional receivers require the modern RHI device."); if (_scope is null || sampleCount != _scope.SampleCount) throw new InvalidOperationException("Receiver and world-pass sample counts must match."); MeshPipelineSet? backbuffer = null; MeshPipelineSet? offscreen = null; try { backbuffer = CreateMeshPipelineSet( device, sampleCount, baseShaders: source.PipelineShaders.WorldReceiver, namePrefix: "wb-mesh-atmospheric", usesRenderPackShaderAbi: true); offscreen = sampleCount == 1 ? backbuffer : CreateMeshPipelineSet( device, 1, baseShaders: source.PipelineShaders.WorldReceiver, namePrefix: "wb-mesh-atmospheric", usesRenderPackShaderAbi: true); return new DirectionalShadowReceiverPipelineState(source, backbuffer, offscreen); } catch { DisposeMeshPipelineSet(backbuffer); if (!ReferenceEquals(offscreen, backbuffer)) DisposeMeshPipelineSet(offscreen); throw; } } /// Assignment-only publication; returned state retires after the coupled swap. internal DirectionalShadowReceiverPipelineState? SwapDirectionalShadowReceiver( DirectionalShadowReceiverPipelineState? candidate) { DirectionalShadowReceiverPipelineState? prior = _directionalShadowReceiver; _directionalShadowReceiver = candidate; return prior; } private MeshPipelineSet PipelinesFor( IGpuPassEncoder encoder, IGpuFrame frame, out DirectionalShadowFrameBinding shadowBinding) { DirectionalShadowReceiverPipelineState? receiver = _directionalShadowReceiver; IDirectionalShadowReceiverSource? source = receiver?.Source; shadowBinding = DirectionalShadowFrameBinding.Disabled; bool bindingValid = source is not null && source.TryGetCurrentFrameBinding(frame, out shadowBinding); if (DirectionalShadowReceiverPolicy.ShouldSelectReceiverPipeline( encoder.Pass.Name, source is not null, bindingValid)) { return receiver!.ForSampleCount(encoder.Pass.SampleCount); } return PipelinesFor(encoder); } // Campaign VM VM6 review fix round 2 (A3 test gap): internal, not // private, so DirectionalShadowReceiverTests can drive it directly with // a bare RecordingGpuDevice pass encoder instead of standing up the // full WbDrawDispatcher/mesh-manager/world-pass-scope dependency chain // just to prove this one bind call is correct. internal static void BindDirectionalShadowReceiver( IGpuPassEncoder encoder, in DirectionalShadowFrameBinding binding) { // Campaign VM VM6 review fix round 4 (item 4): bind on BINDABLE, // not Enabled. A directional-shadow-gated-off frame still publishes // a real (disabled-content) binding via // DirectionalSunShadowRenderer.PublishDisabledReceiverBinding so // the world receiver pipeline (selected on the exact same // TryGetCurrentFrameBinding predicate — see // WbDrawDispatcher.PipelinesFor above) can bind BOTH words: the // disabled shadow block and the real AtmosphericFrame wind data // mesh_atmospheric.vert needs regardless of shadow gating. Review // fix round 5 (F4 correction): with the flag bit clear the // receiver shaders take visibility 1.0 AND fall back to the // authored uLights direction (mesh_atmospheric.vert/ // terrain_atmospheric.vert, round 5) — NOT "the flag bit alone // makes it numerically the plain lighting sum" as an earlier round // claimed; the vertex shaders sourced the sun direction from this // very block unconditionally until round 5's shader fix, so the // fallback needed BOTH the flag-gated visibility (already correct) // and the flag-gated direction (round 5) to actually match the // plain pipeline. if (binding.Buffer is null) return; encoder.BindUniformBuffer( GpuBindingModel.UniformDirectionalShadow, binding.Buffer, binding.OffsetBytes, binding.SizeBytes); // Campaign VM VM6 review fix round (A3): mesh_atmospheric.vert reads // uAtmosphereClockWind/uAtmosphereWindAmplitude from set 3/binding 5 // — bind it here explicitly with the EXACT buffer/offset the caster // pass bound earlier this same frame (carried on // DirectionalShadowFrameBinding.AtmosphericFrame), rather than // depending on that earlier bind surviving un-reset until this pass // runs. Unbound (the declared-pack / no-caster-this-frame case) is a // no-op here exactly like the caster side. // // Review fix round 2 (F5): the buffer bound here is the CASTER's own // independent allocation (AtmosphericPostProcessGraph // .BuildShadowAtmosphericFrameBinding), NOT the receiver's/post- // process's fuller frame block. Its seven ABI v1 members (sun // screen/color, viewport, weather, sun direction, policy, inverse- // view-projection) are ALL ZERO/Identity by construction — only the // two appended v2 members (clock/wind, wind amplitude) carry real // content. That is safe today because mesh_atmospheric.vert reads // this binding SOLELY for foliage_wind.glsl's wind displacement; if // a future change makes mesh_atmospheric.vert or .frag read any v1 // member (sun/weather/policy/etc.) during this same world-mesh // pass, it will read zeros here and must get its own real binding // instead of assuming this one is fully populated. if (binding.AtmosphericFrame.IsBound) { encoder.BindUniformBuffer( GpuBindingModel.UniformAtmosphericFrame, binding.AtmosphericFrame.Buffer!, binding.AtmosphericFrame.OffsetBytes, binding.AtmosphericFrame.SizeBytes); } } private void DisposeDirectionalShadowReceiverPipelines() { DirectionalShadowReceiverPipelineState? state = SwapDirectionalShadowReceiver(null); state?.Dispose(); } }