acdream/src/AcDream.App/Rendering/TerrainModernRenderer.Rhi.cs

357 lines
15 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 DirectionalShadowReceiverPipelineState? _directionalShadowReceiver;
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,
};
IGpuPipeline pipeline = _pipeline!;
DirectionalShadowFrameBinding shadowBinding = default;
DirectionalShadowReceiverPipelineState? receiver =
_directionalShadowReceiver;
IDirectionalShadowReceiverSource? receiverSource = receiver?.Source;
bool bindingValid = receiverSource is not null
&& receiverSource.TryGetCurrentFrameBinding(frame, out shadowBinding);
if (DirectionalShadowReceiverPolicy.ShouldSelectReceiverPipeline(
encoder.Pass.Name,
receiverSource is not null,
bindingValid))
{
pipeline = receiver!.Pipeline;
}
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);
if (shadowBinding.Enabled && shadowBinding.Buffer is not null)
{
encoder.BindUniformBuffer(
GpuBindingModel.UniformDirectionalShadow,
shadowBinding.Buffer,
shadowBinding.OffsetBytes,
shadowBinding.SizeBytes);
}
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()
{
_directionalShadowReceiver?.Dispose();
_directionalShadowReceiver = null;
_pipeline?.Dispose();
_pipeline = null;
_tilingBuffer?.Dispose();
_tilingBuffer = null;
_vertexStore?.Dispose();
_vertexStore = null;
_indexStore?.Dispose();
_indexStore = null;
_globalVboCapacityBytes = 0;
_globalEboCapacityBytes = 0;
_dynamicFrameStarted = false;
_disposed = true;
}
}