using System.Numerics; using AcDream.App.Rendering.Wb; using AcDream.Core.Terrain; using Silk.NET.OpenGL; namespace AcDream.App.Rendering; /// /// Phase N.5b modern terrain dispatcher. Single global VBO/EBO with a slot /// allocator (one slot per landblock, 384 verts × 40 bytes = 15,360 bytes /// per slot). Per-frame: build a DrawElementsIndirectCommand array from /// visible slots, upload, dispatch via glMultiDrawElementsIndirect. Atlas /// textures bound via bindless handles set per-frame as sampler uniforms. /// /// Total ~6-8 GL calls per frame for terrain regardless of visible /// landblock count. /// public sealed unsafe class TerrainModernRenderer : IDisposable { // VertsPerLandblock MUST stay divisible by 6 — terrain_modern.vert uses // `gl_VertexID % 6` to pick the cell-corner index (BL/BR/TR/TL), and // because we bake `slot * VertsPerLandblock` into indices CPU-side and // pass BaseVertex=0 to MultiDrawElementsIndirect, gl_VertexID becomes // `slot * VertsPerLandblock + local_index`. The shader's modulo-6 only // reduces to `local_index % 6` because 384 is a multiple of 6. Changing // either constant without auditing the shader will silently mis-render. private const int VertsPerLandblock = LandblockMesh.VerticesPerLandblock; // 384 (= 64 cells * 6 verts) private const int IndicesPerLandblock = VertsPerLandblock; private const int VertexSize = 40; // sizeof(TerrainVertex) private const int IndexSize = sizeof(uint); private const float LandblockSize = LandblockMesh.LandblockSize; // 192 private readonly GL _gl; private readonly BindlessSupport _bindless; private readonly Shader _shader; private readonly TerrainAtlas _atlas; /// A.5 T22.5: exposes the terrain atlas so callers can update /// anisotropic level mid-session via . public TerrainAtlas Atlas => _atlas; private readonly TerrainSlotAllocator _alloc; // Per-slot live data (index by slot integer; null entries are unused slots). private SlotData?[] _slots; // Reverse map: landblockId -> slot, for RemoveLandblock and replacement. private readonly Dictionary _idToSlot = new(); // GPU buffers. private uint _globalVao; private uint _globalVbo; private uint _globalEbo; private uint _indirectBuffer; private int _indirectCapacity; // Cached uvec2-handle uniform locations (matrix uniforms are set by name via Shader.SetMatrix4). private int _uTerrainHandleLoc; private int _uAlphaHandleLoc; // Reusable per-frame buffers. private readonly List _visibleSlots = new(); private DrawElementsIndirectCommand[] _deicScratch = Array.Empty(); // Diag. public int LoadedSlots => _alloc.LoadedCount; public int VisibleSlots => _visibleSlots.Count; public int CapacitySlots => _alloc.Capacity; public TerrainModernRenderer( GL gl, BindlessSupport bindless, Shader shader, TerrainAtlas atlas, int initialSlotCapacity = 64) { _gl = gl; _bindless = bindless; _shader = shader; _atlas = atlas; _alloc = new TerrainSlotAllocator(initialSlotCapacity); _slots = new SlotData?[initialSlotCapacity]; _uTerrainHandleLoc = _gl.GetUniformLocation(_shader.Program, "uTerrainHandle"); _uAlphaHandleLoc = _gl.GetUniformLocation(_shader.Program, "uAlphaHandle"); _globalVao = _gl.GenVertexArray(); _globalVbo = _gl.GenBuffer(); _globalEbo = _gl.GenBuffer(); AllocateGpuBuffers(initialSlotCapacity); ConfigureVao(); _indirectBuffer = _gl.GenBuffer(); } /// /// Two-tier streaming entry point. Accepts a prebuilt mesh from /// built on the worker /// thread, together with the world-space origin computed by the caller /// (render-thread GameWindow derives it from landblockId + liveCenterX/Y). /// /// Delegates to /// so both paths share one upload path. Per Phase A.5 spec T15. /// public void AddLandblockWithMesh(uint landblockId, LandblockMeshData meshData, Vector3 worldOrigin) => AddLandblock(landblockId, meshData, worldOrigin); public void AddLandblock(uint landblockId, LandblockMeshData meshData, Vector3 worldOrigin) { ArgumentNullException.ThrowIfNull(meshData); if (meshData.Vertices.Length != VertsPerLandblock) throw new ArgumentException( $"Expected {VertsPerLandblock} vertices, got {meshData.Vertices.Length}", nameof(meshData)); if (meshData.Indices.Length != IndicesPerLandblock) throw new ArgumentException( $"Expected {IndicesPerLandblock} indices, got {meshData.Indices.Length}", nameof(meshData)); if (_idToSlot.ContainsKey(landblockId)) RemoveLandblock(landblockId); int slot = _alloc.Allocate(out var needsGrow); if (needsGrow) { int newCap = Math.Max(_alloc.Capacity * 2, slot + 1); EnsureCapacity(newCap); } // Bake worldOrigin into vertex positions; capture min/max Z for AABB. var bakedVerts = new TerrainVertex[VertsPerLandblock]; float zMin = float.MaxValue, zMax = float.MinValue; for (int i = 0; i < VertsPerLandblock; i++) { var v = meshData.Vertices[i]; var worldPos = v.Position + worldOrigin; bakedVerts[i] = new TerrainVertex(worldPos, v.Normal, v.Data0, v.Data1, v.Data2, v.Data3); if (worldPos.Z < zMin) zMin = worldPos.Z; if (worldPos.Z > zMax) zMax = worldPos.Z; } if (zMin == float.MaxValue) { zMin = 0f; zMax = 0f; } // Bake baseVertex into indices on the CPU side (driver-portable pattern). uint baseVertex = (uint)(slot * VertsPerLandblock); var bakedIndices = new uint[IndicesPerLandblock]; for (int i = 0; i < IndicesPerLandblock; i++) bakedIndices[i] = meshData.Indices[i] + baseVertex; // glBufferSubData into the slot's VBO + EBO regions. nint vboByteOffset = (nint)(slot * VertsPerLandblock * VertexSize); nint eboByteOffset = (nint)(slot * IndicesPerLandblock * IndexSize); _gl.BindBuffer(BufferTargetARB.ArrayBuffer, _globalVbo); fixed (TerrainVertex* p = bakedVerts) { _gl.BufferSubData(BufferTargetARB.ArrayBuffer, vboByteOffset, (nuint)(VertsPerLandblock * VertexSize), p); } _gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0); _gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, _globalEbo); fixed (uint* p = bakedIndices) { _gl.BufferSubData(BufferTargetARB.ElementArrayBuffer, eboByteOffset, (nuint)(IndicesPerLandblock * IndexSize), p); } _gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, 0); _slots[slot] = new SlotData { LandblockId = landblockId, WorldOrigin = worldOrigin, FirstIndex = (uint)(slot * IndicesPerLandblock), IndexCount = IndicesPerLandblock, AabbMin = new Vector3(worldOrigin.X, worldOrigin.Y, zMin), AabbMax = new Vector3(worldOrigin.X + LandblockSize, worldOrigin.Y + LandblockSize, zMax), }; _idToSlot[landblockId] = slot; } public void RemoveLandblock(uint landblockId) { if (!_idToSlot.TryGetValue(landblockId, out var slot)) return; _idToSlot.Remove(landblockId); _slots[slot] = null; _alloc.Free(slot); // No GPU clear: the per-frame DEIC array won't reference this slot. } public void Draw(ICamera camera, FrustumPlanes? frustum = null, uint? neverCullLandblockId = null) { if (_alloc.LoadedCount == 0) return; // Build visible slot list with per-slot frustum cull. _visibleSlots.Clear(); for (int slot = 0; slot < _slots.Length; slot++) { var data = _slots[slot]; if (data is null) continue; if (frustum is not null && data.LandblockId != neverCullLandblockId) { if (!FrustumCuller.IsAabbVisible(frustum.Value, data.AabbMin, data.AabbMax)) continue; } _visibleSlots.Add(slot); } if (_visibleSlots.Count == 0) return; // Build DEIC array. if (_deicScratch.Length < _visibleSlots.Count) _deicScratch = new DrawElementsIndirectCommand[Math.Max(_visibleSlots.Count, 64)]; for (int i = 0; i < _visibleSlots.Count; i++) { var data = _slots[_visibleSlots[i]]!; _deicScratch[i] = new DrawElementsIndirectCommand { Count = (uint)data.IndexCount, InstanceCount = 1u, FirstIndex = data.FirstIndex, BaseVertex = 0, // baked into indices on upload BaseInstance = 0, }; } // Grow indirect buffer if needed. if (_visibleSlots.Count > _indirectCapacity) { _indirectCapacity = Math.Max(64, _visibleSlots.Count * 2); _gl.BindBuffer(GLEnum.DrawIndirectBuffer, _indirectBuffer); _gl.BufferData(GLEnum.DrawIndirectBuffer, (nuint)(_indirectCapacity * sizeof(DrawElementsIndirectCommand)), null, GLEnum.DynamicDraw); } else { _gl.BindBuffer(GLEnum.DrawIndirectBuffer, _indirectBuffer); } // Upload DEIC array. fixed (DrawElementsIndirectCommand* p = _deicScratch) { _gl.BufferSubData(GLEnum.DrawIndirectBuffer, 0, (nuint)(_visibleSlots.Count * sizeof(DrawElementsIndirectCommand)), p); } // Bind shader + uniforms + atlas handles. _shader.Use(); _shader.SetMatrix4("uView", camera.View); _shader.SetMatrix4("uProjection", camera.Projection); var (terrainHandle, alphaHandle) = _atlas.GetBindlessHandles(); // Pass each 64-bit handle as a uvec2 (low 32 bits, high 32 bits). // GLSL constructs sampler2DArray(uTerrainHandle) at the use site — // see terrain_modern.frag for why this is the safe pattern. _gl.ProgramUniform2(_shader.Program, _uTerrainHandleLoc, (uint)(terrainHandle & 0xFFFFFFFFu), (uint)(terrainHandle >> 32)); _gl.ProgramUniform2(_shader.Program, _uAlphaHandleLoc, (uint)(alphaHandle & 0xFFFFFFFFu), (uint)(alphaHandle >> 32)); _gl.BindVertexArray(_globalVao); _gl.MemoryBarrier(MemoryBarrierMask.CommandBarrierBit); _gl.MultiDrawElementsIndirect( PrimitiveType.Triangles, DrawElementsType.UnsignedInt, (void*)0, (uint)_visibleSlots.Count, (uint)sizeof(DrawElementsIndirectCommand)); _gl.BindVertexArray(0); _gl.BindBuffer(GLEnum.DrawIndirectBuffer, 0); } public void Dispose() { _gl.DeleteVertexArray(_globalVao); _gl.DeleteBuffer(_globalVbo); _gl.DeleteBuffer(_globalEbo); _gl.DeleteBuffer(_indirectBuffer); } // ---------------------------------------------------------------- // Private helpers // ---------------------------------------------------------------- private void AllocateGpuBuffers(int capacitySlots) { nuint vboBytes = (nuint)(capacitySlots * VertsPerLandblock * VertexSize); nuint eboBytes = (nuint)(capacitySlots * IndicesPerLandblock * IndexSize); _gl.BindBuffer(BufferTargetARB.ArrayBuffer, _globalVbo); _gl.BufferData(BufferTargetARB.ArrayBuffer, vboBytes, null, BufferUsageARB.DynamicDraw); _gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0); _gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, _globalEbo); _gl.BufferData(BufferTargetARB.ElementArrayBuffer, eboBytes, null, BufferUsageARB.DynamicDraw); _gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, 0); } private void ConfigureVao() { _gl.BindVertexArray(_globalVao); _gl.BindBuffer(BufferTargetARB.ArrayBuffer, _globalVbo); _gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, _globalEbo); uint stride = (uint)VertexSize; // location 0: Position _gl.EnableVertexAttribArray(0); _gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, stride, (void*)0); // location 1: Normal _gl.EnableVertexAttribArray(1); _gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, stride, (void*)(3 * sizeof(float))); // locations 2-5: Data0..Data3 (uvec4 byte attributes) nint dataOffset = 6 * sizeof(float); _gl.EnableVertexAttribArray(2); _gl.VertexAttribIPointer(2, 4, VertexAttribIType.UnsignedByte, stride, (void*)dataOffset); _gl.EnableVertexAttribArray(3); _gl.VertexAttribIPointer(3, 4, VertexAttribIType.UnsignedByte, stride, (void*)(dataOffset + 4)); _gl.EnableVertexAttribArray(4); _gl.VertexAttribIPointer(4, 4, VertexAttribIType.UnsignedByte, stride, (void*)(dataOffset + 8)); _gl.EnableVertexAttribArray(5); _gl.VertexAttribIPointer(5, 4, VertexAttribIType.UnsignedByte, stride, (void*)(dataOffset + 12)); _gl.BindVertexArray(0); } private void EnsureCapacity(int newCapacity) { if (newCapacity <= _alloc.Capacity) return; // Allocate new VBO + EBO at new size; copy old contents; swap; recreate VAO. uint newVbo = _gl.GenBuffer(); uint newEbo = _gl.GenBuffer(); nuint newVboBytes = (nuint)(newCapacity * VertsPerLandblock * VertexSize); nuint newEboBytes = (nuint)(newCapacity * IndicesPerLandblock * IndexSize); nuint oldVboBytes = (nuint)(_alloc.Capacity * VertsPerLandblock * VertexSize); nuint oldEboBytes = (nuint)(_alloc.Capacity * IndicesPerLandblock * IndexSize); _gl.BindBuffer(BufferTargetARB.ArrayBuffer, newVbo); _gl.BufferData(BufferTargetARB.ArrayBuffer, newVboBytes, null, BufferUsageARB.DynamicDraw); _gl.BindBuffer(BufferTargetARB.CopyReadBuffer, _globalVbo); _gl.BindBuffer(BufferTargetARB.CopyWriteBuffer, newVbo); _gl.CopyBufferSubData(CopyBufferSubDataTarget.CopyReadBuffer, CopyBufferSubDataTarget.CopyWriteBuffer, 0, 0, oldVboBytes); _gl.DeleteBuffer(_globalVbo); _globalVbo = newVbo; _gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, newEbo); _gl.BufferData(BufferTargetARB.ElementArrayBuffer, newEboBytes, null, BufferUsageARB.DynamicDraw); _gl.BindBuffer(BufferTargetARB.CopyReadBuffer, _globalEbo); _gl.BindBuffer(BufferTargetARB.CopyWriteBuffer, newEbo); _gl.CopyBufferSubData(CopyBufferSubDataTarget.CopyReadBuffer, CopyBufferSubDataTarget.CopyWriteBuffer, 0, 0, oldEboBytes); _gl.DeleteBuffer(_globalEbo); _globalEbo = newEbo; // Recreate VAO with new buffer bindings. _gl.DeleteVertexArray(_globalVao); _globalVao = _gl.GenVertexArray(); ConfigureVao(); // Grow slot tracking array. Array.Resize(ref _slots, newCapacity); _alloc.GrowTo(newCapacity); } private sealed class SlotData { public uint LandblockId; public Vector3 WorldOrigin; public uint FirstIndex; public int IndexCount; public Vector3 AabbMin; public Vector3 AabbMax; } }