acdream/src/AcDream.App/Rendering/Gpu/GpuResources.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

109 lines
4.3 KiB
C#

namespace AcDream.App.Rendering.Gpu;
/// <summary>
/// A GPU buffer. Disposal does not free immediately: every backend routes the
/// physical release through the device's retirement queue so the memory outlives
/// any frame still referencing it. That is the same contract
/// <c>GpuFrameFlightController</c> already enforces for GL names today.
/// </summary>
internal interface IGpuBuffer : IDisposable
{
string Name { get; }
long SizeBytes { get; }
GpuBufferUsage Usage { get; }
GpuMemoryResidency Residency { get; }
/// <summary>
/// Writes <paramref name="data"/> at <paramref name="offsetBytes"/>. On a
/// <see cref="GpuMemoryResidency.DeviceLocal"/> buffer this stages through a
/// transfer; on a host-writable buffer it is a direct memory write. Per-frame
/// data should not use this at all — take a ring allocation and write into it.
/// </summary>
void Upload(long offsetBytes, ReadOnlySpan<byte> data);
/// <summary>
/// Device-side copy, used by the mesh arena's grow-and-copy migration so
/// arena growth never round-trips through system memory.
/// </summary>
void CopyTo(IGpuBuffer destination, long sourceOffsetBytes, long destinationOffsetBytes, long byteCount);
/// <summary>
/// Reads back into <paramref name="destination"/>. Only valid on
/// <see cref="GpuMemoryResidency.HostReadable"/> buffers; diagnostics only.
/// </summary>
void Read(long offsetBytes, Span<byte> destination);
}
/// <summary>A sampled texture or an attachment image.</summary>
internal interface IGpuTexture : IDisposable
{
string Name { get; }
GpuTextureKind Kind { get; }
GpuTextureFormat Format { get; }
int Width { get; }
int Height { get; }
int LayerCount { get; }
int MipLevelCount { get; }
/// <summary>
/// Uploads one mip level of one array layer. <paramref name="data"/> is raw
/// texels for uncompressed formats and raw blocks for BC formats.
/// </summary>
void Upload(int mipLevel, int layer, ReadOnlySpan<byte> data);
/// <summary>
/// Fills mip levels 1..N-1 from level 0.
///
/// Explicit rather than automatic because the two backends cannot do this the
/// same way: GL calls <c>glGenerateMipmap</c>, while Vulkan blits uncompressed
/// images and CANNOT blit compressed ones. For BC formats the Vulkan backend
/// requires the caller to have supplied a CPU-built chain via
/// <see cref="Upload"/> and this call throws — the GL path's reliance on
/// driver-defined compressed-mip regeneration is the behaviour we are
/// deliberately not carrying forward.
/// </summary>
void GenerateMipChain();
}
/// <summary>Immutable sampler state. Owned and de-duplicated by the device.</summary>
internal interface IGpuSampler : IDisposable
{
GpuSamplerDescription Description { get; }
}
/// <summary>
/// A compiled shader program plus every piece of fixed pipeline state it draws
/// with. This is the type that replaces the imperative
/// <c>Enable/BlendFunc/DepthMask/CullFace</c> brackets scattered through the GL
/// renderers: state that Vulkan bakes at creation lives here, and only the state
/// core Vulkan 1.3 makes dynamic stays callable per draw
/// (<see cref="IGpuPassEncoder.SetCullMode"/> and friends).
/// </summary>
internal interface IGpuPipeline : IDisposable
{
GpuPipelineDescription Description { get; }
}
/// <summary>An offscreen render target whose colour attachment is sampleable once the pass ends.</summary>
internal interface IGpuRenderTarget : IDisposable
{
GpuRenderTargetDescription Description { get; }
/// <summary>The colour attachment, for registering into the texture table or blitting into UI.</summary>
IGpuTexture ColorTexture { get; }
}
/// <summary>
/// GPU-side timing. Backed by GL <c>TimeElapsed</c> queries or Vulkan timestamp
/// queries; results are only readable once the issuing frame has retired, so
/// <see cref="TryResolve"/> reports the most recent completed measurement rather
/// than blocking.
/// </summary>
internal interface IGpuTimerPool
{
/// <summary>True when the backend can measure GPU time at all.</summary>
bool IsSupported { get; }
/// <summary>Milliseconds measured for <paramref name="scopeName"/> in the most recent retired frame.</summary>
bool TryResolve(string scopeName, out double milliseconds);
}