acdream/src/AcDream.App/Rendering/TerrainModernRenderer.Rhi.cs
Erik 8a7a0837e1 feat(render): Vulkan campaign V11 step 2 — delete the OpenGL backend
Vulkan is the sole, user-signed-off backend (V10 landed) and step 1
already removed ImGui/Studio/DevTools. This step deletes the GL
rendering backend itself: every Gpu/Gl/** implementation, the Wb
ManagedGL*/GLHelpers/GLSLShader/GLStateScope/RenderStateCache/
BindlessSupport family, Shader/ShaderProgramConstruction/SamplerCache,
RenderBootstrap, and RenderFrameGlStateController.

GameWindow.cs's Run()/CreateGraphics()/CreateBackbufferReader()/
OnLoad() collapse to their Vulkan-only arm; GameWindowGraphics loses
its OpenGlGameWindowGraphics subclass. RuntimeOptions.RenderBackend and
RenderBackendKind (incl. the Gl member of GpuBackendKind) are gone —
there is nothing left to select between. The five world-draw dual-arm
renderers (WbDrawDispatcher, EnvCellRenderer, TerrainModernRenderer,
ParticleRenderer, SkyRenderer) and the composition roots
(WorldRenderComposition, HostInputCameraComposition,
LivePresentationComposition, FrameRootComposition) collapse to their
RHI-only arm. GL-only diagnostic properties with a live external reader
(DynamicBufferCount and friends) simplify to a documented `=> 0`/no-op
rather than disappearing, since the reader is out of this commit's
scope.

A few GL-flavored mechanisms turned out to be backend-neutral once
isolated: GlConstructionCleanupLedger is renamed
ResourceConstructionCleanupLedger (exception-chain walking has nothing
to do with GL), and GlfwNativePlatformProbe moved out of the otherwise
GL-only GraphicalCapabilityRecord.cs into
GraphicalWindowBackendSelection.cs before the rest of that file was
deleted.

