phase(N.5b): wire TerrainModernRenderer into GameWindow

Swap TerrainChunkRenderer → TerrainModernRenderer (drop-in: same
AddLandblock/RemoveLandblock/Draw interface). Pass BindlessSupport
to TerrainAtlas.Build so GetBindlessHandles() is callable. Load the
new terrain_modern shader pair and pass to the renderer ctor. Add
[TERRAIN-DIAG] rollup mirroring the existing [WB-DIAG] pattern.

Bindless detection moved above terrain construction so atlas + ctor
can consume BindlessSupport (was previously detected after — order
required for N.5b).

Visual verification at four scenes (Holtburg flat + sloped, Foundry,
sloped landblock) is the next gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-09 09:21:32 +02:00
parent 3418f65462
commit 75913c1c97

View file

@ -18,8 +18,13 @@ public sealed class GameWindow : IDisposable
private IWindow? _window; private IWindow? _window;
private GL? _gl; private GL? _gl;
private IInputContext? _input; private IInputContext? _input;
private TerrainChunkRenderer? _terrain; private TerrainModernRenderer? _terrain;
private Shader? _shader; private Shader? _shader;
/// <summary>Phase N.5b: terrain_modern.vert/.frag program. Owned by
/// <see cref="_terrain"/> at draw time but allocated + disposed here. Lives
/// in parallel with <see cref="_shader"/> (legacy terrain.vert/.frag) until
/// Task 9 deletes the legacy renderer.</summary>
private Shader? _terrainModernShader;
private CameraController? _cameraController; private CameraController? _cameraController;
private IMouse? _capturedMouse; private IMouse? _capturedMouse;
private DatCollection? _dats; private DatCollection? _dats;
@ -68,6 +73,15 @@ public sealed class GameWindow : IDisposable
private string _lastNearestObjLabel = "-"; private string _lastNearestObjLabel = "-";
private bool _lastColliding; private bool _lastColliding;
// Phase N.5b: CPU timing for [TERRAIN-DIAG] under ACDREAM_WB_DIAG=1
// (parallel diagnostic to [WB-DIAG] in WbDrawDispatcher — same env var
// gate so flipping one switch turns on both dispatcher rollups). Mirrors
// the rolling-256-sample buffer pattern from WbDrawDispatcher.
private readonly System.Diagnostics.Stopwatch _terrainCpuStopwatch = new();
private readonly long[] _terrainCpuSamples = new long[256]; // microseconds
private int _terrainCpuSampleCursor;
private long _terrainLastDiagTick;
// Phase A.1: streaming fields replacing the one-shot _entities list. // Phase A.1: streaming fields replacing the one-shot _entities list.
private AcDream.App.Streaming.LandblockStreamer? _streamer; private AcDream.App.Streaming.LandblockStreamer? _streamer;
private AcDream.App.Streaming.GpuWorldState _worldState = new(); private AcDream.App.Streaming.GpuWorldState _worldState = new();
@ -969,6 +983,13 @@ public sealed class GameWindow : IDisposable
Path.Combine(shadersDir, "terrain.vert"), Path.Combine(shadersDir, "terrain.vert"),
Path.Combine(shadersDir, "terrain.frag")); Path.Combine(shadersDir, "terrain.frag"));
// Phase N.5b: terrain_modern shader pair — bindless texture handles +
// glMultiDrawElementsIndirect dispatch path. Loaded in parallel with
// the legacy `_shader`; Task 9 will retire the legacy program.
_terrainModernShader = new Shader(_gl,
Path.Combine(shadersDir, "terrain_modern.vert"),
Path.Combine(shadersDir, "terrain_modern.frag"));
// Phase G.1/G.2: shared scene-lighting UBO. Stays bound at // Phase G.1/G.2: shared scene-lighting UBO. Stays bound at
// binding=1 for the lifetime of the process — every shader that // binding=1 for the lifetime of the process — every shader that
// declares `layout(std140, binding = 1) uniform SceneLighting` // declares `layout(std140, binding = 1) uniform SceneLighting`
@ -1385,10 +1406,44 @@ public sealed class GameWindow : IDisposable
// TimeSync arrives. // TimeSync arrives.
WorldTime.SyncFromServer(AcDream.Core.World.DerethDateTime.DayTicks / 16.0); // = 476.25 = Midsong (noon) WorldTime.SyncFromServer(AcDream.Core.World.DerethDateTime.DayTicks / 16.0); // = 476.25 = Midsong (noon)
// Build the terrain atlas once from the Region dat. // N.5: detect ARB_bindless_texture + ARB_shader_draw_parameters BEFORE
var terrainAtlas = AcDream.App.Rendering.TerrainAtlas.Build(_gl, _dats); // building the terrain atlas / renderer — both consume BindlessSupport
// (atlas via Texture2DArray bindless handles, renderer for SSBO uploads).
// The modern path (SSBO + glMultiDrawElementsIndirect + bindless textures)
// is mandatory as of Phase N.5 — missing extensions throw at startup with
// a clear error so users can file a real bug report rather than silently
// falling back to a half-working renderer.
if (AcDream.App.Rendering.Wb.BindlessSupport.TryCreate(_gl, out var bindless))
{
if (bindless!.HasShaderDrawParameters(_gl))
{
_bindlessSupport = bindless;
Console.WriteLine("[N.5] modern path capabilities present (bindless + ARB_shader_draw_parameters)");
}
else
{
Console.WriteLine("[N.5] GL_ARB_shader_draw_parameters not present — modern path not available");
}
}
else
{
Console.WriteLine("[N.5] GL_ARB_bindless_texture not present — modern path not available");
}
_terrain = new TerrainChunkRenderer(_gl, _shader, terrainAtlas); if (_bindlessSupport is null)
{
throw new NotSupportedException(
"acdream requires GL_ARB_bindless_texture + GL_ARB_shader_draw_parameters " +
"(GL 4.3+ with bindless support). Your GPU/driver does not expose these extensions. " +
"If this is unexpected, please file a bug report with your GPU vendor + driver version.");
}
// Build the terrain atlas once from the Region dat. Phase N.5b: the
// atlas exposes bindless handles for the modern terrain path, so
// BindlessSupport is threaded through.
var terrainAtlas = AcDream.App.Rendering.TerrainAtlas.Build(_gl, _dats, _bindlessSupport);
_terrain = new TerrainModernRenderer(_gl, _bindlessSupport, _terrainModernShader!, terrainAtlas);
int centerX = (int)((centerLandblockId >> 24) & 0xFFu); int centerX = (int)((centerLandblockId >> 24) & 0xFFu);
int centerY = (int)((centerLandblockId >> 16) & 0xFFu); int centerY = (int)((centerLandblockId >> 16) & 0xFFu);
@ -1418,35 +1473,8 @@ public sealed class GameWindow : IDisposable
_heightTable = heightTable; _heightTable = heightTable;
_surfaceCache = new Dictionary<uint, AcDream.Core.Terrain.SurfaceInfo>(); _surfaceCache = new Dictionary<uint, AcDream.Core.Terrain.SurfaceInfo>();
// N.5: detect ARB_bindless_texture + ARB_shader_draw_parameters. // (Bindless detection moved above — must precede TerrainAtlas.Build /
// The modern path (SSBO + glMultiDrawElementsIndirect + bindless textures) // TerrainModernRenderer ctor so they can consume BindlessSupport.)
// is mandatory as of Phase N.5 — missing extensions throw at startup with
// a clear error so users can file a real bug report rather than silently
// falling back to a half-working renderer.
if (AcDream.App.Rendering.Wb.BindlessSupport.TryCreate(_gl, out var bindless))
{
if (bindless!.HasShaderDrawParameters(_gl))
{
_bindlessSupport = bindless;
Console.WriteLine("[N.5] modern path capabilities present (bindless + ARB_shader_draw_parameters)");
}
else
{
Console.WriteLine("[N.5] GL_ARB_shader_draw_parameters not present — modern path not available");
}
}
else
{
Console.WriteLine("[N.5] GL_ARB_bindless_texture not present — modern path not available");
}
if (_bindlessSupport is null)
{
throw new NotSupportedException(
"acdream requires GL_ARB_bindless_texture + GL_ARB_shader_draw_parameters " +
"(GL 4.3+ with bindless support). Your GPU/driver does not expose these extensions. " +
"If this is unexpected, please file a bug report with your GPU vendor + driver version.");
}
// Mesh shader always loads (modern path is the only path). // Mesh shader always loads (modern path is the only path).
_meshShader = new Shader(_gl, _meshShader = new Shader(_gl,
@ -6314,7 +6342,15 @@ public sealed class GameWindow : IDisposable
goto SkipWorldGeometry; goto SkipWorldGeometry;
} }
// Phase N.5b: wrap Draw in CPU stopwatch for [TERRAIN-DIAG] rollup
// (gated on ACDREAM_WB_DIAG=1, same env var as [WB-DIAG]). Stopwatch
// is cheap; only the periodic Console.WriteLine is gated.
_terrainCpuStopwatch.Restart();
_terrain?.Draw(camera, frustum, neverCullLandblockId: playerLb); _terrain?.Draw(camera, frustum, neverCullLandblockId: playerLb);
_terrainCpuStopwatch.Stop();
_terrainCpuSamples[_terrainCpuSampleCursor] = (long)(_terrainCpuStopwatch.Elapsed.TotalMicroseconds);
_terrainCpuSampleCursor = (_terrainCpuSampleCursor + 1) % _terrainCpuSamples.Length;
MaybeFlushTerrainDiag();
// Conditional depth clear: when camera is inside a building, clear // Conditional depth clear: when camera is inside a building, clear
// depth (not color) so interior geometry writes fresh Z values on top // depth (not color) so interior geometry writes fresh Z values on top
@ -8713,6 +8749,51 @@ public sealed class GameWindow : IDisposable
} }
} }
/// <summary>Phase N.5b: emits [TERRAIN-DIAG] once per ~5s under
/// ACDREAM_WB_DIAG=1. Mirrors <c>WbDrawDispatcher.MaybeFlushDiag</c>:
/// rolling 256-sample buffer of microseconds, median + p95 reported.
/// Sample buffer is NOT cleared on flush — it's a moving window so the
/// next 5s window already has 256 frames of recent history.</summary>
private void MaybeFlushTerrainDiag()
{
if (!string.Equals(Environment.GetEnvironmentVariable("ACDREAM_WB_DIAG"), "1", StringComparison.Ordinal))
return;
long now = Environment.TickCount64;
if (now - _terrainLastDiagTick <= 5000) return;
long cpuMedUs = TerrainDiagMedianMicros(_terrainCpuSamples);
long cpuP95Us = TerrainDiagPercentile95Micros(_terrainCpuSamples);
Console.WriteLine(
$"[TERRAIN-DIAG] cpu_ms={cpuMedUs / 1000.0:F2}/{cpuP95Us / 1000.0:F2} " +
$"draws={_terrain?.VisibleSlots ?? 0}/frame " +
$"visible={_terrain?.VisibleSlots ?? 0} " +
$"loaded={_terrain?.LoadedSlots ?? 0} " +
$"capacity={_terrain?.CapacitySlots ?? 0}");
_terrainLastDiagTick = now;
}
private static long TerrainDiagMedianMicros(long[] samples)
{
var copy = (long[])samples.Clone();
Array.Sort(copy);
int nz = 0;
foreach (var v in copy) if (v > 0) nz++;
if (nz == 0) return 0;
return copy[copy.Length - nz / 2];
}
private static long TerrainDiagPercentile95Micros(long[] samples)
{
var copy = (long[])samples.Clone();
Array.Sort(copy);
int nz = 0;
foreach (var v in copy) if (v > 0) nz++;
if (nz == 0) return 0;
int idx = copy.Length - 1 - (int)(nz * 0.05);
return copy[idx];
}
private void OnClosing() private void OnClosing()
{ {
// Phase A.1: join the streamer worker thread before tearing down GL // Phase A.1: join the streamer worker thread before tearing down GL
@ -8733,6 +8814,7 @@ public sealed class GameWindow : IDisposable
_meshShader?.Dispose(); _meshShader?.Dispose();
_terrain?.Dispose(); _terrain?.Dispose();
_shader?.Dispose(); _shader?.Dispose();
_terrainModernShader?.Dispose();
_sceneLightingUbo?.Dispose(); _sceneLightingUbo?.Dispose();
_particleRenderer?.Dispose(); _particleRenderer?.Dispose();
_debugLines?.Dispose(); _debugLines?.Dispose();