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>
88 lines
3.8 KiB
C#
88 lines
3.8 KiB
C#
namespace AcDream.App.Rendering.Gpu;
|
||
|
||
/// <summary>
|
||
/// The RHI root: creates every GPU resource, owns the global texture table, and
|
||
/// drives the frame loop. One instance per graphics context, constructed during
|
||
/// composition and threaded into renderers in place of the raw <c>GL</c> handle.
|
||
///
|
||
/// Campaign V (see <c>docs/plans/2026-07-27-vulkan-campaign.md</c>) implements
|
||
/// this interface twice: first on OpenGL — behaviour-preserving, so each renderer
|
||
/// port is pixel-gated against the previous commit on the shipping backend — and
|
||
/// then on Vulkan, gated by a GL-versus-Vulkan differential. The GL
|
||
/// implementation is deleted at slice V11.
|
||
/// </summary>
|
||
internal interface IGpuDevice : IDisposable
|
||
{
|
||
GpuBackendKind Backend { get; }
|
||
|
||
GpuCapabilityRecord Capabilities { get; }
|
||
|
||
/// <summary>
|
||
/// Frame-flight-gated resource release. Resource disposal routes through here
|
||
/// so nothing is freed while a submitted frame may still reference it.
|
||
/// </summary>
|
||
IGpuResourceRetirementQueue Retirement { get; }
|
||
|
||
/// <summary>GPU timing results from retired frames.</summary>
|
||
IGpuTimerPool Timers { get; }
|
||
|
||
/// <summary>
|
||
/// A registered 1×1 opaque-white texture. Renderers that need a defined
|
||
/// fallback use this rather than assuming slot 0 means anything — an
|
||
/// unregistered <see cref="GpuTextureSlot"/> is
|
||
/// <see cref="GpuTextureSlot.Unassigned"/> and must never reach a shader.
|
||
/// </summary>
|
||
GpuTextureSlot DefaultTextureSlot { get; }
|
||
|
||
IGpuBuffer CreateBuffer(in GpuBufferDescription description);
|
||
|
||
IGpuTexture CreateTexture(in GpuTextureDescription description);
|
||
|
||
/// <summary>Creates or returns a cached sampler; sampler state is de-duplicated by value.</summary>
|
||
IGpuSampler CreateSampler(in GpuSamplerDescription description);
|
||
|
||
/// <summary>
|
||
/// Compiles and links a pipeline. Both backends build every pipeline during
|
||
/// startup, so no frame ever pays a shader-compile or state-revalidation cost.
|
||
/// </summary>
|
||
IGpuPipeline CreatePipeline(GpuPipelineDescription description);
|
||
|
||
IGpuRenderTarget CreateRenderTarget(in GpuRenderTargetDescription description);
|
||
|
||
/// <summary>
|
||
/// Publishes a (texture, sampler) pair into the global table and returns the
|
||
/// slot shaders index it by. The same texture registered with two samplers
|
||
/// occupies two slots — matching how it holds two bindless handles today.
|
||
/// </summary>
|
||
GpuTextureSlot RegisterTexture(IGpuTexture texture, IGpuSampler sampler);
|
||
|
||
/// <summary>
|
||
/// Returns a slot to the free list. The slot is not reused until the frames
|
||
/// that could still reference it have retired, so eviction (the texture and
|
||
/// mesh caches' LRU) cannot alias a live draw onto a new texture.
|
||
/// </summary>
|
||
void ReleaseTextureSlot(GpuTextureSlot slot);
|
||
|
||
/// <summary>Opens the next frame, waiting for its flight slot to retire first.</summary>
|
||
IGpuFrame BeginFrame();
|
||
|
||
/// <summary>
|
||
/// Defers <paramref name="action"/> to the render thread. Replaces
|
||
/// <c>OpenGLGraphicsDevice.QueueGLAction</c>; loader threads use it to hand
|
||
/// GPU work back to the thread that owns the context or queue.
|
||
/// </summary>
|
||
void QueueDeviceAction(Action action);
|
||
|
||
/// <summary>Runs queued device actions. Called once per frame from the render thread.</summary>
|
||
void ProcessDeviceActions();
|
||
|
||
/// <summary>
|
||
/// Reads the presented image back as tightly packed top-left-origin RGBA8.
|
||
/// This is the seam the automated screenshot gates already use, so the
|
||
/// pixel-comparison tooling is unaffected by the backend swap.
|
||
/// </summary>
|
||
byte[] CaptureBackbuffer(int width, int height);
|
||
|
||
/// <summary>Blocks until all submitted work completes and every pending retirement has run.</summary>
|
||
void WaitIdle();
|
||
}
|