Test files with no surviving subject are deleted outright
(GraphicalCapabilityRequirementsTests, ShaderProgramConstructionTests,
PortalDepthShaderParityTests, TextureCacheBindlessTests,
TextRendererFailureSafetyTests, ClipFrameUploadTests, every
Gpu/Gl/*Tests, GlTextureOwnershipTests, RenderFrameGlStateControllerTests);
others get their dead GL-only members trimmed while their live
assertions stay (ClipFrameLayoutTests' MeshClipSsboBinding check now
reads GpuBindingModel.StorageClipRegions, the same binding index under
its new backend-neutral name; GpuResourceRetirementTransactionTests
drops its OpenGLGraphicsDevice-subclassing test double and the two GL
queue tests it existed for). EnvCellRendererTests' construction helper
now builds a real ObjectMeshManager via VulkanMeshPipelineDevice
instead of passing null through a null-forgiving operator, since the
RHI constructor never tolerated a null mesh manager and the old GL
constructor (which did) is gone.

Deferred to the next two steps, deliberately not touched here: the
Silk.NET.OpenGL/.Extensions.ARB package references, IMeshPipelineDevice.Gl
(WbMeshAdapter's GL? threading stays in place), Chorizite.Core's stale
csproj comment (the package itself is still load-bearing —
TextureFormat and friends are used well beyond the deleted
ManagedGLUniformBuffer), and the CI/gate scripts.

Build: `dotnet build AcDream.slnx -c Release` — 0 warnings, 0 errors.
Tests: full-solution `dotnet test` green across every project
(App.Tests 3937/3940 + 3 skips, Core.Tests 3296/3298 + 2 skips, all
others 100%); the 2 App.Tests names that flake under full-suite
parallel execution (#250-family, documented pre-existing) pass in
isolation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 02:19:53 +02:00

331 lines
14 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.

using System.Collections.Immutable;
using System.Numerics;
using System.Runtime.InteropServices;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Wb;
using AcDream.Core.Terrain;
namespace AcDream.App.Rendering;
/// <summary>
/// Campaign V slice V6j: terrain's RHI submission arm.
///
/// <para>This is V4d-2's content, re-landed as a SECOND arm rather than as a
/// replacement. §5.5.6 selected option (B) after NVIDIA rendered the V4c binary
/// 10/10 and AMD's GL stack did not: GL keeps its raw world path through to V10
/// as a documented, scoped fork confined to the submission seam, and the RHI
/// world path ships on Vulkan. So every GL statement in the sibling file is
/// untouched, and everything here runs only when there is no GL context.</para>
///
/// <para>Three things differ from V4d-2, each because the tree moved under it.
/// The texture slots come from <c>TerrainAtlas</c>'s device table (V4t) rather
/// than a per-renderer bindless table, so there is no binding-9 table to bind at
/// all — the Vulkan texture table is set 2 and the encoder binds it. The tiling
/// block is the shared <c>TerrainTextureTilingTable</c> constants (V6f-2) rather
/// than locals. And the pass is BORROWED from <see cref="IWorldPassScope"/>
/// rather than opened, because the frame's one backbuffer pass resolves and a
/// second pass could not load what it left.</para>
/// </summary>
public sealed unsafe partial class TerrainModernRenderer
{
/// <summary>
/// Terrain's vertex layout: the same 40-byte record <c>ConfigureVao</c>
/// describes with <c>glVertexAttribPointer</c>/<c>glVertexAttribIPointer</c>.
///
/// <para>Locations 25 are <see cref="GpuVertexFormat.UByte4UInt"/>, not
/// <c>UByte4Normalized</c>. They are <c>uvec4</c> in the shader and carry
/// terrain-type, road and split-direction codes; normalising them would not
/// be an approximation, it would be garbage.</para>
/// </summary>
internal static readonly GpuVertexLayout TerrainVertexLayout = GpuVertexLayout.Interleaved(
strideBytes: VertexSize,
ImmutableArray.Create(
new GpuVertexAttribute(0, GpuVertexFormat.Float3, 0),
new GpuVertexAttribute(1, GpuVertexFormat.Float3, 12),
new GpuVertexAttribute(2, GpuVertexFormat.UByte4UInt, 24),
new GpuVertexAttribute(3, GpuVertexFormat.UByte4UInt, 28),
new GpuVertexAttribute(4, GpuVertexFormat.UByte4UInt, 32),
new GpuVertexAttribute(5, GpuVertexFormat.UByte4UInt, 36)));
private readonly IGpuDevice? _device;
private readonly ICurrentGpuFrameSource? _frames;
private readonly IWorldPassScope? _scope;
private IGpuPipeline? _pipeline;
private IGpuBuffer? _vertexStore;
private IGpuBuffer? _indexStore;
private IGpuBuffer? _tilingBuffer;
/// <summary>
/// The RHI arm's constructor. No GL context, no <c>Shader</c>, no
/// <c>BindlessSupport</c>: the pipeline compiles <c>terrain_modern</c> from
/// the committed SPIR-V and the atlas's slots index the device's one table.
/// </summary>
internal TerrainModernRenderer(
IGpuDevice device,
ICurrentGpuFrameSource frames,
IWorldPassScope scope,
TerrainAtlas atlas,
IGpuResourceRetirementQueue resourceRetirement,
int initialSlotCapacity = 64)
{
_device = device ?? throw new ArgumentNullException(nameof(device));
_frames = frames ?? throw new ArgumentNullException(nameof(frames));
_scope = scope ?? throw new ArgumentNullException(nameof(scope));
_atlas = atlas ?? throw new ArgumentNullException(nameof(atlas));
ArgumentNullException.ThrowIfNull(resourceRetirement);
_retirementLedger = new GpuRetirementLedger(resourceRetirement);
_alloc = new GpuRetiredTerrainSlotAllocator(initialSlotCapacity, resourceRetirement);
_slots = new SlotData?[initialSlotCapacity];
_pipeline = device.CreatePipeline(new GpuPipelineDescription
{
Name = "terrain",
Shaders = new GpuShaderSet("terrain_modern"),
VertexLayout = TerrainVertexLayout,
Topology = GpuPrimitiveTopology.TriangleList,
Blend = GpuBlendMode.None,
// GL_LESS, not the contract's LessOrEqual default: the world frame
// runs under GL_LESS and terrain never called glDepthFunc, so it
// inherited it. LessOrEqual would change which of two coplanar retail
// surfaces wins — visible exactly where terrain meets roads and
// building footings, which is what zFightTerrainAdjust is about.
Depth = new GpuDepthState(Test: true, Write: true, GpuCompareOp.Less),
// #108-residual: retail terrain is SINGLE-SIDED. See the GL arm's
// Draw for the full reasoning; this bakes the same triple.
Cull = GpuCullMode.Back,
FrontFace = GpuFrontFace.CounterClockwise,
AlphaToCoverage = false,
ColorWrite = true,
SampleCount = scope.SampleCount,
});
AllocateRhiBuffers(initialSlotCapacity);
}
private void AllocateRhiBuffers(int capacitySlots)
{
long vertexBytes = checked((long)capacitySlots * VertsPerLandblock * VertexSize);
long indexBytes = checked((long)capacitySlots * IndicesPerLandblock * IndexSize);
IGpuDevice device = RequireDevice();
_vertexStore = device.CreateBuffer(new GpuBufferDescription(
"terrain-vertices",
vertexBytes,
GpuBufferUsage.Vertex
| GpuBufferUsage.TransferSource
| GpuBufferUsage.TransferDestination,
GpuMemoryResidency.DeviceLocal));
_globalVboCapacityBytes = vertexBytes;
_indexStore = device.CreateBuffer(new GpuBufferDescription(
"terrain-indices",
indexBytes,
GpuBufferUsage.Index
| GpuBufferUsage.TransferSource
| GpuBufferUsage.TransferDestination,
GpuMemoryResidency.DeviceLocal));
_globalEboCapacityBytes = indexBytes;
}
/// <summary>
/// Grow-and-copy, device-side. <see cref="IGpuBuffer.CopyTo"/> keeps resident
/// landblock meshes from round-tripping through system memory, exactly as the
/// GL arm's <c>glCopyBufferSubData</c> does.
/// </summary>
private void EnsureRhiCapacity(int newCapacitySlots)
{
if (newCapacitySlots <= _alloc.Capacity)
return;
long vertexBytes = checked((long)newCapacitySlots * VertsPerLandblock * VertexSize);
long indexBytes = checked((long)newCapacitySlots * IndicesPerLandblock * IndexSize);
IGpuDevice device = RequireDevice();
IGpuBuffer oldVertices = RequireVertexStore();
IGpuBuffer oldIndices = RequireIndexStore();
IGpuBuffer newVertices = device.CreateBuffer(new GpuBufferDescription(
"terrain-vertices",
vertexBytes,
GpuBufferUsage.Vertex
| GpuBufferUsage.TransferSource
| GpuBufferUsage.TransferDestination,
GpuMemoryResidency.DeviceLocal));
IGpuBuffer newIndices;
try
{
newIndices = device.CreateBuffer(new GpuBufferDescription(
"terrain-indices",
indexBytes,
GpuBufferUsage.Index
| GpuBufferUsage.TransferSource
| GpuBufferUsage.TransferDestination,
GpuMemoryResidency.DeviceLocal));
}
catch
{
newVertices.Dispose();
throw;
}
oldVertices.CopyTo(newVertices, 0, 0, _globalVboCapacityBytes);
oldIndices.CopyTo(newIndices, 0, 0, _globalEboCapacityBytes);
_vertexStore = newVertices;
_indexStore = newIndices;
_globalVboCapacityBytes = vertexBytes;
_globalEboCapacityBytes = indexBytes;
// Dispose routes the physical free through the device's retirement queue,
// so the old arena outlives every frame that can still reference it.
oldVertices.Dispose();
oldIndices.Dispose();
var grownSlots = new SlotData?[newCapacitySlots];
Array.Copy(_slots, grownSlots, _slots.Length);
_slots = grownSlots;
_alloc.GrowTo(newCapacitySlots);
}
private void UploadRhiLandblock(
int slot,
TerrainVertex[] bakedVerts,
uint[] bakedIndices)
{
RequireVertexStore().Upload(
(long)slot * VertsPerLandblock * VertexSize,
MemoryMarshal.AsBytes<TerrainVertex>(bakedVerts));
RequireIndexStore().Upload(
(long)slot * IndicesPerLandblock * IndexSize,
MemoryMarshal.AsBytes<uint>(bakedIndices));
}
/// <summary>
/// Records terrain's multi-draw into the borrowed world pass.
///
/// <para>Order matters twice. <c>BindPipeline</c> re-issues the pipeline's own
/// cull/front-face/depth-write defaults, so anything dynamic has to come
/// after it. And the frame-global sections — SceneLighting and the terrain
/// clip block — are bound HERE, after this renderer's own binds, because its
/// own binds are what select the descriptor scope those sections must land in
/// (plan §5.5.14 item 2).</para>
/// </summary>
private void DrawRhi(Matrix4x4 viewProjection, int drawCount)
{
IWorldPassScope scope = _scope!;
IGpuPassEncoder encoder = scope.RequireEncoder();
IGpuFrame frame = _frames!.CurrentFrame
?? throw new InvalidOperationException(
"TerrainModernRenderer requires an open IGpuFrame (see GpuDeviceFrameLifetime).");
// V6i-2's backend-neutral atlas registers both slots at construction, so
// there is no per-draw acquire-and-reregister step and no binding-9 table
// to flush — the Vulkan texture table is set 2 and the encoder binds it.
(GpuTextureSlot terrainSlot, GpuTextureSlot alphaSlot) = _atlas.TextureSlots;
var pushConstants = new GpuPushConstants
{
ViewProjection = viewProjection,
DrawIdOffset = 0,
LightingMode = 0,
RenderPass = 0,
LightDebug = 0,
TextureIndexA = terrainSlot.Index,
TextureIndexB = alphaSlot.Index,
ParamA = 0f,
ParamB = 0f,
};
encoder.BindPipeline(_pipeline!);
encoder.SetPushConstants(in pushConstants);
encoder.BindVertexBuffer(0, RequireVertexStore(), 0);
encoder.BindIndexBuffer(RequireIndexStore(), 0, GpuIndexType.UInt32);
BindTilingTable(encoder);
WorldFrameSectionBinding.BindSceneLighting(encoder, scope.Sections, frame);
WorldFrameSectionBinding.BindTerrainClip(encoder, scope.Sections, frame);
GpuRingAllocation commands = frame.AllocateRing(
drawCount * sizeof(DrawElementsIndirectCommand),
GpuRingUsage.Indirect);
MemoryMarshal.AsBytes(_deicScratch.AsSpan(0, drawCount))
.CopyTo(commands.Data);
encoder.MultiDrawIndexedIndirect(
commands.Buffer,
commands.OffsetBytes,
(uint)drawCount,
(uint)sizeof(DrawElementsIndirectCommand));
}
/// <summary>
/// Binds the immutable 36-entry tiling table. Long-lived and written once, so
/// its range never moves — which also keeps it out of the descriptor-scope
/// key's moving parts.
/// </summary>
private void BindTilingTable(IGpuPassEncoder encoder)
{
if (_tilingBuffer is null)
{
if (_atlas.TilingByLayer.Count != TerrainTextureTilingTable.LayerCapacity)
{
throw new InvalidOperationException(
$"Terrain tiling table has {_atlas.TilingByLayer.Count} entries; " +
$"expected {TerrainTextureTilingTable.LayerCapacity}.");
}
Span<byte> block = stackalloc byte[TerrainTextureTilingTable.UniformBufferBytes];
block.Clear();
for (int i = 0; i < TerrainTextureTilingTable.LayerCapacity; i++)
{
BitConverter.TryWriteBytes(
block[(i * TerrainTextureTilingTable.UniformElementStrideBytes)..],
_atlas.TilingByLayer[i]);
}
IGpuBuffer buffer = RequireDevice().CreateBuffer(new GpuBufferDescription(
"terrain-tiling",
TerrainTextureTilingTable.UniformBufferBytes,
GpuBufferUsage.Uniform | GpuBufferUsage.TransferDestination,
GpuMemoryResidency.DeviceLocal));
try
{
buffer.Upload(0, block);
}
catch
{
buffer.Dispose();
throw;
}
_tilingBuffer = buffer;
}
encoder.BindUniformBuffer(
GpuBindingModel.UniformTerrainTiling,
_tilingBuffer,
0,
TerrainTextureTilingTable.UniformBufferBytes);
}
private IGpuDevice RequireDevice() =>
_device ?? throw new InvalidOperationException(
"TerrainModernRenderer's RHI arm was reached without an IGpuDevice.");
private IGpuBuffer RequireVertexStore() =>
_vertexStore ?? throw new InvalidOperationException(
"The terrain vertex arena has not been created.");
private IGpuBuffer RequireIndexStore() =>
_indexStore ?? throw new InvalidOperationException(
"The terrain index arena has not been created.");
private void DisposeRhi()
{
_pipeline?.Dispose();
_pipeline = null;
_tilingBuffer?.Dispose();
_tilingBuffer = null;
_vertexStore?.Dispose();
_vertexStore = null;
_indexStore?.Dispose();
_indexStore = null;
_globalVboCapacityBytes = 0;
_globalEboCapacityBytes = 0;
_dynamicFrameStarted = false;
_disposed = true;
}
}