using AcDream.Core.Textures; using DatReaderWriter; using DatReaderWriter.DBObjs; using DatReaderWriter.Enums; using Silk.NET.OpenGL; using DatPixelFormat = DatReaderWriter.Enums.PixelFormat; using GLPixelFormat = Silk.NET.OpenGL.PixelFormat; namespace AcDream.App.Rendering; /// /// Holds both texture arrays the terrain renderer samples from: /// /// /// Terrain atlas — one GL_TEXTURE_2D_ARRAY layer per terrain type /// (grass, dirt, sand, forest...), sourced from /// Region.TerrainInfo.LandSurfaces.TexMerge.TerrainDesc. /// /// /// Alpha atlas — one GL_TEXTURE_2D_ARRAY layer per blend mask, /// sourced from CornerTerrainMaps / SideTerrainMaps / RoadMaps in the /// same TexMerge. Used by the fragment shader to blend up to three /// terrain overlays and two roads on top of a base cell texture. /// /// /// The alpha atlas is built but not yet sampled by any shader — that wiring /// lands in Phase 3c.4 along with the shader rewrite. /// public sealed unsafe class TerrainAtlas : IDisposable { private readonly GL _gl; // --- Terrain atlas (unchanged public API from Phase 2b) --- public uint GlTexture { get; } // terrain atlas, kept as GlTexture for back-compat with TerrainRenderer public IReadOnlyDictionary TerrainTypeToLayer { get; } public int LayerCount { get; } // --- Alpha atlas (new in Phase 3c.2) --- public uint GlAlphaTexture { get; } public int AlphaLayerCount { get; } /// Layer indices in the alpha atlas for CornerTerrainMaps (typically 4 entries). public IReadOnlyList CornerAlphaLayers { get; } /// Layer indices in the alpha atlas for SideTerrainMaps (typically 4 entries). public IReadOnlyList SideAlphaLayers { get; } /// Layer indices in the alpha atlas for RoadMaps (variable count). public IReadOnlyList RoadAlphaLayers { get; } // --- Parallel TCode/RCode arrays (added in Phase 3c.4 for BuildSurface) --- /// TCode for each CornerTerrainMap, parallel to . public IReadOnlyList CornerAlphaTCodes { get; } /// TCode for each SideTerrainMap, parallel to . public IReadOnlyList SideAlphaTCodes { get; } /// 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; GlAlphaTexture = glAlphaTexture; AlphaLayerCount = alphaLayerCount; CornerAlphaLayers = cornerLayers; SideAlphaLayers = sideLayers; RoadAlphaLayers = roadLayers; CornerAlphaTCodes = cornerTCodes; SideAlphaTCodes = sideTCodes; RoadAlphaRCodes = roadRCodes; } /// /// Build the atlas by walking Region.TerrainInfo.LandSurfaces.TexMerge.TerrainDesc /// 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, Wb.BindlessSupport? bindless = null) { var region = dats.Get(0x13000000u) ?? throw new InvalidOperationException("Region dat id 0x13000000 missing"); var texMerge = region.TerrainInfo?.LandSurfaces?.TexMerge; var terrainDesc = texMerge?.TerrainDesc; if (terrainDesc is null || terrainDesc.Count == 0) { Console.WriteLine("WARN: TerrainDesc missing, using single white fallback layer"); return BuildFallback(gl, bindless); } // ---- Terrain atlas (unchanged Phase 2b logic) ---- var decodedByType = new Dictionary(); int maxW = 1, maxH = 1; foreach (var tmtd in terrainDesc) { uint typeKey = (uint)tmtd.TerrainType; if (decodedByType.ContainsKey(typeKey)) continue; var surfaceTextureId = (uint)tmtd.TerrainTex.TextureId; var st = dats.Get(surfaceTextureId); if (st is null || st.Textures.Count == 0) { Console.WriteLine($"WARN: TerrainType {tmtd.TerrainType} SurfaceTexture 0x{surfaceTextureId:X8} missing"); decodedByType[typeKey] = DecodedTexture.Magenta; continue; } var rs = dats.Get((uint)st.Textures[0]); if (rs is null) { decodedByType[typeKey] = DecodedTexture.Magenta; continue; } Palette? palette = rs.DefaultPaletteId != 0 ? dats.Get(rs.DefaultPaletteId) : null; var decoded = SurfaceDecoder.DecodeRenderSurface(rs, palette); decodedByType[typeKey] = decoded; if (decoded.Width > maxW) maxW = decoded.Width; if (decoded.Height > maxH) maxH = decoded.Height; } int layerCount = decodedByType.Count; uint tex = gl.GenTexture(); gl.BindTexture(TextureTarget.Texture2DArray, tex); gl.TexImage3D( TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8, (uint)maxW, (uint)maxH, (uint)layerCount, 0, GLPixelFormat.Rgba, PixelType.UnsignedByte, null); var map = new Dictionary(); int layerIdx = 0; foreach (var kvp in decodedByType) { byte[] buffer = ResizeRgba8Nearest(kvp.Value, maxW, maxH); fixed (byte* p = buffer) { gl.TexSubImage3D( TextureTarget.Texture2DArray, 0, 0, 0, layerIdx, (uint)maxW, (uint)maxH, 1, GLPixelFormat.Rgba, PixelType.UnsignedByte, p); } map[kvp.Key] = (uint)layerIdx; layerIdx++; } // A.5 T19: generate mipmaps + trilinear + 16x anisotropic for distant-LB quality. gl.GenerateMipmap(TextureTarget.Texture2DArray); gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.LinearMipmapLinear); gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear); gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat); gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat); // GL_TEXTURE_MAX_ANISOTROPY = 0x84FE (GL_EXT_texture_filter_anisotropic / ARB_texture_filter_anisotropic). gl.TexParameter(TextureTarget.Texture2DArray, (TextureParameterName)0x84FE, 16.0f); gl.BindTexture(TextureTarget.Texture2DArray, 0); Console.WriteLine($"TerrainAtlas: {layerCount} terrain layers at {maxW}x{maxH} (mipmaps+aniso16x)"); // ---- Alpha atlas (new in Phase 3c.2) ---- // texMerge is guaranteed non-null here: the early return above exited // if texMerge?.TerrainDesc was null. var alphaBuild = BuildAlphaAtlas(gl, dats, texMerge!); return new TerrainAtlas( gl, bindless, tex, map, layerCount, alphaBuild.gl, alphaBuild.layerCount, alphaBuild.corner, alphaBuild.side, alphaBuild.road, alphaBuild.cornerTCodes, alphaBuild.sideTCodes, alphaBuild.roadRCodes); } /// /// Load corner, side, and road alpha maps from the TexMerge into a second /// GL_TEXTURE_2D_ARRAY. AC ships these as 512×512 PFID_A8 textures; /// expands each alpha byte /// into all four RGBA channels so the shader can sample from any channel. /// /// Layers are appended in TexMerge insertion order: corners first, then /// sides, then roads. The returned index lists tell /// TerrainBlending.BuildSurface which layer to cite for each /// corner/side/road alpha source. /// private readonly record struct AlphaAtlasBuildResult( uint gl, int layerCount, IReadOnlyList corner, IReadOnlyList side, IReadOnlyList road, IReadOnlyList cornerTCodes, IReadOnlyList sideTCodes, IReadOnlyList roadRCodes); private static AlphaAtlasBuildResult BuildAlphaAtlas( GL gl, DatCollection dats, DatReaderWriter.Types.TexMerge texMerge) { var decoded = new List(); var cornerLayers = new List(); var sideLayers = new List(); var roadLayers = new List(); var cornerTCodes = new List(); var sideTCodes = new List(); var roadRCodes = new List(); foreach (var entry in texMerge.CornerTerrainMaps) { if (TryDecodeAlphaMap(dats, (uint)entry.TextureId, out var dtex)) { cornerLayers.Add((byte)decoded.Count); cornerTCodes.Add(entry.TCode); decoded.Add(dtex); } else { Console.WriteLine($"WARN: CornerTerrainMap TextureId 0x{(uint)entry.TextureId:X8} failed to decode"); } } foreach (var entry in texMerge.SideTerrainMaps) { if (TryDecodeAlphaMap(dats, (uint)entry.TextureId, out var dtex)) { sideLayers.Add((byte)decoded.Count); sideTCodes.Add(entry.TCode); decoded.Add(dtex); } else { Console.WriteLine($"WARN: SideTerrainMap TextureId 0x{(uint)entry.TextureId:X8} failed to decode"); } } foreach (var entry in texMerge.RoadMaps) { if (TryDecodeAlphaMap(dats, (uint)entry.TextureId, out var dtex)) { roadLayers.Add((byte)decoded.Count); roadRCodes.Add(entry.RCode); decoded.Add(dtex); } else { Console.WriteLine($"WARN: RoadMap TextureId 0x{(uint)entry.TextureId:X8} failed to decode"); } } if (decoded.Count == 0) { Console.WriteLine("WARN: no alpha maps loaded; alpha atlas will be a 1x1 white fallback"); uint fallbackAlpha = gl.GenTexture(); gl.BindTexture(TextureTarget.Texture2DArray, fallbackAlpha); gl.TexImage3D(TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8, 1, 1, 1, 0, GLPixelFormat.Rgba, PixelType.UnsignedByte, null); var white = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }; fixed (byte* p = white) gl.TexSubImage3D(TextureTarget.Texture2DArray, 0, 0, 0, 0, 1, 1, 1, GLPixelFormat.Rgba, PixelType.UnsignedByte, p); gl.BindTexture(TextureTarget.Texture2DArray, 0); return new AlphaAtlasBuildResult( fallbackAlpha, 1, cornerLayers, sideLayers, roadLayers, cornerTCodes, sideTCodes, roadRCodes); } // Alpha maps should all be uniform size (WorldBuilder asserts 512×512). // Fall back to the max observed so a stray mismatch doesn't crash us. int aMaxW = 1, aMaxH = 1; foreach (var d in decoded) { if (d.Width > aMaxW) aMaxW = d.Width; if (d.Height > aMaxH) aMaxH = d.Height; } uint glAlpha = gl.GenTexture(); gl.BindTexture(TextureTarget.Texture2DArray, glAlpha); gl.TexImage3D( TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8, (uint)aMaxW, (uint)aMaxH, (uint)decoded.Count, 0, GLPixelFormat.Rgba, PixelType.UnsignedByte, null); for (int i = 0; i < decoded.Count; i++) { var buffer = ResizeRgba8Nearest(decoded[i], aMaxW, aMaxH); fixed (byte* p = buffer) { gl.TexSubImage3D( TextureTarget.Texture2DArray, 0, 0, 0, i, (uint)aMaxW, (uint)aMaxH, 1, GLPixelFormat.Rgba, PixelType.UnsignedByte, p); } } gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear); gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear); gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToEdge); gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToEdge); gl.BindTexture(TextureTarget.Texture2DArray, 0); Console.WriteLine( $"AlphaAtlas: {decoded.Count} layers at {aMaxW}x{aMaxH} " + $"(corners={cornerLayers.Count}, sides={sideLayers.Count}, roads={roadLayers.Count})"); return new AlphaAtlasBuildResult( glAlpha, decoded.Count, cornerLayers, sideLayers, roadLayers, cornerTCodes, sideTCodes, roadRCodes); } private static bool TryDecodeAlphaMap(DatCollection dats, uint surfaceTextureId, out DecodedTexture decoded) { decoded = DecodedTexture.Magenta; var st = dats.Get(surfaceTextureId); if (st is null || st.Textures.Count == 0) return false; var rs = dats.Get((uint)st.Textures[0]); if (rs is null) return false; // Alpha maps ship as PFID_CUSTOM_LSCAPE_ALPHA (AC's landscape-alpha // format) or the more generic PFID_A8; terrain blending alpha masks // MUST use isAdditive=true so R=G=B=A=val — the terrain fragment shader // reads .r for the blend weight. Palette is not used. var d = SurfaceDecoder.DecodeRenderSurface(rs, palette: null, isClipMap: false, isAdditive: true); if (ReferenceEquals(d, DecodedTexture.Magenta)) return false; decoded = d; return true; } private static byte[] ResizeRgba8Nearest(DecodedTexture src, int dstW, int dstH) { if (src.Width == dstW && src.Height == dstH) return src.Rgba8; var dst = new byte[dstW * dstH * 4]; for (int y = 0; y < dstH; y++) { int srcY = y * src.Height / dstH; for (int x = 0; x < dstW; x++) { int srcX = x * src.Width / dstW; int si = (srcY * src.Width + srcX) * 4; int di = (y * dstW + x) * 4; dst[di + 0] = src.Rgba8[si + 0]; dst[di + 1] = src.Rgba8[si + 1]; dst[di + 2] = src.Rgba8[si + 2]; dst[di + 3] = src.Rgba8[si + 3]; } } return dst; } private static TerrainAtlas BuildFallback(GL gl, Wb.BindlessSupport? bindless = null) { uint tex = gl.GenTexture(); gl.BindTexture(TextureTarget.Texture2DArray, tex); var white = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }; gl.TexImage3D(TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8, 1, 1, 1, 0, GLPixelFormat.Rgba, PixelType.UnsignedByte, null); fixed (byte* p = white) gl.TexSubImage3D(TextureTarget.Texture2DArray, 0, 0, 0, 0, 1, 1, 1, GLPixelFormat.Rgba, PixelType.UnsignedByte, p); gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear); gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear); gl.BindTexture(TextureTarget.Texture2DArray, 0); // Fallback alpha atlas: 1x1 white, no layers tracked uint alphaTex = gl.GenTexture(); gl.BindTexture(TextureTarget.Texture2DArray, alphaTex); gl.TexImage3D(TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8, 1, 1, 1, 0, GLPixelFormat.Rgba, PixelType.UnsignedByte, null); fixed (byte* p = white) gl.TexSubImage3D(TextureTarget.Texture2DArray, 0, 0, 0, 0, 1, 1, 1, GLPixelFormat.Rgba, PixelType.UnsignedByte, p); gl.BindTexture(TextureTarget.Texture2DArray, 0); return new TerrainAtlas( gl, bindless, tex, new Dictionary { [0] = 0u }, 1, alphaTex, 1, Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty()); } /// /// A.5 T22.5: update GL_TEXTURE_MAX_ANISOTROPY on the terrain atlas at /// runtime (called by when /// the user changes Quality preset mid-session). Idempotent — calling with /// the same level as the current setting is safe and produces no visual /// change. The texture must not be resident-bindless when its parameters /// are mutated; we temporarily make it non-resident if needed. /// public void SetAnisotropic(int level) { // If bindless handles are live we must make them non-resident before // mutating texture state, then re-resident after. bool wasResident = _handlesGenerated && _bindless is not null; if (wasResident) { _bindless!.MakeNonResident(_terrainHandle); // Alpha texture is not affected by anisotropic but we must keep // residency symmetric — re-generate both handles after. _bindless.MakeNonResident(_alphaHandle); _handlesGenerated = false; } _gl.BindTexture(TextureTarget.Texture2DArray, GlTexture); // GL_TEXTURE_MAX_ANISOTROPY = 0x84FE _gl.TexParameter(TextureTarget.Texture2DArray, (TextureParameterName)0x84FE, (float)level); _gl.BindTexture(TextureTarget.Texture2DArray, 0); // Re-generate bindless handles if they were live before. if (wasResident) { // GetBindlessHandles regenerates and makes resident. _ = GetBindlessHandles(); } Console.WriteLine($"TerrainAtlas: anisotropic updated to {level}x"); } 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); } }