acdream/src/AcDream.App/Rendering/Gpu/GpuResourceDescriptions.cs
Erik 621b16364b 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>
2026-07-27 14:26:26 +02:00

117 lines
5.2 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.

namespace AcDream.App.Rendering.Gpu;
/// <summary>
/// Creation parameters for a GPU buffer. <paramref name="Name"/> is not
/// cosmetic: the Vulkan backend publishes it through <c>VK_EXT_debug_utils</c>
/// so RenderDoc captures and validation-layer messages name our objects.
/// </summary>
/// <param name="Name">Stable identifier, e.g. <c>"mesh-arena-vertex"</c>.</param>
/// <param name="SizeBytes">Allocation size. Growth is a create-copy-retire cycle, never a resize.</param>
/// <param name="Usage">Every way the buffer will be consumed.</param>
/// <param name="Residency">Where the memory lives and whether the CPU may write it directly.</param>
internal readonly record struct GpuBufferDescription(
string Name,
long SizeBytes,
GpuBufferUsage Usage,
GpuMemoryResidency Residency);
/// <summary>Creation parameters for a sampled texture or a render-target image.</summary>
/// <param name="Name">Stable identifier for debug tooling.</param>
/// <param name="Kind">2D, or the 2D array every world material uses.</param>
/// <param name="Format">Pixel format; BC formats are uploaded as compressed blocks.</param>
/// <param name="Width">Width in texels of mip level 0.</param>
/// <param name="Height">Height in texels of mip level 0.</param>
/// <param name="LayerCount">Array layers; 1 for <see cref="GpuTextureKind.Texture2D"/>.</param>
/// <param name="MipLevelCount">
/// Levels to allocate. 1 disables mipping. The backend never silently generates
/// mips: <see cref="IGpuTexture.GenerateMipChain"/> is an explicit call, because
/// Vulkan cannot blit-generate compressed mips and must take a CPU-built chain.
/// </param>
internal readonly record struct GpuTextureDescription(
string Name,
GpuTextureKind Kind,
GpuTextureFormat Format,
int Width,
int Height,
int LayerCount,
int MipLevelCount);
/// <summary>
/// Sampler state. The set of distinct samplers acdream uses is tiny (wrap/clamp
/// × nearest/linear), which is what makes a combined image-sampler descriptor
/// table practical: a texture registered twice with different samplers simply
/// occupies two table slots, exactly as it holds two bindless handles today.
/// </summary>
internal readonly record struct GpuSamplerDescription(
GpuFilter MinFilter,
GpuFilter MagFilter,
GpuMipFilter MipFilter,
GpuAddressMode AddressU,
GpuAddressMode AddressV,
float MaxAnisotropy)
{
/// <summary>Trilinear repeat — the default for world materials.</summary>
public static GpuSamplerDescription WorldRepeat { get; } = new(
GpuFilter.Linear,
GpuFilter.Linear,
GpuMipFilter.Linear,
GpuAddressMode.Repeat,
GpuAddressMode.Repeat,
MaxAnisotropy: 1f);
/// <summary>Trilinear clamped — atlas pages and anything whose edges must not wrap.</summary>
public static GpuSamplerDescription WorldClamp { get; } = new(
GpuFilter.Linear,
GpuFilter.Linear,
GpuMipFilter.Linear,
GpuAddressMode.ClampToEdge,
GpuAddressMode.ClampToEdge,
MaxAnisotropy: 1f);
/// <summary>Unfiltered clamped — retail UI icons and the composited 32×32 item art.</summary>
public static GpuSamplerDescription UiNearest { get; } = new(
GpuFilter.Nearest,
GpuFilter.Nearest,
GpuMipFilter.None,
GpuAddressMode.ClampToEdge,
GpuAddressMode.ClampToEdge,
MaxAnisotropy: 1f);
}
/// <summary>An offscreen colour(+depth) bundle: paperdoll, creature appraisal, portal masking.</summary>
/// <param name="Name">Stable identifier for debug tooling.</param>
/// <param name="Width">Colour attachment width in pixels.</param>
/// <param name="Height">Colour attachment height in pixels.</param>
/// <param name="ColorFormat">Colour attachment format.</param>
/// <param name="DepthFormat">Depth/stencil format, or null for a colour-only target.</param>
/// <param name="SampleCount">1 for single-sampled. Offscreen targets stay single-sampled.</param>
internal readonly record struct GpuRenderTargetDescription(
string Name,
int Width,
int Height,
GpuTextureFormat ColorFormat,
GpuTextureFormat? DepthFormat,
int SampleCount);
/// <summary>
/// A slot in the device's global texture table — the backend-neutral replacement
/// for a 64-bit <c>ARB_bindless_texture</c> handle. Renderers write
/// <see cref="Index"/> into batch data; the shader indexes the descriptor array
/// (Vulkan) or the uvec2 handle buffer (GL) with it.
///
/// <see cref="Unassigned"/> is a loud sentinel, never a usable slot. It exists so
/// an unset index is an assertable programming error rather than a silent
/// resolve to slot 0 — the failure mode that produced the magenta 1×1 UI
/// placeholder bug. Renderers that genuinely need a fallback ask the device for
/// <see cref="IGpuDevice.DefaultTextureSlot"/>, which is a real registered texture.
/// </summary>
internal readonly record struct GpuTextureSlot(uint Index)
{
/// <summary>Sentinel for "no texture assigned". Must never reach a shader.</summary>
public static GpuTextureSlot Unassigned { get; } = new(uint.MaxValue);
public bool IsAssigned => Index != uint.MaxValue;
public override string ToString() =>
IsAssigned ? $"slot#{Index}" : "slot#unassigned";
}