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,
0f);
_terrain.AddLandblock(meshData, origin);
_terrain.AddLandblock(lb.LandblockId, meshData, origin);
}
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 Shader _shader;
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)
{
@ -31,10 +31,19 @@ public sealed unsafe class TerrainRenderer : IDisposable
_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
{
LandblockId = landblockId,
Vao = _gl.GenVertexArray(),
WorldOrigin = worldOrigin,
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.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)
@ -96,7 +121,7 @@ public sealed unsafe class TerrainRenderer : IDisposable
int alphaLoc = _gl.GetUniformLocation(_shader.Program, "uAlpha");
if (alphaLoc >= 0) _gl.Uniform1(alphaLoc, 1);
foreach (var lb in _landblocks)
foreach (var lb in _landblocks.Values)
{
var model = Matrix4x4.CreateTranslation(lb.WorldOrigin);
_shader.SetMatrix4("uModel", model);
@ -108,7 +133,7 @@ public sealed unsafe class TerrainRenderer : IDisposable
public void Dispose()
{
foreach (var lb in _landblocks)
foreach (var lb in _landblocks.Values)
{
_gl.DeleteBuffer(lb.Vbo);
_gl.DeleteBuffer(lb.Ebo);
@ -119,6 +144,7 @@ public sealed unsafe class TerrainRenderer : IDisposable
private sealed class LandblockGpu
{
public uint LandblockId;
public uint Vao;
public uint Vbo;
public uint Ebo;