380 lines
16 KiB
C#
380 lines
16 KiB
C#
using System.Collections.Immutable;
|
||
|
||
namespace AcDream.App.Rendering.Gpu;
|
||
|
||
/// <summary>Vertex attribute component layout. Only the formats acdream's meshes actually use.</summary>
|
||
internal enum GpuVertexFormat
|
||
{
|
||
Float1,
|
||
Float2,
|
||
Float3,
|
||
Float4,
|
||
|
||
/// <summary>Four unsigned bytes scaled to [0,1] floats — a shader <c>vec4</c> input.</summary>
|
||
UByte4Normalized,
|
||
|
||
/// <summary>
|
||
/// Four unsigned bytes delivered as INTEGERS — a shader <c>uvec4</c> input.
|
||
///
|
||
/// Distinct from <see cref="UByte4Normalized"/> in kind, not just in scaling:
|
||
/// GL requires <c>glVertexAttribIPointer</c> for an integer shader input and
|
||
/// leaves the value undefined if it arrives through the float path, and Vulkan
|
||
/// needs the format named as <c>R8G8B8A8_UINT</c> rather than <c>_UNORM</c>.
|
||
///
|
||
/// Added at slice V4d, which found `terrain_modern.vert` declares locations 2–5
|
||
/// as <c>uvec4</c> and feeds them with <c>glVertexAttribIPointer</c>. Those
|
||
/// packed bytes carry terrain-type, road and split-direction codes that drive
|
||
/// every blend decision, so normalising them would not be an approximation —
|
||
/// it would be garbage.
|
||
/// </summary>
|
||
UByte4UInt,
|
||
|
||
/// <summary>
|
||
/// One unsigned 32-bit integer — a shader <c>uint</c> input.
|
||
///
|
||
/// <para>Added at slice V6l with the instanced-vertex-input amendment, and
|
||
/// necessary to it: <c>particle.vert</c> declares
|
||
/// <c>layout(location = 6) in uint aTextureIndex</c>, the per-instance
|
||
/// texture-table slot, and the amendment's premise is that no shader is
|
||
/// edited. Same kind-distinction as <see cref="UByte4UInt"/> — GL needs
|
||
/// <c>glVertexAttribIPointer</c> and Vulkan needs <c>R32_UINT</c>; the float
|
||
/// path would deliver the value's bits reinterpreted rather than a scaled
|
||
/// approximation of it.</para>
|
||
/// </summary>
|
||
UInt1,
|
||
}
|
||
|
||
/// <summary>
|
||
/// How often a vertex binding advances.
|
||
///
|
||
/// <para>Added at slice V6l. Both particle pipelines draw with PER-INSTANCE
|
||
/// vertex attributes — <c>particle</c> at locations 2–6 (centre, two sheet axes,
|
||
/// colour, texture slot) and <c>particle_mesh</c> at 3–7 (a <c>mat4</c> model and
|
||
/// a colour) — and the V0 contract could express instanced DRAWING but not
|
||
/// instanced vertex INPUT, which blocked V4e (plan §5.5.16). Both backends carry
|
||
/// this natively and at no cost: <c>VK_VERTEX_INPUT_RATE_INSTANCE</c> and
|
||
/// <c>glVertexAttribDivisor</c>.</para>
|
||
/// </summary>
|
||
internal enum GpuVertexInputRate
|
||
{
|
||
/// <summary>The binding advances once per vertex — the default for every layout written before V6l.</summary>
|
||
Vertex,
|
||
|
||
/// <summary>The binding advances once per instance (GL divisor 1).</summary>
|
||
Instance,
|
||
}
|
||
|
||
/// <summary>
|
||
/// One bound vertex buffer's shape: which binding index it occupies, how many
|
||
/// bytes one element occupies, and how often it advances.
|
||
///
|
||
/// <para>Slice V6l. Before it, a layout had exactly one stride and one implicit
|
||
/// binding 0 at vertex rate; that is now the <see cref="GpuVertexLayout.Interleaved"/>
|
||
/// case rather than the only case.</para>
|
||
/// </summary>
|
||
internal readonly record struct GpuVertexBinding(
|
||
uint Binding,
|
||
uint StrideBytes,
|
||
GpuVertexInputRate InputRate = GpuVertexInputRate.Vertex);
|
||
|
||
/// <summary>
|
||
/// One vertex attribute, matching a <c>layout(location = N) in</c> declaration.
|
||
/// <paramref name="Binding"/> names which <see cref="GpuVertexBinding"/> supplies
|
||
/// it and defaults to 0, so every layout written before slice V6l keeps its
|
||
/// meaning unchanged.
|
||
/// </summary>
|
||
internal readonly record struct GpuVertexAttribute(
|
||
uint Location,
|
||
GpuVertexFormat Format,
|
||
uint OffsetBytes,
|
||
uint Binding = 0);
|
||
|
||
/// <summary>
|
||
/// Vertex input layout: the bound buffers and the attributes they feed.
|
||
///
|
||
/// <para>Slice V6l grew this from one stride to a list of bindings. The overwhelmingly
|
||
/// common case is still one interleaved vertex-rate buffer, which
|
||
/// <see cref="Interleaved"/> spells in one line and which every layout in the
|
||
/// tree before V6l uses.</para>
|
||
/// </summary>
|
||
internal sealed record GpuVertexLayout(
|
||
ImmutableArray<GpuVertexBinding> Bindings,
|
||
ImmutableArray<GpuVertexAttribute> Attributes)
|
||
{
|
||
/// <summary>
|
||
/// One interleaved vertex-rate buffer at binding 0 — the shape of every
|
||
/// layout the campaign wrote before slice V6l.
|
||
/// </summary>
|
||
public static GpuVertexLayout Interleaved(
|
||
uint strideBytes,
|
||
ImmutableArray<GpuVertexAttribute> attributes) =>
|
||
new(
|
||
[new GpuVertexBinding(0, strideBytes, GpuVertexInputRate.Vertex)],
|
||
attributes);
|
||
|
||
/// <summary>
|
||
/// Stride of binding 0. Kept because it is what an interleaved layout means
|
||
/// and what every single-binding consumer asks for; multi-binding consumers
|
||
/// use <see cref="StrideOf"/>.
|
||
/// </summary>
|
||
public uint StrideBytes =>
|
||
Bindings.IsDefaultOrEmpty ? 0u : Bindings[0].StrideBytes;
|
||
|
||
/// <summary>Stride of <paramref name="binding"/>, or a composition error if it is not declared.</summary>
|
||
public uint StrideOf(uint binding)
|
||
{
|
||
foreach (GpuVertexBinding candidate in Bindings)
|
||
{
|
||
if (candidate.Binding == binding)
|
||
return candidate.StrideBytes;
|
||
}
|
||
|
||
throw new ArgumentOutOfRangeException(
|
||
nameof(binding),
|
||
binding,
|
||
"The vertex layout declares no such binding.");
|
||
}
|
||
|
||
/// <summary>How often <paramref name="binding"/> advances.</summary>
|
||
public GpuVertexInputRate InputRateOf(uint binding)
|
||
{
|
||
foreach (GpuVertexBinding candidate in Bindings)
|
||
{
|
||
if (candidate.Binding == binding)
|
||
return candidate.InputRate;
|
||
}
|
||
|
||
throw new ArgumentOutOfRangeException(
|
||
nameof(binding),
|
||
binding,
|
||
"The vertex layout declares no such binding.");
|
||
}
|
||
|
||
/// <summary>
|
||
/// The world mesh vertex shared by <c>mesh_modern</c>, EnvCells, and terrain:
|
||
/// position, normal, texcoord — 32 bytes, matching the format
|
||
/// <c>ObjectMeshManager</c> packs into <c>GlobalMeshBuffer</c>.
|
||
/// </summary>
|
||
public static GpuVertexLayout WorldMesh { get; } = Interleaved(
|
||
strideBytes: 32,
|
||
[
|
||
new GpuVertexAttribute(0, GpuVertexFormat.Float3, 0),
|
||
new GpuVertexAttribute(1, GpuVertexFormat.Float3, 12),
|
||
new GpuVertexAttribute(2, GpuVertexFormat.Float2, 24),
|
||
]);
|
||
|
||
/// <summary>Empty layout for pipelines whose vertices come entirely from storage buffers.</summary>
|
||
public static GpuVertexLayout None { get; } = new([], []);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Names one SPIR-V shader pair. Renderer-owned shaders resolve from the
|
||
/// committed shader directory. A selected render pack instead supplies an
|
||
/// immutable candidate-owned byte pair, so validation never turns into a
|
||
/// second host-path lookup or a private built-in shortcut.
|
||
/// </summary>
|
||
internal readonly record struct GpuShaderSet
|
||
{
|
||
internal GpuShaderSet(string name)
|
||
: this(name, ReadOnlyMemory<byte>.Empty, ReadOnlyMemory<byte>.Empty)
|
||
{
|
||
}
|
||
|
||
internal GpuShaderSet(
|
||
string name,
|
||
ReadOnlyMemory<byte> vertexSpirv,
|
||
ReadOnlyMemory<byte> fragmentSpirv)
|
||
{
|
||
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||
if (vertexSpirv.IsEmpty != fragmentSpirv.IsEmpty)
|
||
throw new ArgumentException("Both SPIR-V stages must be supplied together.");
|
||
Name = name;
|
||
VertexSpirv = vertexSpirv;
|
||
FragmentSpirv = fragmentSpirv;
|
||
}
|
||
|
||
internal string Name { get; }
|
||
|
||
internal ReadOnlyMemory<byte> VertexSpirv { get; }
|
||
|
||
internal ReadOnlyMemory<byte> FragmentSpirv { get; }
|
||
|
||
internal bool HasEmbeddedSpirv => !VertexSpirv.IsEmpty;
|
||
}
|
||
|
||
/// <summary>Depth-buffer behaviour baked into a pipeline.</summary>
|
||
/// <param name="Test">Whether depth testing is enabled at all.</param>
|
||
/// <param name="Write">Default depth-write state; overridable per draw via dynamic state.</param>
|
||
/// <param name="Compare">Comparison function when testing is enabled.</param>
|
||
internal readonly record struct GpuDepthState(bool Test, bool Write, GpuCompareOp Compare)
|
||
{
|
||
/// <summary>Standard opaque geometry: test and write, nearer wins.</summary>
|
||
public static GpuDepthState OpaqueDefault { get; } = new(Test: true, Write: true, GpuCompareOp.LessOrEqual);
|
||
|
||
/// <summary>Translucent geometry: test against existing depth but do not occlude later draws.</summary>
|
||
public static GpuDepthState TranslucentDefault { get; } = new(Test: true, Write: false, GpuCompareOp.LessOrEqual);
|
||
|
||
/// <summary>Sky and 2-D overlays: depth is irrelevant.</summary>
|
||
public static GpuDepthState Disabled { get; } = new(Test: false, Write: false, GpuCompareOp.Always);
|
||
}
|
||
|
||
/// <summary>
|
||
/// The per-draw half of stencil state: the comparison, what happens at each of
|
||
/// its three outcomes, and the reference and masks it uses.
|
||
///
|
||
/// <para>Added at slice V6l. Core Vulkan 1.3 makes ALL of these dynamic
|
||
/// (<c>VK_DYNAMIC_STATE_STENCIL_OP</c>, <c>_COMPARE_MASK</c>, <c>_WRITE_MASK</c>,
|
||
/// <c>_REFERENCE</c>). They live here as a pipeline DEFAULT and on
|
||
/// <see cref="IGpuPassEncoder.SetStencil"/> as the per-draw override — exactly
|
||
/// the split cull mode, front face and depth write already have. The portal
|
||
/// renderer no longer consumes stencil after Campaign FW restored retail's
|
||
/// ordered one-pass depth punch; the facility remains for render packs and
|
||
/// future stencil users.
|
||
/// Whether the pipeline uses the stencil aspect at all is
|
||
/// <see cref="GpuPipelineDescription.StencilTest"/>, because that is also the
|
||
/// attachment intent.</para>
|
||
///
|
||
/// <para>The front and back faces always carry the same state. Retail's portal
|
||
/// fans are drawn with culling off and face either way, so a two-sided
|
||
/// distinction would be a facility with no consumer.</para>
|
||
/// </summary>
|
||
internal readonly record struct GpuStencilState(
|
||
GpuCompareOp Compare,
|
||
GpuStencilOp Fail,
|
||
GpuStencilOp DepthFail,
|
||
GpuStencilOp Pass,
|
||
uint Reference,
|
||
uint CompareMask,
|
||
uint WriteMask)
|
||
{
|
||
/// <summary>GL's and Vulkan's own defaults: always pass, never write.</summary>
|
||
public static GpuStencilState Default { get; } = new(
|
||
GpuCompareOp.Always,
|
||
GpuStencilOp.Keep,
|
||
GpuStencilOp.Keep,
|
||
GpuStencilOp.Keep,
|
||
Reference: 0,
|
||
CompareMask: 0xFF,
|
||
WriteMask: 0xFF);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Everything a draw needs beyond its buffers: the shader pair and all fixed
|
||
/// state. Vulkan bakes this into one <c>VkPipeline</c> at startup, which is why
|
||
/// runtime shader compilation and driver state revalidation both disappear.
|
||
///
|
||
/// Equality is NOT part of this contract — pipelines are created explicitly and
|
||
/// held by their renderer. <see cref="Name"/> is the identity used by debug
|
||
/// tooling and by the backend's own pipeline cache key.
|
||
/// </summary>
|
||
internal sealed record GpuPipelineDescription
|
||
{
|
||
/// <summary>Non-zero only for a pipeline compiled for a matching multiview pass.</summary>
|
||
public uint ViewMask { get; init; }
|
||
/// <summary>Stable identifier, e.g. <c>"mesh-opaque"</c>. Surfaced to RenderDoc and validation layers.</summary>
|
||
public required string Name { get; init; }
|
||
|
||
/// <summary>The GLSL pair this pipeline draws with.</summary>
|
||
public required GpuShaderSet Shaders { get; init; }
|
||
|
||
/// <summary>Vertex input layout; <see cref="GpuVertexLayout.None"/> for buffer-fed geometry.</summary>
|
||
public required GpuVertexLayout VertexLayout { get; init; }
|
||
|
||
public GpuPrimitiveTopology Topology { get; init; } = GpuPrimitiveTopology.TriangleList;
|
||
|
||
public GpuBlendMode Blend { get; init; } = GpuBlendMode.None;
|
||
|
||
public GpuDepthState Depth { get; init; } = GpuDepthState.OpaqueDefault;
|
||
|
||
/// <summary>Default cull mode; overridable per draw via <see cref="IGpuPassEncoder.SetCullMode"/>.</summary>
|
||
public GpuCullMode Cull { get; init; } = GpuCullMode.Back;
|
||
|
||
/// <summary>
|
||
/// Winding treated as front-facing, expressed in GL convention. The Vulkan
|
||
/// backend inverts it internally to compensate for its negative viewport
|
||
/// height; no renderer performs that flip itself.
|
||
/// </summary>
|
||
public GpuFrontFace FrontFace { get; init; } = GpuFrontFace.CounterClockwise;
|
||
|
||
/// <summary>
|
||
/// Alpha-to-coverage for foliage. Only meaningful when the pass is
|
||
/// multisampled; the backend ignores it at one sample.
|
||
/// </summary>
|
||
public bool AlphaToCoverage { get; init; }
|
||
|
||
/// <summary>Whether the pipeline writes colour at all. False for depth/stencil-only prepasses.</summary>
|
||
public bool ColorWrite { get; init; } = true;
|
||
|
||
/// <summary>
|
||
/// Whether the compatible dynamic-rendering pass carries a colour
|
||
/// attachment. False creates a true depth-only graphics pipeline.
|
||
/// </summary>
|
||
public bool HasColorAttachment { get; init; } = true;
|
||
|
||
/// <summary>
|
||
/// Whether this pipeline uses the stencil aspect at all.
|
||
///
|
||
/// <para>Added at slice V6l for #117's portal punch, which is the only
|
||
/// consumer in the tree and which nothing else could express: the V0 contract
|
||
/// carried no stencil state, so <c>PortalDepthMaskRenderer</c> stayed raw GL
|
||
/// and V4g's "stencil/depth-mask pipelines" row could not be written (plan
|
||
/// §5.5.16 defect 2).</para>
|
||
///
|
||
/// <para>This is the ENABLE and the attachment intent together, which is why
|
||
/// it is baked while everything in <see cref="Stencil"/> is dynamic. Vulkan
|
||
/// makes <c>stencilTestEnable</c> dynamic too, but a pipeline that declares
|
||
/// the stencil dynamic states obliges every draw with it to have set them,
|
||
/// so a pipeline that will never test stencil is better off saying so once.
|
||
/// </para>
|
||
/// </summary>
|
||
public bool StencilTest { get; init; }
|
||
|
||
/// <summary>
|
||
/// Default stencil compare/op/reference/mask, re-established by
|
||
/// <c>BindPipeline</c> and overridable per draw through
|
||
/// <see cref="IGpuPassEncoder.SetStencil"/>. Ignored entirely when
|
||
/// <see cref="StencilTest"/> is false.
|
||
/// </summary>
|
||
public GpuStencilState Stencil { get; init; } = GpuStencilState.Default;
|
||
|
||
/// <summary>
|
||
/// Format of the colour attachment this pipeline renders into.
|
||
///
|
||
/// Vulkan's dynamic rendering bakes the attachment format into the pipeline:
|
||
/// <c>VkPipelineRenderingCreateInfo</c> has to name it at creation, and a
|
||
/// pipeline whose declared format differs from the attachment it is used
|
||
/// with is undefined. GL has no equivalent — a framebuffer carries its own
|
||
/// attachment formats and a program is bound to whatever is attached — so
|
||
/// the GL backend ignores this field entirely.
|
||
///
|
||
/// The default is the offscreen render-target format, which the Vulkan
|
||
/// backend maps to the swapchain's <c>B8G8R8A8_UNORM</c>
|
||
/// (<c>VulkanTextureFormatMapping.CanonicalColorAttachmentFormat</c>) so
|
||
/// that backbuffer and offscreen pipelines really do agree. That mapping is
|
||
/// what made this field necessary and is why the default preserves it: slice
|
||
/// V6c had to hard-code one format for every pipeline because the contract
|
||
/// could not express the question, and recorded the gap rather than hiding
|
||
/// it. Naming the format here is the reviewed fix, in the same shape as
|
||
/// <see cref="GpuBlendMode.InverseAlpha"/> (V4c) and
|
||
/// <see cref="GpuVertexFormat.UByte4UInt"/> (V4d).
|
||
/// </summary>
|
||
public GpuTextureFormat ColorFormat { get; init; } = GpuTextureFormat.Rgba8UnormRenderTarget;
|
||
|
||
/// <summary>
|
||
/// Whether an opt-in graph may prebuild this pipeline against an additional
|
||
/// colour-attachment format. World pipelines leave this enabled; dedicated
|
||
/// fullscreen pipelines already name their only format and disable it.
|
||
/// </summary>
|
||
public bool AllowColorFormatVariants { get; init; } = true;
|
||
|
||
/// <summary>
|
||
/// Opts this pipeline into render-pack shader ABI v1. Vulkan then uses the
|
||
/// lazy four-set pipeline layout whose set 3 contains bindings 5..8; retail
|
||
/// pipelines keep the authoritative three-set layout and create no pack
|
||
/// descriptors or layouts.
|
||
/// </summary>
|
||
public bool UsesRenderPackShaderAbi { get; init; }
|
||
|
||
/// <summary>Sample count of the passes this pipeline is used in. Must match the pass.</summary>
|
||
public int SampleCount { get; init; } = 1;
|
||
}
|