From 75913c1c97d4bd7c292ee9a2192684fa5b9f9d5d Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 9 May 2026 09:21:32 +0200 Subject: [PATCH] phase(N.5b): wire TerrainModernRenderer into GameWindow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/AcDream.App/Rendering/GameWindow.cs | 148 ++++++++++++++++++------ 1 file changed, 115 insertions(+), 33 deletions(-) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 273f4d4..f8edcaa 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -18,8 +18,13 @@ public sealed class GameWindow : IDisposable private IWindow? _window; private GL? _gl; private IInputContext? _input; - private TerrainChunkRenderer? _terrain; + private TerrainModernRenderer? _terrain; private Shader? _shader; + /// Phase N.5b: terrain_modern.vert/.frag program. Owned by + /// at draw time but allocated + disposed here. Lives + /// in parallel with (legacy terrain.vert/.frag) until + /// Task 9 deletes the legacy renderer. + private Shader? _terrainModernShader; private CameraController? _cameraController; private IMouse? _capturedMouse; private DatCollection? _dats; @@ -68,6 +73,15 @@ public sealed class GameWindow : IDisposable private string _lastNearestObjLabel = "-"; 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. private AcDream.App.Streaming.LandblockStreamer? _streamer; 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.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 // binding=1 for the lifetime of the process — every shader that // declares `layout(std140, binding = 1) uniform SceneLighting` @@ -1385,10 +1406,44 @@ public sealed class GameWindow : IDisposable // TimeSync arrives. WorldTime.SyncFromServer(AcDream.Core.World.DerethDateTime.DayTicks / 16.0); // = 476.25 = Midsong (noon) - // Build the terrain atlas once from the Region dat. - var terrainAtlas = AcDream.App.Rendering.TerrainAtlas.Build(_gl, _dats); + // N.5: detect ARB_bindless_texture + ARB_shader_draw_parameters BEFORE + // 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 centerY = (int)((centerLandblockId >> 16) & 0xFFu); @@ -1418,35 +1473,8 @@ public sealed class GameWindow : IDisposable _heightTable = heightTable; _surfaceCache = new Dictionary(); - // N.5: detect ARB_bindless_texture + ARB_shader_draw_parameters. - // 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"); - } - - 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."); - } + // (Bindless detection moved above — must precede TerrainAtlas.Build / + // TerrainModernRenderer ctor so they can consume BindlessSupport.) // Mesh shader always loads (modern path is the only path). _meshShader = new Shader(_gl, @@ -6314,7 +6342,15 @@ public sealed class GameWindow : IDisposable 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); + _terrainCpuStopwatch.Stop(); + _terrainCpuSamples[_terrainCpuSampleCursor] = (long)(_terrainCpuStopwatch.Elapsed.TotalMicroseconds); + _terrainCpuSampleCursor = (_terrainCpuSampleCursor + 1) % _terrainCpuSamples.Length; + MaybeFlushTerrainDiag(); // Conditional depth clear: when camera is inside a building, clear // depth (not color) so interior geometry writes fresh Z values on top @@ -8713,6 +8749,51 @@ public sealed class GameWindow : IDisposable } } + /// Phase N.5b: emits [TERRAIN-DIAG] once per ~5s under + /// ACDREAM_WB_DIAG=1. Mirrors WbDrawDispatcher.MaybeFlushDiag: + /// 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. + 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() { // Phase A.1: join the streamer worker thread before tearing down GL @@ -8733,6 +8814,7 @@ public sealed class GameWindow : IDisposable _meshShader?.Dispose(); _terrain?.Dispose(); _shader?.Dispose(); + _terrainModernShader?.Dispose(); _sceneLightingUbo?.Dispose(); _particleRenderer?.Dispose(); _debugLines?.Dispose();