219 lines
8.4 KiB
C#
219 lines
8.4 KiB
C#
using System.Collections.Immutable;
|
|
using System.Numerics;
|
|
using System.Runtime.InteropServices;
|
|
using AcDream.App.Rendering.Gpu;
|
|
|
|
namespace AcDream.App.Rendering;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
///
|
|
/// <para>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.</para>
|
|
///
|
|
/// <para><b>Two other differences from the GL arm.</b> The fan is expanded to a
|
|
/// triangle LIST on the CPU, because <see cref="GpuPrimitiveTopology"/> 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 <c>TerrainClip</c> uniform block at binding 2 rather than as a loose
|
|
/// <c>vec4[8]</c> array, because Vulkan GLSL has no default uniform block — the
|
|
/// block already has precisely this shape and is already read by
|
|
/// <c>terrain_modern.vert</c> and <c>sky.vert</c>.</para>
|
|
/// </summary>
|
|
public sealed partial class PortalDepthMaskRenderer
|
|
{
|
|
private readonly IGpuDevice? _device;
|
|
private readonly ICurrentGpuFrameSource? _frames;
|
|
private readonly IWorldPassScope? _scope;
|
|
private IGpuPipeline? _depthWritePipeline;
|
|
private bool _rhiFrameStarted;
|
|
|
|
/// <summary>One position per vertex — the only attribute <c>portal_depth.vert</c> reads.</summary>
|
|
internal static GpuVertexLayout PortalVertexLayout { get; } = GpuVertexLayout.Interleaved(
|
|
strideBytes: 3 * sizeof(float),
|
|
ImmutableArray.Create(
|
|
new GpuVertexAttribute(0, GpuVertexFormat.Float3, 0)));
|
|
|
|
/// <summary>
|
|
/// The RHI arm's constructor. No GL context and no inline program: the one
|
|
/// pipeline compiles <c>portal_depth</c> from the committed SPIR-V, and the
|
|
/// per-frame fan vertices come from the frame ring.
|
|
/// </summary>
|
|
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<Vector3> worldVerts,
|
|
in Matrix4x4 viewProjection,
|
|
ReadOnlySpan<Vector4> 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<float> positions = vertices.AsSpan<float>();
|
|
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<Vector4> clipPlanes = MemoryMarshal.Cast<byte, Vector4>(
|
|
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<float> destination, int offset, Vector3 position)
|
|
{
|
|
destination[offset] = position.X;
|
|
destination[offset + 1] = position.Y;
|
|
destination[offset + 2] = position.Z;
|
|
}
|
|
|
|
private void DisposeRhiResources()
|
|
{
|
|
List<Exception>? 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);
|
|
}
|
|
}
|
|
}
|