phase(N.5b) Task 1: TerrainAtlas bindless extension

Add optional BindlessSupport ctor parameter + GetBindlessHandles()
method that returns (terrainHandle, alphaHandle) ulongs with both
textures made resident. Two-phase Dispose mirroring TextureCache
(MakeNonResident before DeleteTexture per ARB_bindless_texture spec).

Existing callers pass `Build(gl, dats)` unchanged; bindless = null
default keeps them working until T6/T8 wires the renderer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-09 08:37:23 +02:00
parent 79367d4c15
commit db0f010544

View file

@ -53,14 +53,45 @@ public sealed unsafe class TerrainAtlas : IDisposable
/// <summary>RCode for each RoadMap, parallel to <see cref="RoadAlphaLayers"/>.</summary>
public IReadOnlyList<uint> RoadAlphaRCodes { get; }
private readonly Wb.BindlessSupport? _bindless;
// Cached bindless handles. Generated lazily on first GetBindlessHandles() call;
// reused for the lifetime of the atlas.
private ulong _terrainHandle;
private ulong _alphaHandle;
private bool _handlesGenerated;
/// <summary>
/// Get 64-bit bindless handles for the terrain + alpha texture arrays.
/// Throws <see cref="InvalidOperationException"/> if the atlas was constructed
/// without a <see cref="Wb.BindlessSupport"/> instance. Handles are generated
/// lazily on first call and cached for the atlas's lifetime; both textures
/// are made resident.
/// </summary>
public (ulong terrain, ulong alpha) GetBindlessHandles()
{
if (_bindless is null)
throw new InvalidOperationException(
"TerrainAtlas was constructed without BindlessSupport; cannot return bindless handles.");
if (!_handlesGenerated)
{
_terrainHandle = _bindless.GetResidentHandle(GlTexture);
_alphaHandle = _bindless.GetResidentHandle(GlAlphaTexture);
_handlesGenerated = true;
}
return (_terrainHandle, _alphaHandle);
}
private TerrainAtlas(
GL gl,
Wb.BindlessSupport? bindless,
uint glTexture, IReadOnlyDictionary<uint, uint> map, int layerCount,
uint glAlphaTexture, int alphaLayerCount,
IReadOnlyList<byte> cornerLayers, IReadOnlyList<byte> sideLayers, IReadOnlyList<byte> roadLayers,
IReadOnlyList<uint> cornerTCodes, IReadOnlyList<uint> sideTCodes, IReadOnlyList<uint> roadRCodes)
{
_gl = gl;
_bindless = bindless;
GlTexture = glTexture;
TerrainTypeToLayer = map;
LayerCount = layerCount;
@ -79,7 +110,7 @@ public sealed unsafe class TerrainAtlas : IDisposable
/// for the mapping from TerrainTextureType to SurfaceTexture id, decoding each
/// to RGBA8, and uploading as layers in a single GL_TEXTURE_2D_ARRAY.
/// </summary>
public static TerrainAtlas Build(GL gl, DatCollection dats)
public static TerrainAtlas Build(GL gl, DatCollection dats, Wb.BindlessSupport? bindless = null)
{
var region = dats.Get<Region>(0x13000000u)
?? throw new InvalidOperationException("Region dat id 0x13000000 missing");
@ -89,7 +120,7 @@ public sealed unsafe class TerrainAtlas : IDisposable
if (terrainDesc is null || terrainDesc.Count == 0)
{
Console.WriteLine("WARN: TerrainDesc missing, using single white fallback layer");
return BuildFallback(gl);
return BuildFallback(gl, bindless);
}
// ---- Terrain atlas (unchanged Phase 2b logic) ----
@ -167,6 +198,7 @@ public sealed unsafe class TerrainAtlas : IDisposable
return new TerrainAtlas(
gl,
bindless,
tex, map, layerCount,
alphaBuild.gl, alphaBuild.layerCount,
alphaBuild.corner, alphaBuild.side, alphaBuild.road,
@ -350,7 +382,7 @@ public sealed unsafe class TerrainAtlas : IDisposable
return dst;
}
private static TerrainAtlas BuildFallback(GL gl)
private static TerrainAtlas BuildFallback(GL gl, Wb.BindlessSupport? bindless = null)
{
uint tex = gl.GenTexture();
gl.BindTexture(TextureTarget.Texture2DArray, tex);
@ -372,6 +404,7 @@ public sealed unsafe class TerrainAtlas : IDisposable
return new TerrainAtlas(
gl,
bindless,
tex, new Dictionary<uint, uint> { [0] = 0u }, 1,
alphaTex, 1,
Array.Empty<byte>(), Array.Empty<byte>(), Array.Empty<byte>(),
@ -380,6 +413,16 @@ public sealed unsafe class TerrainAtlas : IDisposable
public void Dispose()
{
// Phase 1: release bindless residency BEFORE deleting textures.
// ARB_bindless_texture requires this ordering; interleaving is UB.
if (_handlesGenerated && _bindless is not null)
{
_bindless.MakeNonResident(_terrainHandle);
_bindless.MakeNonResident(_alphaHandle);
_handlesGenerated = false;
}
// Phase 2: delete the underlying GL textures.
_gl.DeleteTexture(GlTexture);
_gl.DeleteTexture(GlAlphaTexture);
}