using System.Collections.Immutable;
using System.Numerics;
using System.Runtime.InteropServices;
using AcDream.App.Rendering.Gpu;
namespace AcDream.App.Rendering;
///
/// Campaign V slice V6l: the portal depth mask's RHI submission arm — V4g's
/// remaining half, and the reason the slice grew a stencil dimension.
///
/// Plan §5.5.16 defect 2: this renderer's two-pass punch (#117) is built
/// on glStencilFunc/glStencilOp/glStencilMask and the pinned
/// carried no stencil state at all, so it
/// stayed raw GL and was invisible to the Vulkan arm. The reviewed amendment
/// puts the ENABLE and the attachment intent in the pipeline
/// () and the per-draw compare,
/// ops, reference and masks on , because
/// core Vulkan 1.3 makes exactly that split dynamic.
///
/// Three pipelines, not one. Depth COMPARE is not dynamic in the
/// contract (only depth write is), and the punch's two passes differ in it —
/// mark tests LEQUAL and writes no depth, punch tests ALWAYS and
/// writes. The seal is a third: ALWAYS + write, with no stencil at all.
/// All three write no colour, which is what retail's "COLOR-INVISIBLE triangle
/// fan" means.
///
/// Two other differences from the GL arm. The fan is expanded to a
/// triangle LIST on the CPU, because has no
/// fan and Vulkan's is not portable; the expansion is exact (v0, vi, vi+1) and
/// rasterises the same triangles in the same order. And the clip planes travel
/// in the TerrainClip uniform block at binding 2 rather than as a loose
/// vec4[8] array, because Vulkan GLSL has no default uniform block — the
/// block already has precisely this shape and is already read by
/// terrain_modern.vert and sky.vert.
///
public sealed partial class PortalDepthMaskRenderer
{
private readonly IGpuDevice? _device;
private readonly ICurrentGpuFrameSource? _frames;
private readonly IWorldPassScope? _scope;
private IGpuPipeline? _sealPipeline;
private IGpuPipeline? _punchMarkPipeline;
private IGpuPipeline? _punchWritePipeline;
private bool _rhiFrameStarted;
/// One position per vertex — the only attribute portal_depth.vert reads.
internal static GpuVertexLayout PortalVertexLayout { get; } = GpuVertexLayout.Interleaved(
strideBytes: 3 * sizeof(float),
ImmutableArray.Create(
new GpuVertexAttribute(0, GpuVertexFormat.Float3, 0)));
///
/// The stencil reference the mark pass writes and the punch pass gates on.
/// Retail has no equivalent — the whole stencil pass is acdream's
/// z-buffered replacement for retail's painter's-order safety (#117) — so
/// the value is arbitrary and only has to agree with itself.
///
private const uint PunchStencilReference = 1;
///
/// The RHI arm's constructor. No GL context and no inline program: the three
/// pipelines compile portal_depth from the committed SPIR-V, and the
/// per-frame fan vertices come from the frame ring.
///
internal PortalDepthMaskRenderer(
IGpuDevice device,
ICurrentGpuFrameSource frames,
IWorldPassScope scope)
{
_device = device ?? throw new ArgumentNullException(nameof(device));
_frames = frames ?? throw new ArgumentNullException(nameof(frames));
_scope = scope ?? throw new ArgumentNullException(nameof(scope));
_resources = new ResourceCleanupGroup();
try
{
int samples = scope.SampleCount;
// SEAL: retail maxZ2, bit0 clear, data 0x00820e14 — depth ALWAYS at
// the polygon's true projected depth, no stencil. It runs
// immediately after the gated full depth clear, so there is no
// nearer content to stomp.
_sealPipeline = CreatePortalPipeline(
device,
"portal-depth-seal",
GpuCompareOp.Always,
depthWrite: true,
stencilTest: false,
GpuStencilState.Default,
samples);
// PUNCH pass A: mark stencil where the aperture fan passes a LEQUAL
// depth test at its (biased) true depth — i.e. where the aperture is
// actually visible against everything drawn so far.
_punchMarkPipeline = CreatePortalPipeline(
device,
"portal-depth-punch-mark",
GpuCompareOp.LessOrEqual,
depthWrite: false,
stencilTest: true,
GpuStencilState.Default with
{
Compare = GpuCompareOp.Always,
Fail = GpuStencilOp.Keep,
DepthFail = GpuStencilOp.Keep,
Pass = GpuStencilOp.Replace,
Reference = PunchStencilReference,
},
samples);
// PUNCH pass B: the far-Z write on marked pixels only, zeroing the
// stencil as it goes so the buffer is self-cleaning.
_punchWritePipeline = CreatePortalPipeline(
device,
"portal-depth-punch-write",
GpuCompareOp.Always,
depthWrite: true,
stencilTest: true,
GpuStencilState.Default with
{
Compare = GpuCompareOp.Equal,
Fail = GpuStencilOp.Keep,
DepthFail = GpuStencilOp.Keep,
Pass = GpuStencilOp.Zero,
Reference = PunchStencilReference,
},
samples);
}
catch
{
DisposeRhiResources();
throw;
}
}
private static IGpuPipeline CreatePortalPipeline(
IGpuDevice device,
string name,
GpuCompareOp depthCompare,
bool depthWrite,
bool stencilTest,
GpuStencilState stencil,
int sampleCount) =>
device.CreatePipeline(new GpuPipelineDescription
{
Name = name,
Shaders = new GpuShaderSet("portal_depth"),
VertexLayout = PortalVertexLayout,
Topology = GpuPrimitiveTopology.TriangleList,
Blend = GpuBlendMode.None,
Depth = new GpuDepthState(Test: true, Write: depthWrite, depthCompare),
// Portal fans face either way; the GL arm disables culling for the
// same reason.
Cull = GpuCullMode.None,
FrontFace = GpuFrontFace.CounterClockwise,
AlphaToCoverage = false,
// "an alpha-0 fan is no colour" in retail; a colour mask here.
ColorWrite = false,
StencilTest = stencilTest,
Stencil = stencil,
SampleCount = sampleCount,
});
private void DrawDepthFanRhi(
ReadOnlySpan worldVerts,
in Matrix4x4 viewProjection,
ReadOnlySpan planes,
bool forceFarZ)
{
if (!_rhiFrameStarted)
throw new InvalidOperationException("BeginFrame must be called before drawing portal depth masks.");
int n = Math.Min(worldVerts.Length, MaxFanVerts);
int planeCount = Math.Min(planes.Length, ClipFrame.MaxPlanes);
IGpuPassEncoder encoder = _scope!.RequireEncoder();
IGpuFrame frame = _frames!.CurrentFrame
?? throw new InvalidOperationException(
"PortalDepthMaskRenderer requires an open IGpuFrame (see GpuDeviceFrameLifetime).");
// The fan, expanded exactly: triangle i is (v0, v[i+1], v[i+2]).
int triangleCount = n - 2;
int vertexCount = triangleCount * 3;
GpuRingAllocation vertices = frame.AllocateRing(
vertexCount * 3 * sizeof(float),
GpuRingUsage.Vertex);
Span positions = vertices.AsSpan();
for (int triangle = 0; triangle < triangleCount; triangle++)
{
WritePosition(positions, triangle * 9, worldVerts[0]);
WritePosition(positions, triangle * 9 + 3, worldVerts[triangle + 1]);
WritePosition(positions, triangle * 9 + 6, worldVerts[triangle + 2]);
}
// The TerrainClip std140 block: an int count padded to 16 bytes, then
// eight clip-space half-planes. Every unused plane stays zero, which the
// shader never reads because it compares the index against the count.
GpuRingAllocation clip = frame.AllocateRing(
ClipFrame.TerrainUboBytes,
GpuRingUsage.Uniform);
clip.Data.Clear();
MemoryMarshal.Write(clip.Data, in planeCount);
Span clipPlanes = MemoryMarshal.Cast(
clip.Data[ClipFrame.CellClipPlanesOffset..]);
for (int i = 0; i < planeCount; i++)
clipPlanes[i] = planes[i];
if (!forceFarZ)
{
RecordPortalPass(
encoder,
_sealPipeline!,
clip,
vertices,
vertexCount,
in viewProjection,
renderPass: 0,
depthBias: 0f);
return;
}
RecordPortalPass(
encoder,
_punchMarkPipeline!,
clip,
vertices,
vertexCount,
in viewProjection,
renderPass: 0,
depthBias: PunchMarkDepthBias);
RecordPortalPass(
encoder,
_punchWritePipeline!,
clip,
vertices,
vertexCount,
in viewProjection,
renderPass: 1,
depthBias: 0f);
}
private static void RecordPortalPass(
IGpuPassEncoder encoder,
IGpuPipeline pipeline,
in GpuRingAllocation clip,
in GpuRingAllocation vertices,
int vertexCount,
in Matrix4x4 viewProjection,
int renderPass,
float depthBias)
{
encoder.BindPipeline(pipeline);
encoder.SetPushConstants(new GpuPushConstants
{
ViewProjection = viewProjection,
DrawIdOffset = 0,
LightingMode = 0,
// portal_depth.vert's "render pass" IS the seal/punch selector —
// the GL arm's uForceFarZ, rehomed onto the shared block.
RenderPass = renderPass,
LightDebug = 0,
TextureIndexA = 0,
TextureIndexB = 0,
ParamA = depthBias,
ParamB = PunchMarkBiasEyeCapMeters * CameraNearPlaneMeters,
});
encoder.BindUniformBuffer(
ClipFrame.TerrainClipUboBinding,
clip.Buffer,
clip.OffsetBytes,
(uint)ClipFrame.TerrainUboBytes);
encoder.BindVertexBuffer(0, vertices.Buffer, vertices.OffsetBytes);
encoder.Draw((uint)vertexCount, 1, 0, 0);
}
private static void WritePosition(Span destination, int offset, Vector3 position)
{
destination[offset] = position.X;
destination[offset + 1] = position.Y;
destination[offset + 2] = position.Z;
}
private void DisposeRhiResources()
{
List? failures = null;
void Attempt(Action action)
{
try { action(); }
catch (Exception error) { (failures ??= []).Add(error); }
}
Attempt(() => _sealPipeline?.Dispose());
_sealPipeline = null;
Attempt(() => _punchMarkPipeline?.Dispose());
_punchMarkPipeline = null;
Attempt(() => _punchWritePipeline?.Dispose());
_punchWritePipeline = null;
_rhiFrameStarted = false;
if (failures is not null)
{
throw new AggregateException(
"The portal depth mask's RHI resources did not fully release.",
failures);
}
}
}