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; }