feat(app): Phase A.1 — TerrainRenderer.RemoveLandblock for streaming unloads

TerrainRenderer's internal landblock collection is now a Dictionary
keyed by landblock id so the streaming system can release GPU
resources per-landblock as the visible window moves. AddLandblock
takes the id as its first parameter; if the same id is added twice,
the old buffers are freed before the new ones land (defensive but
cheap). RemoveLandblock is a no-op for unknown ids and deletes
VBO/EBO/VAO for known ones.

Single existing caller in GameWindow.cs updated to pass the id.

Build green. No unit tests — direct-to-GL methods need a live context.
Tasks 5-7 will validate end-to-end.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-11 22:22:49 +02:00
parent c5e207a51f
commit 495f87a4ad
2 changed files with 32 additions and 6 deletions

View file

@ -245,7 +245,7 @@ public sealed class GameWindow : IDisposable
((int)lbY - centerY) * 192f, ((int)lbY - centerY) * 192f,
0f); 0f);
_terrain.AddLandblock(meshData, origin); _terrain.AddLandblock(lb.LandblockId, meshData, origin);
} }
Console.WriteLine($"terrain: {surfaceCache.Count} unique palette codes across {worldView.Landblocks.Count} landblocks"); Console.WriteLine($"terrain: {surfaceCache.Count} unique palette codes across {worldView.Landblocks.Count} landblocks");

View file

@ -22,7 +22,7 @@ public sealed unsafe class TerrainRenderer : IDisposable
private readonly GL _gl; private readonly GL _gl;
private readonly Shader _shader; private readonly Shader _shader;
private readonly TerrainAtlas _atlas; private readonly TerrainAtlas _atlas;
private readonly List<LandblockGpu> _landblocks = new(); private readonly Dictionary<uint, LandblockGpu> _landblocks = new();
public TerrainRenderer(GL gl, Shader shader, TerrainAtlas atlas) public TerrainRenderer(GL gl, Shader shader, TerrainAtlas atlas)
{ {
@ -31,10 +31,19 @@ public sealed unsafe class TerrainRenderer : IDisposable
_atlas = atlas; _atlas = atlas;
} }
public void AddLandblock(LandblockMeshData meshData, Vector3 worldOrigin) public void AddLandblock(uint landblockId, LandblockMeshData meshData, Vector3 worldOrigin)
{ {
if (_landblocks.TryGetValue(landblockId, out var existing))
{
_gl.DeleteBuffer(existing.Vbo);
_gl.DeleteBuffer(existing.Ebo);
_gl.DeleteVertexArray(existing.Vao);
_landblocks.Remove(landblockId);
}
var gpu = new LandblockGpu var gpu = new LandblockGpu
{ {
LandblockId = landblockId,
Vao = _gl.GenVertexArray(), Vao = _gl.GenVertexArray(),
WorldOrigin = worldOrigin, WorldOrigin = worldOrigin,
IndexCount = meshData.Indices.Length, IndexCount = meshData.Indices.Length,
@ -76,7 +85,23 @@ public sealed unsafe class TerrainRenderer : IDisposable
_gl.VertexAttribIPointer(5, 4, VertexAttribIType.UnsignedByte, stride, (void*)(dataOffset + 12)); _gl.VertexAttribIPointer(5, 4, VertexAttribIType.UnsignedByte, stride, (void*)(dataOffset + 12));
_gl.BindVertexArray(0); _gl.BindVertexArray(0);
_landblocks.Add(gpu); _landblocks[landblockId] = gpu;
}
/// <summary>
/// Release GPU buffers for a previously-added landblock. No-op if the
/// landblock wasn't added. Called by the streaming system when a
/// landblock falls outside the visible window.
/// </summary>
public void RemoveLandblock(uint landblockId)
{
if (!_landblocks.TryGetValue(landblockId, out var gpu))
return;
_gl.DeleteBuffer(gpu.Vbo);
_gl.DeleteBuffer(gpu.Ebo);
_gl.DeleteVertexArray(gpu.Vao);
_landblocks.Remove(landblockId);
} }
public void Draw(ICamera camera) public void Draw(ICamera camera)
@ -96,7 +121,7 @@ public sealed unsafe class TerrainRenderer : IDisposable
int alphaLoc = _gl.GetUniformLocation(_shader.Program, "uAlpha"); int alphaLoc = _gl.GetUniformLocation(_shader.Program, "uAlpha");
if (alphaLoc >= 0) _gl.Uniform1(alphaLoc, 1); if (alphaLoc >= 0) _gl.Uniform1(alphaLoc, 1);
foreach (var lb in _landblocks) foreach (var lb in _landblocks.Values)
{ {
var model = Matrix4x4.CreateTranslation(lb.WorldOrigin); var model = Matrix4x4.CreateTranslation(lb.WorldOrigin);
_shader.SetMatrix4("uModel", model); _shader.SetMatrix4("uModel", model);
@ -108,7 +133,7 @@ public sealed unsafe class TerrainRenderer : IDisposable
public void Dispose() public void Dispose()
{ {
foreach (var lb in _landblocks) foreach (var lb in _landblocks.Values)
{ {
_gl.DeleteBuffer(lb.Vbo); _gl.DeleteBuffer(lb.Vbo);
_gl.DeleteBuffer(lb.Ebo); _gl.DeleteBuffer(lb.Ebo);
@ -119,6 +144,7 @@ public sealed unsafe class TerrainRenderer : IDisposable
private sealed class LandblockGpu private sealed class LandblockGpu
{ {
public uint LandblockId;
public uint Vao; public uint Vao;
public uint Vbo; public uint Vbo;
public uint Ebo; public uint Ebo;