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>
331 lines
14 KiB
C#
331 lines
14 KiB
C#
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 2–5 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;
|
||
}
|
||
}
|