acdream/src/AcDream.App/Rendering/Gpu/GpuPipelineDescription.cs

380 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 25
/// 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 26 (centre, two sheet axes,
/// colour, texture slot) and <c>particle_mesh</c> at 37 (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;
}