acdream/src/AcDream.App/Rendering/TerrainModernRenderer.cs
Erik 0a77bd1fd7 phase(N.5b) Task 6: TerrainModernRenderer
The new terrain dispatcher. Single global VBO/EBO with a slot
allocator (one slot per landblock, 384 verts × 40 bytes per slot).
Per-frame: build DEIC 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 (vs today's per-LB binds at radius=2 → ~25 calls,
radius=5 → ~121 calls).

API mirrors TerrainChunkRenderer so GameWindow integration in T8 is
a drop-in field+ctor swap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 09:05:28 +02:00

344 lines
14 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.Numerics;
using AcDream.App.Rendering.Wb;
using AcDream.Core.Terrain;
using Silk.NET.OpenGL;
namespace AcDream.App.Rendering;
/// <summary>
/// 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.
/// </summary>
public sealed unsafe class TerrainModernRenderer : IDisposable
{
private const int VertsPerLandblock = LandblockMesh.VerticesPerLandblock; // 384
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;
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<uint, int> _idToSlot = new();
// GPU buffers.
private uint _globalVao;
private uint _globalVbo;
private uint _globalEbo;
private uint _indirectBuffer;
private int _indirectCapacity;
// Cached sampler-uniform locations (matrix uniforms are set by name via Shader.SetMatrix4).
private int _uTerrainLoc;
private int _uAlphaLoc;
// Reusable per-frame buffers.
private readonly List<int> _visibleSlots = new();
private DrawElementsIndirectCommand[] _deicScratch = Array.Empty<DrawElementsIndirectCommand>();
// 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];
_uTerrainLoc = _gl.GetUniformLocation(_shader.Program, "uTerrain");
_uAlphaLoc = _gl.GetUniformLocation(_shader.Program, "uAlpha");
_globalVao = _gl.GenVertexArray();
_globalVbo = _gl.GenBuffer();
_globalEbo = _gl.GenBuffer();
AllocateGpuBuffers(initialSlotCapacity);
ConfigureVao();
_indirectBuffer = _gl.GenBuffer();
}
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 (_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();
_bindless.SetSamplerHandleUniform(_shader.Program, _uTerrainLoc, terrainHandle);
_bindless.SetSamplerHandleUniform(_shader.Program, _uAlphaLoc, alphaHandle);
_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;
}
}