feat(render): Campaign V slice V0 — pin the Vulkan-shaped RHI contract
Campaign V migrates the renderer from OpenGL 4.3+extensions to a single
Vulkan 1.3 backend on Windows x64 and Linux x64, then deletes the GL path.
Motivation is compatibility and efficiency, not rescue: mandatory
GL_ARB_bindless_texture is the exact floor that parked Slice L (Mesa
D3D12/llvmpipe lack it) while Vulkan descriptor indexing is core, and
per-frame data can be written straight into mapped memory rather than
copied through BufferSubData.
V0 pins the contract every later slice codes against. Nothing consumes it
yet, so this commit changes no runtime behavior.
The seam is a minimal Vulkan-shaped RHI implemented FIRST on GL. That
ordering is the point: the twelve renderers then port one at a time under a
strict pixel gate on the still-shipping backend, so a divergence is
attributed to one slice instead of surfacing at a big-bang integration.
Duplicating renderers per backend was rejected because WbDrawDispatcher is
4,449 lines holding only ~62 GL call sites — the API surface is small and
the retail-fidelity CPU logic is large, and forking the latter is how subtle
regressions enter.
Contract highlights:
- GpuBindingModel pins set/binding numbers dual-legal for GL and Vulkan
GLSL. Storage bindings 0-8 keep today's shader numbering; UBOs move to
their own set, which resolves the binding=1 collision GL only tolerates
because it keeps SSBO and UBO tables separate.
- GpuRingAllocation is a ref struct replacing every per-frame
BufferSubData; the compiler forbids outliving the owning frame.
- GpuTextureSlot replaces bindless handles. Unassigned is a loud
uint.MaxValue sentinel rather than a silent resolve to slot 0 — the
failure mode behind the magenta 1x1 UI placeholder bug. Renderers
needing a fallback take the device's really-registered default slot.
- Renderers always speak GL winding/viewport conventions; the Vulkan
backend compensates with a negative viewport height in exactly one
mapping function.
Verified while writing the plan: acdream's cameras already build
[0,1]-NDC projections (PortalProjection.cs:12-13), which is Vulkan's
convention. No projection rework is needed and depth precision improves,
at the cost of shifted z-fight patterns — the one pre-approved divergence
class, registered per instance at V7.
Gate: Release build green; App suite 3,785 passed / 3 skipped (3,763
baseline plus 22 new contract tests). Note for later slices, recorded in
the plan: run the suite in Release. LandblockBuildOriginTests'
far-strip test asserts behavior that LandblockStreamer.cs:505 deliberately
turns into a loud Debug.Assert in Debug builds, so a Debug run shows one
pre-existing failure that is not a regression.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
f6275f4501
commit
621b16364b
17 changed files with 2577 additions and 0 deletions
113
src/AcDream.App/Rendering/Gpu/GpuPipelineDescription.cs
Normal file
113
src/AcDream.App/Rendering/Gpu/GpuPipelineDescription.cs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
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,
|
||||
UByte4Normalized,
|
||||
}
|
||||
|
||||
/// <summary>One vertex attribute, matching a <c>layout(location = N) in</c> declaration.</summary>
|
||||
internal readonly record struct GpuVertexAttribute(
|
||||
uint Location,
|
||||
GpuVertexFormat Format,
|
||||
uint OffsetBytes);
|
||||
|
||||
/// <summary>Interleaved vertex layout for a single bound vertex buffer.</summary>
|
||||
internal sealed record GpuVertexLayout(uint StrideBytes, ImmutableArray<GpuVertexAttribute> Attributes)
|
||||
{
|
||||
/// <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; } = new(
|
||||
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(0, []);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Names one GLSL shader pair. The backend resolves it: the GL backend loads
|
||||
/// <c>Rendering/Shaders/{Name}.vert</c> and <c>.frag</c> and compiles at startup;
|
||||
/// the Vulkan backend loads the committed <c>Rendering/Shaders/spv/{Name}.vert.spv</c>
|
||||
/// and <c>.frag.spv</c> produced by <c>tools/compile-shaders.ps1</c>. One source
|
||||
/// of truth (the GLSL), two consumption paths.
|
||||
/// </summary>
|
||||
internal readonly record struct GpuShaderSet(string Name);
|
||||
|
||||
/// <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>
|
||||
/// 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>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>Sample count of the passes this pipeline is used in. Must match the pass.</summary>
|
||||
public int SampleCount { get; init; } = 1;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue