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. /// /// Campaign FW restores retail's ordered frame walk, so this Vulkan arm /// now records the exact retail state for both operations: one color-invisible /// triangle fan, depth compare ALWAYS, depth write enabled, culling disabled. /// The push-constant pass selector chooses true projected depth (seal) or /// forced far-Z (punch). There is no stencil mark pass or depth bias. /// /// 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? _depthWritePipeline; 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 RHI arm's constructor. No GL context and no inline program: the one /// pipeline compiles 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; // Retail DrawPortalPolyInternal @0x0059BC90 uses the same depth // state for seals and punches. maxZ2/maxZ1 only select the vertex // depth written by the shader. _depthWritePipeline = CreatePortalPipeline( device, "portal-depth-write", GpuCompareOp.Always, depthWrite: true, stencilTest: false, GpuStencilState.Default, 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]; RecordPortalPass( encoder, _depthWritePipeline!, clip, vertices, vertexCount, in viewProjection, renderPass: forceFarZ ? 1 : 0); } private static void RecordPortalPass( IGpuPassEncoder encoder, IGpuPipeline pipeline, in GpuRingAllocation clip, in GpuRingAllocation vertices, int vertexCount, in Matrix4x4 viewProjection, int renderPass) { 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 = 0f, ParamB = 0f, }); 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(() => _depthWritePipeline?.Dispose()); _depthWritePipeline = null; _rhiFrameStarted = false; if (failures is not null) { throw new AggregateException( "The portal depth mask's RHI resources did not fully release.", failures); } } }