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; } }