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