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 vertex attribute, matching a layout(location = N) in declaration.
internal readonly record struct GpuVertexAttribute(
uint Location,
GpuVertexFormat Format,
uint OffsetBytes);
/// Interleaved vertex layout for a single bound vertex buffer.
internal sealed record GpuVertexLayout(uint StrideBytes, ImmutableArray Attributes)
{
///
/// 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; } = new(
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(0, []);
}
///
/// Names one GLSL shader pair. The backend resolves it: the GL backend loads
/// Rendering/Shaders/{Name}.vert and .frag and compiles at startup;
/// the Vulkan backend loads the committed Rendering/Shaders/spv/{Name}.vert.spv
/// and .frag.spv produced by tools/compile-shaders.ps1. One source
/// of truth (the GLSL), two consumption paths.
///
internal readonly record struct GpuShaderSet(string Name);
/// 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);
}
///
/// 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
{
/// 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;
///
/// 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;
/// Sample count of the passes this pipeline is used in. Must match the pass.
public int SampleCount { get; init; } = 1;
}