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