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;
///
/// Campaign V slice V6j: terrain's RHI submission arm.
///
/// 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.
///
/// Three things differ from V4d-2, each because the tree moved under it.
/// The texture slots come from TerrainAtlas'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 TerrainTextureTilingTable constants (V6f-2) rather
/// than locals. And the pass is BORROWED from
/// rather than opened, because the frame's one backbuffer pass resolves and a
/// second pass could not load what it left.
///
public sealed unsafe partial class TerrainModernRenderer
{
///
/// Terrain's vertex layout: the same 40-byte record ConfigureVao
/// describes with glVertexAttribPointer/glVertexAttribIPointer.
///
/// Locations 2–5 are , not
/// UByte4Normalized. They are uvec4 in the shader and carry
/// terrain-type, road and split-direction codes; normalising them would not
/// be an approximation, it would be garbage.
///
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;
///
/// The RHI arm's constructor. No GL context, no Shader, no
/// BindlessSupport: the pipeline compiles terrain_modern from
/// the committed SPIR-V and the atlas's slots index the device's one table.
///
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;
}
///
/// Grow-and-copy, device-side. keeps resident
/// landblock meshes from round-tripping through system memory, exactly as the
/// GL arm's glCopyBufferSubData does.
///
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(bakedVerts));
RequireIndexStore().Upload(
(long)slot * IndicesPerLandblock * IndexSize,
MemoryMarshal.AsBytes(bakedIndices));
}
///
/// Records terrain's multi-draw into the borrowed world pass.
///
/// Order matters twice. BindPipeline 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).
///
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));
}
///
/// 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.
///
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 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;
}
}