using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using AcDream.App.Rendering; using AcDream.App.Tests.Rendering.Gpu; using AcDream.Content; using DatReaderWriter; using DatReaderWriter.DBObjs; using DatReaderWriter.Enums; using DatReaderWriter.Lib.IO; using Xunit; namespace AcDream.App.Tests.Rendering; /// /// Review fix round finding 6 (docs/plans/2026-09-06-plugin-shelf-and-dat-icons.md /// Slice B): must key its /// render-surface cache by (id, nearest), not id alone. Before this /// fix, a plugin markup <icon did> (nearest:true) resolving the /// SAME id an authored chrome sprite already resolved (nearest:false, via /// ResolveChrome) would silently inherit whichever sampler the FIRST /// caller asked for — 's own glyph atlases already /// call this nearest:true while chrome calls it with no /// nearest argument at all (defaults false), so the collision is a /// real, reachable production shape, not a hypothetical. /// public sealed class TextureCacheRenderSurfaceSamplerKeyTests { private const uint RenderSurfaceId = 0x06006D59u; [Fact] public void SameId_DifferentNearest_ReturnsDistinctHandles_AndBothDisposeCleanly() { var device = new RecordingGpuDevice(); device.Clear(); var dats = new FakeRenderSurfaceDats(); dats.Register(new RenderSurface { Width = 4, Height = 4, Format = PixelFormat.PFID_A8R8G8B8, SourceData = new byte[4 * 4 * 4], }, RenderSurfaceId); var cache = new TextureCache(device, dats); uint linear = cache.GetOrUploadRenderSurface( RenderSurfaceId, out _, out _, nearest: false); uint nearest = cache.GetOrUploadRenderSurface( RenderSurfaceId, out _, out _, nearest: true); Assert.NotEqual(0u, linear); Assert.NotEqual(0u, nearest); Assert.NotEqual(linear, nearest); // Repeat requests for the SAME (id, nearest) pair are cached — no // re-upload, same handle back. Assert.Equal( linear, cache.GetOrUploadRenderSurface(RenderSurfaceId, out _, out _, nearest: false)); Assert.Equal( nearest, cache.GetOrUploadRenderSurface(RenderSurfaceId, out _, out _, nearest: true)); Assert.Equal(2, device.CreatedTextures.Count); cache.Dispose(); // Both textures — not just the first-cached one — were disposed. Assert.All(device.CreatedTextures, texture => Assert.True(texture.IsDisposed)); } /// /// Minimal synthetic , same hermetic-test /// shape as TerrainAtlasDetailTextureTests.FakeDetailTextureDats, /// except is a real (fake) /// rather than a throwing stub — /// calls Portal.TryGet/HighRes.TryGet directly, unlike /// TryCreateDetailTexture's Get-only access pattern. /// private sealed class FakeRenderSurfaceDats : IDatReaderWriter { private readonly Dictionary _objects = new(); public FakeRenderSurfaceDats() { Portal = new FakeDatDatabase(_objects); HighRes = new FakeDatDatabase(new Dictionary()); } public void Register(T obj, uint id) where T : IDBObj => _objects[id] = obj; public string SourceDirectory => string.Empty; public IDatDatabase Portal { get; } public IDatDatabase Cell => throw new NotSupportedException(); public ReadOnlyDictionary CellRegions { get; } = new(new Dictionary()); public IDatDatabase HighRes { get; } public IDatDatabase Language => throw new NotSupportedException(); public IDatDatabase Local => throw new NotSupportedException(); public ReadOnlyDictionary RegionFileMap { get; } = new(new Dictionary()); public int PortalIteration => 0; public int CellIteration => 0; public int HighResIteration => 0; public int LanguageIteration => 0; public bool TryGetFileBytes( uint regionId, uint fileId, ref byte[] bytes, out int bytesRead) { bytesRead = 0; return false; } public IEnumerable GetAllIdsOfType() where T : IDBObj => Array.Empty(); public IEnumerable ResolveId(uint id) => Array.Empty(); public bool TrySave(T obj, int iteration = 0) where T : IDBObj => throw new NotSupportedException(); public bool TrySave(uint regionId, T obj, int iteration = 0) where T : IDBObj => throw new NotSupportedException(); [return: MaybeNull] public T Get(uint fileId) where T : IDBObj => _objects.TryGetValue(fileId, out IDBObj? obj) && obj is T typed ? typed : default; public bool TryGet(uint fileId, [MaybeNullWhen(false)] out T value) where T : IDBObj { if (_objects.TryGetValue(fileId, out IDBObj? obj) && obj is T typed) { value = typed; return true; } value = default; return false; } public void Dispose() { } } private sealed class FakeDatDatabase : IDatDatabase { private readonly Dictionary _objects; public FakeDatDatabase(Dictionary objects) => _objects = objects; public DatDatabase Db => throw new NotSupportedException(); public int Iteration => 0; public IEnumerable GetAllIdsOfType() where T : IDBObj => Array.Empty(); public bool TryGet(uint fileId, [MaybeNullWhen(false)] out T value) where T : IDBObj { if (_objects.TryGetValue(fileId, out IDBObj? obj) && obj is T typed) { value = typed; return true; } value = default; return false; } public bool TryGetFileBytes(uint fileId, [MaybeNullWhen(false)] out byte[] value) { value = null; return false; } public bool TryGetFileBytes(uint fileId, ref byte[] bytes, out int bytesRead) { bytesRead = 0; return false; } public bool TrySave(T obj, int iteration = 0) where T : IDBObj => throw new NotSupportedException(); public void Dispose() { } } }