From db0f010544b589a2c1ade2ed6094d6b6a02866ef Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 9 May 2026 08:37:23 +0200 Subject: [PATCH] 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) --- src/AcDream.App/Rendering/TerrainAtlas.cs | 49 +++++++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/src/AcDream.App/Rendering/TerrainAtlas.cs b/src/AcDream.App/Rendering/TerrainAtlas.cs index faa3a6e..d49610e 100644 --- a/src/AcDream.App/Rendering/TerrainAtlas.cs +++ b/src/AcDream.App/Rendering/TerrainAtlas.cs @@ -53,14 +53,45 @@ public sealed unsafe class TerrainAtlas : IDisposable /// RCode for each RoadMap, parallel to . public IReadOnlyList 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; + + /// + /// Get 64-bit bindless handles for the terrain + alpha texture arrays. + /// Throws if the atlas was constructed + /// without a instance. Handles are generated + /// lazily on first call and cached for the atlas's lifetime; both textures + /// are made resident. + /// + 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 map, int layerCount, uint glAlphaTexture, int alphaLayerCount, IReadOnlyList cornerLayers, IReadOnlyList sideLayers, IReadOnlyList roadLayers, IReadOnlyList cornerTCodes, IReadOnlyList sideTCodes, IReadOnlyList 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. /// - public static TerrainAtlas Build(GL gl, DatCollection dats) + public static TerrainAtlas Build(GL gl, DatCollection dats, Wb.BindlessSupport? bindless = null) { var region = dats.Get(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 { [0] = 0u }, 1, alphaTex, 1, Array.Empty(), Array.Empty(), Array.Empty(), @@ -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); }