using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using AcDream.App.Rendering; using AcDream.App.Rendering.Gpu; using AcDream.App.Rendering.Wb; using AcDream.App.Tests.Rendering.Gpu; using AcDream.Content; using DatReaderWriter; using DatReaderWriter.DBObjs; using DatReaderWriter.Enums; using DatReaderWriter.Lib.IO; using DatReaderWriter.Types; using Xunit; namespace AcDream.App.Tests.Rendering; /// /// Campaign VM VM1 review fix: pins the two facts the #226 note relies on /// for retail's mip-driven attenuation to actually be true — TerrainAtlas /// .TryCreateDetailTexture (~TerrainAtlas.cs:488-556) must upload a FULL /// mip chain, and it must be sampled with the repeat/linear world sampler, /// or the "attenuation is the sampler's linear mip chain" claim in /// mesh_detail.vert's header comment and the VM1 commit is unverified. /// Drives the private method directly (reflection, same pattern as /// EnvCellRendererTests' private-method tests) against a synthetic /// PFID_A8R8G8B8 RenderSurface, so no installed DAT is required — this lane /// stays hermetic. /// public sealed class TerrainAtlasDetailTextureTests { private const uint SurfaceTextureId = 0x05001787u; private const uint RenderSurfaceId = 0x06006D58u; [Fact] public void DetailTexture_UploadsFullMipChainAndUsesRepeatLinearSampler() { const int width = 4; const int height = 4; using var device = new RecordingGpuDevice(); device.Clear(); var dats = new FakeDetailTextureDats(); dats.Register(new SurfaceTexture { Textures = new List> { RenderSurfaceId }, }, SurfaceTextureId); dats.Register(new RenderSurface { Width = width, Height = height, Format = PixelFormat.PFID_A8R8G8B8, SourceData = new byte[width * height * 4], }, RenderSurfaceId); var terrain = new TMTerrainDesc { TerrainTex = new TerrainTex { DetailTextureId = SurfaceTextureId, DetailTexTiling = 4u, }, }; // The sampler is created from the PRODUCTION constant so that changing // TerrainAtlas's choice (e.g. to a clamp sampler) fails the property // assertions below rather than being forwarded unnoticed. IGpuSampler sampler = device.CreateSampler(TerrainAtlas.DetailSamplerDescription); MethodInfo method = typeof(TerrainAtlas).GetMethod( "TryCreateDetailTexture", BindingFlags.NonPublic | BindingFlags.Static)!; object? result = method.Invoke( null, new object[] { device, dats, sampler, terrain, "building" }); Assert.NotNull(result); // Full mip chain: TerrainAtlas.cs sizes MipLevelCount from // RhiWorldTextureArray.MipLevelsFor(decoded.Width, decoded.Height) and // then calls GenerateMipChain() — not a single-level upload. RecordingGpuTexture texture = Assert.Single(device.CreatedTextures); Assert.Equal(width, texture.Width); Assert.Equal(height, texture.Height); int expectedMipLevels = RhiWorldTextureArray.MipLevelsFor(width, height); Assert.True(expectedMipLevels > 1, "the test fixture must exercise a real mip chain, not a 1x1 edge case"); Assert.Equal(expectedMipLevels, texture.MipLevelCount); Assert.True(texture.MipChainGenerated); // Repeat/linear sampler: the exact sampler GpuBindingModel world // draws use, not WorldClamp (the alpha atlas' sampler) or any // point-filtered UI sampler. GpuRecordedTextureRegistration registration = Assert.Single( device.Calls.OfType()); Assert.Equal(TerrainAtlas.DetailSamplerDescription, registration.Sampler); Assert.Equal(GpuFilter.Linear, registration.Sampler.MinFilter); Assert.Equal(GpuFilter.Linear, registration.Sampler.MagFilter); Assert.Equal(GpuMipFilter.Linear, registration.Sampler.MipFilter); Assert.Equal(GpuAddressMode.Repeat, registration.Sampler.AddressU); Assert.Equal(GpuAddressMode.Repeat, registration.Sampler.AddressV); } /// /// Minimal synthetic — same shape as the /// NoopDatReaderWriter pattern already used for hermetic tests /// (LiveEntityNetworkOnPositionCollapseMatrixTests), except Get /// resolves from an explicit id->object map instead of always missing. /// Every other member is unreachable by TryCreateDetailTexture and throws /// if that assumption ever changes. /// private sealed class FakeDetailTextureDats : IDatReaderWriter { private readonly Dictionary _objects = new(); public void Register(T obj, uint id) where T : IDBObj => _objects[id] = obj; public string SourceDirectory => string.Empty; public IDatDatabase Portal => throw new NotSupportedException(); public IDatDatabase Cell => throw new NotSupportedException(); public ReadOnlyDictionary CellRegions { get; } = new(new Dictionary()); public IDatDatabase HighRes => throw new NotSupportedException(); 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() { } } }