fix(N.5b): black terrain — switch to uvec2 handle + sampler constructor

Symptom: terrain renders pure black in modern path (legacy renderer
correct). Diagnostic at TerrainModernRenderer.Draw showed:
  glProgramUniformHandle(prog=4, loc=5, handle=0x100251xxx) → GL_INVALID_OPERATION (0x0502)
on both terrain and alpha sampler uniforms.

Root cause: the `uniform sampler2DArray` + glProgramUniformHandleARB
combination is rejected by the NVIDIA Windows driver in this configuration.
The handle is valid and resident; the uniform location is valid; the
program is valid; but the driver refuses to bind a 64-bit handle to a
sampler uniform via the program-uniform path.

Fix: switch to N.5's mesh_modern pattern — pass each 64-bit handle as a
`uniform uvec2` (low + high 32-bit halves) and construct the sampler at
the use site via the GLSL `sampler2DArray(handle)` constructor. This
form is what ARB_bindless_texture documents as universally supported and
is what N.5 already uses successfully.

Files:
- terrain_modern.frag: replace `uniform sampler2DArray uTerrain/uAlpha`
  with `uniform uvec2 uTerrainHandle/uAlphaHandle` + `#define`s
- TerrainModernRenderer.cs: cache uvec2 uniform locations; set via
  `glProgramUniform2(program, loc, low32, high32)` per frame
- BindlessSupport.cs: remove now-unused `SetSamplerHandleUniform`,
  leave a comment noting why the helper was retired
- GameWindow.cs: also strip the temporary [TERRAIN-DBG] cursor-wrap
  print added during the perf-baseline investigation

Build green; 114/114 tests in N.5+N.5b filter still pass; user-verified
terrain renders correctly in modern path post-fix. Captured fresh perf
baseline:
- Legacy:  cpu_us median  1.5  / p95  3.0  (1 chunk = 1 glDrawElements)
- Modern:  cpu_us median  6.4-7.0 / p95  9-14 (51 visible LBs, 1 MDI call)

Modern is ~4× slower on CPU at radius=5 because the chunked legacy path
already collapsed the scene to one draw call. The architectural wins
(zero glBindTexture/frame; constant-cost dispatch as A.5 raises radius)
will be documented in T10's perf baseline doc; the spec's
"≥10% lower CPU" acceptance criterion is invalid at radius=5 and needs
revision.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-09 12:53:21 +02:00
parent 55e516c538
commit da56063be5
4 changed files with 45 additions and 23 deletions

View file

@ -6364,7 +6364,11 @@ public sealed class GameWindow : IDisposable
else
_terrain?.Draw(camera, frustum, neverCullLandblockId: playerLb);
_terrainCpuStopwatch.Stop();
_terrainCpuSamples[_terrainCpuSampleCursor] = (long)(_terrainCpuStopwatch.Elapsed.TotalMicroseconds);
// Multiply by 100 then divide by 100 in the diag print to keep
// 0.01 µs precision in the long-typed sample buffer. Terrain Draw
// is sub-microsecond on simple scenes; truncating to integer µs
// would round nearly every sample to 0.
_terrainCpuSamples[_terrainCpuSampleCursor] = (long)(_terrainCpuStopwatch.Elapsed.TotalMicroseconds * 100.0);
_terrainCpuSampleCursor = (_terrainCpuSampleCursor + 1) % _terrainCpuSamples.Length;
MaybeFlushTerrainDiag();
@ -8778,10 +8782,13 @@ public sealed class GameWindow : IDisposable
long now = Environment.TickCount64;
if (now - _terrainLastDiagTick <= 5000) return;
long cpuMedUs = TerrainDiagMedianMicros(_terrainCpuSamples);
long cpuP95Us = TerrainDiagPercentile95Micros(_terrainCpuSamples);
// Samples are stored as microseconds × 100 (so 1.23 µs becomes 123 long).
long cpuMedHundredthsUs = TerrainDiagMedianMicros(_terrainCpuSamples);
long cpuP95HundredthsUs = TerrainDiagPercentile95Micros(_terrainCpuSamples);
double cpuMedUs = cpuMedHundredthsUs / 100.0;
double cpuP95Us = cpuP95HundredthsUs / 100.0;
Console.WriteLine(
$"[TERRAIN-DIAG{(_useLegacyTerrain ? "/legacy" : "/modern")}] cpu_ms={cpuMedUs / 1000.0:F2}/{cpuP95Us / 1000.0:F2} " +
$"[TERRAIN-DIAG{(_useLegacyTerrain ? "/legacy" : "/modern")}] cpu_us={cpuMedUs:F2}m/{cpuP95Us:F2}p95 " +
$"draws={_terrain?.VisibleSlots ?? 0}/frame " +
$"visible={_terrain?.VisibleSlots ?? 0} " +
$"loaded={_terrain?.LoadedSlots ?? 0} " +