using System.Collections.Immutable;
namespace AcDream.App.Rendering.Gpu;
/// Vertex attribute component layout. Only the formats acdream's meshes actually use.
internal enum GpuVertexFormat
{
Float1,
Float2,
Float3,
Float4,
/// Four unsigned bytes scaled to [0,1] floats — a shader vec4 input.
UByte4Normalized,
///
/// Four unsigned bytes delivered as INTEGERS — a shader uvec4 input.
///
/// Distinct from in kind, not just in scaling:
/// GL requires glVertexAttribIPointer for an integer shader input and
/// leaves the value undefined if it arrives through the float path, and Vulkan
/// needs the format named as R8G8B8A8_UINT rather than _UNORM.
///
/// Added at slice V4d, which found `terrain_modern.vert` declares locations 2–5
/// as uvec4 and feeds them with glVertexAttribIPointer. 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.
///
UByte4UInt,
///
/// One unsigned 32-bit integer — a shader uint input.
///
/// Added at slice V6l with the instanced-vertex-input amendment, and
/// necessary to it: particle.vert declares
/// layout(location = 6) in uint aTextureIndex, the per-instance
/// texture-table slot, and the amendment's premise is that no shader is
/// edited. Same kind-distinction as — GL needs
/// glVertexAttribIPointer and Vulkan needs R32_UINT; the float
/// path would deliver the value's bits reinterpreted rather than a scaled
/// approximation of it.
///
UInt1,
}
///
/// How often a vertex binding advances.
///
/// Added at slice V6l. Both particle pipelines draw with PER-INSTANCE
/// vertex attributes — particle at locations 2–6 (centre, two sheet axes,
/// colour, texture slot) and particle_mesh at 3–7 (a mat4 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: VK_VERTEX_INPUT_RATE_INSTANCE and
/// glVertexAttribDivisor.
///
internal enum GpuVertexInputRate
{
/// The binding advances once per vertex — the default for every layout written before V6l.
Vertex,
/// The binding advances once per instance (GL divisor 1).
Instance,
}
///
/// One bound vertex buffer's shape: which binding index it occupies, how many
/// bytes one element occupies, and how often it advances.
///
/// Slice V6l. Before it, a layout had exactly one stride and one implicit
/// binding 0 at vertex rate; that is now the
/// case rather than the only case.
///
internal readonly record struct GpuVertexBinding(
uint Binding,
uint StrideBytes,
GpuVertexInputRate InputRate = GpuVertexInputRate.Vertex);
///
/// One vertex attribute, matching a layout(location = N) in declaration.
/// names which supplies
/// it and defaults to 0, so every layout written before slice V6l keeps its
/// meaning unchanged.
///
internal readonly record struct GpuVertexAttribute(
uint Location,
GpuVertexFormat Format,
uint OffsetBytes,
uint Binding = 0);
///
/// Vertex input layout: the bound buffers and the attributes they feed.
///
/// Slice V6l grew this from one stride to a list of bindings. The overwhelmingly
/// common case is still one interleaved vertex-rate buffer, which
/// spells in one line and which every layout in the
/// tree before V6l uses.
///
internal sealed record GpuVertexLayout(
ImmutableArray Bindings,
ImmutableArray Attributes)
{
///
/// One interleaved vertex-rate buffer at binding 0 — the shape of every
/// layout the campaign wrote before slice V6l.
///
public static GpuVertexLayout Interleaved(
uint strideBytes,
ImmutableArray attributes) =>
new(
[new GpuVertexBinding(0, strideBytes, GpuVertexInputRate.Vertex)],
attributes);
///
/// 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 .
///
public uint StrideBytes =>
Bindings.IsDefaultOrEmpty ? 0u : Bindings[0].StrideBytes;
/// Stride of , or a composition error if it is not declared.
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.");
}
/// How often advances.
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.");
}
///
/// The world mesh vertex shared by mesh_modern, EnvCells, and terrain:
/// position, normal, texcoord — 32 bytes, matching the format
/// ObjectMeshManager packs into GlobalMeshBuffer.
///
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),
]);
/// Empty layout for pipelines whose vertices come entirely from storage buffers.
public static GpuVertexLayout None { get; } = new([], []);
}
///
/// 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.
///
internal readonly record struct GpuShaderSet
{
internal GpuShaderSet(string name)
: this(name, ReadOnlyMemory.Empty, ReadOnlyMemory.Empty)
{
}
internal GpuShaderSet(
string name,
ReadOnlyMemory vertexSpirv,
ReadOnlyMemory 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 VertexSpirv { get; }
internal ReadOnlyMemory FragmentSpirv { get; }
internal bool HasEmbeddedSpirv => !VertexSpirv.IsEmpty;
}
/// Depth-buffer behaviour baked into a pipeline.
/// Whether depth testing is enabled at all.
/// Default depth-write state; overridable per draw via dynamic state.
/// Comparison function when testing is enabled.
internal readonly record struct GpuDepthState(bool Test, bool Write, GpuCompareOp Compare)
{
/// Standard opaque geometry: test and write, nearer wins.
public static GpuDepthState OpaqueDefault { get; } = new(Test: true, Write: true, GpuCompareOp.LessOrEqual);
/// Translucent geometry: test against existing depth but do not occlude later draws.
public static GpuDepthState TranslucentDefault { get; } = new(Test: true, Write: false, GpuCompareOp.LessOrEqual);
/// Sky and 2-D overlays: depth is irrelevant.
public static GpuDepthState Disabled { get; } = new(Test: false, Write: false, GpuCompareOp.Always);
}
///
/// The per-draw half of stencil state: the comparison, what happens at each of
/// its three outcomes, and the reference and masks it uses.
///
/// Added at slice V6l. Core Vulkan 1.3 makes ALL of these dynamic
/// (VK_DYNAMIC_STATE_STENCIL_OP, _COMPARE_MASK, _WRITE_MASK,
/// _REFERENCE). They live here as a pipeline DEFAULT and on
/// 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
/// , because that is also the
/// attachment intent.
///
/// 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.
///
internal readonly record struct GpuStencilState(
GpuCompareOp Compare,
GpuStencilOp Fail,
GpuStencilOp DepthFail,
GpuStencilOp Pass,
uint Reference,
uint CompareMask,
uint WriteMask)
{
/// GL's and Vulkan's own defaults: always pass, never write.
public static GpuStencilState Default { get; } = new(
GpuCompareOp.Always,
GpuStencilOp.Keep,
GpuStencilOp.Keep,
GpuStencilOp.Keep,
Reference: 0,
CompareMask: 0xFF,
WriteMask: 0xFF);
}
///
/// Everything a draw needs beyond its buffers: the shader pair and all fixed
/// state. Vulkan bakes this into one VkPipeline 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. is the identity used by debug
/// tooling and by the backend's own pipeline cache key.
///
internal sealed record GpuPipelineDescription
{
/// Non-zero only for a pipeline compiled for a matching multiview pass.
public uint ViewMask { get; init; }
/// Stable identifier, e.g. "mesh-opaque". Surfaced to RenderDoc and validation layers.
public required string Name { get; init; }
/// The GLSL pair this pipeline draws with.
public required GpuShaderSet Shaders { get; init; }
/// Vertex input layout; for buffer-fed geometry.
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;
/// Default cull mode; overridable per draw via .
public GpuCullMode Cull { get; init; } = GpuCullMode.Back;
///
/// 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.
///
public GpuFrontFace FrontFace { get; init; } = GpuFrontFace.CounterClockwise;
///
/// Alpha-to-coverage for foliage. Only meaningful when the pass is
/// multisampled; the backend ignores it at one sample.
///
public bool AlphaToCoverage { get; init; }
/// Whether the pipeline writes colour at all. False for depth/stencil-only prepasses.
public bool ColorWrite { get; init; } = true;
///
/// Whether the compatible dynamic-rendering pass carries a colour
/// attachment. False creates a true depth-only graphics pipeline.
///
public bool HasColorAttachment { get; init; } = true;
///
/// Whether this pipeline uses the stencil aspect at all.
///
/// 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 PortalDepthMaskRenderer stayed raw GL
/// and V4g's "stencil/depth-mask pipelines" row could not be written (plan
/// §5.5.16 defect 2).
///
/// This is the ENABLE and the attachment intent together, which is why
/// it is baked while everything in is dynamic. Vulkan
/// makes stencilTestEnable 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.
///
///
public bool StencilTest { get; init; }
///
/// Default stencil compare/op/reference/mask, re-established by
/// BindPipeline and overridable per draw through
/// . Ignored entirely when
/// is false.
///
public GpuStencilState Stencil { get; init; } = GpuStencilState.Default;
///
/// Format of the colour attachment this pipeline renders into.
///
/// Vulkan's dynamic rendering bakes the attachment format into the pipeline:
/// VkPipelineRenderingCreateInfo 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 B8G8R8A8_UNORM
/// (VulkanTextureFormatMapping.CanonicalColorAttachmentFormat) 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
/// (V4c) and
/// (V4d).
///
public GpuTextureFormat ColorFormat { get; init; } = GpuTextureFormat.Rgba8UnormRenderTarget;
///
/// 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.
///
public bool AllowColorFormatVariants { get; init; } = true;
///
/// 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.
///
public bool UsesRenderPackShaderAbi { get; init; }
/// Sample count of the passes this pipeline is used in. Must match the pass.
public int SampleCount { get; init; } = 1;
}