using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Numerics; using AcDream.App.Rendering; using AcDream.App.Rendering.Gpu; using AcDream.App.Rendering.Gpu.Vk; using AcDream.App.Tests.Rendering.Gpu; using AcDream.Content; using AcDream.Core.Terrain; using DatReaderWriter; using DatReaderWriter.DBObjs; using DatReaderWriter.Lib.IO; using Xunit; namespace AcDream.App.Tests.Rendering; /// /// S3 chunk 3 fix round 1 (§9.6 F1): TerrainModernRenderer._idToSlot /// is keyed by the DAT landblock id 0xXXYYFFFF /// (LandblockRenderPublisher.LandblockId => Build.Landblock.LandblockId, /// which stores under /// verbatim), while the walk hands 0xXXYY0000 /// (WalkLandBlock.LandblockId = bx<<24 | by<<16). Before /// this fix, normalized /// nothing, so every walk-path lookup missed and the walk drew NO terrain. /// This pins the fix end-to-end through the REAL RHI submission path (a /// , not a mock of the lookup alone) — an /// entry keyed by the walk's 0xA9B40000 convention must resolve the /// SAME slot published /// under the DAT's 0xA9B4FFFF convention, and an entry for a /// genuinely unknown landblock must be a silent per-entry no-op rather than /// a thrown exception or a spurious draw. /// public sealed class TerrainWalkSlotKeyNormalizationTests : IDisposable { private readonly RecordingGpuDevice _device = new(); private readonly GpuDeviceFrameLifetime _frameLifetime; private readonly VulkanWorldPassScope _scope = new(sampleCount: 1); private readonly TerrainAtlas _atlas; private readonly TerrainModernRenderer _terrain; public TerrainWalkSlotKeyNormalizationTests() { _frameLifetime = new GpuDeviceFrameLifetime(_device); // A Region with no TerrainInfo takes BuildBackendNeutral's own // documented single-white-fallback-layer branch — no installed DAT // needed, and this suite stays hermetic. _atlas = TerrainAtlas.BuildBackendNeutral(_device, new EmptyRegionDats()); _terrain = new TerrainModernRenderer(_device, _frameLifetime, _scope, _atlas, _device.Retirement); } private DrawScope BeginDraw() { _frameLifetime.BeginFrame(); IGpuFrame frame = _frameLifetime.CurrentFrame!; IGpuPassEncoder pass = frame.BeginPass( GpuPassDescription.BackbufferClear( "s3-chunk3-fix-round-1-f1", Vector4.Zero, sampleCount: 1)); IDisposable publication = _scope.Publish(pass); _device.Clear(); return new DrawScope(frame, pass, publication); } private readonly struct DrawScope( IGpuFrame frame, IGpuPassEncoder pass, IDisposable publication) : IDisposable { public IGpuFrame Frame { get; } = frame; public IGpuPassEncoder Pass { get; } = pass; public void Dispose() => publication.Dispose(); } private static LandblockMeshData MakeFullSizeMesh() { var vertices = new TerrainVertex[LandblockMesh.VerticesPerLandblock]; var indices = new uint[LandblockMesh.VerticesPerLandblock]; for (int i = 0; i < indices.Length; i++) indices[i] = (uint)i; return new LandblockMeshData(vertices, indices); } [Fact] public void DrawLandCells_ResolvesTheDatSlotFromTheWalksLowWordZeroLandblockId() { // Stored under the DAT id (LandblockRenderPublisher's own // convention, low word 0xFFFF). _terrain.AddLandblock(0xA9B4FFFFu, MakeFullSizeMesh(), Vector3.Zero); using DrawScope draw = BeginDraw(); _terrain.BeginFrame(frameSlot: 0); // The walk's own convention (WalkLandBlock.LandblockId), low word // 0x0000 — F1's normalization must still find the slot above. _terrain.DrawLandCells( Matrix4x4.Identity, new (uint LandblockId, int SideCellCount, int CellIndex)[] { (0xA9B40000u, 8, 0) }); GpuRecordedMultiDrawIndirect call = Assert.Single( _device.Calls.OfType()); Assert.Equal(1u, call.DrawCount); } [Fact] public void DrawLandCells_UnknownLandblockIsASilentNoOp() { using DrawScope draw = BeginDraw(); _terrain.BeginFrame(frameSlot: 0); _terrain.DrawLandCells( Matrix4x4.Identity, new (uint LandblockId, int SideCellCount, int CellIndex)[] { (0xDEAD0000u, 8, 0) }); Assert.Empty(_device.Calls.OfType()); Assert.Equal(0, _terrain.WalkDrawCount); Assert.Equal(0, _terrain.WalkVisibleSlotCount); } [Fact] public void DrawLandCells_KnownAndUnknownLandblocksInOneBatch_SubmitsOnlyTheKnownEntry() { _terrain.AddLandblock(0xA9B4FFFFu, MakeFullSizeMesh(), Vector3.Zero); using DrawScope draw = BeginDraw(); _terrain.BeginFrame(frameSlot: 0); _terrain.DrawLandCells( Matrix4x4.Identity, new (uint LandblockId, int SideCellCount, int CellIndex)[] { (0xDEAD0000u, 8, 0), (0xA9B40000u, 8, 1), }); GpuRecordedMultiDrawIndirect call = Assert.Single( _device.Calls.OfType()); Assert.Equal(1u, call.DrawCount); Assert.Equal(1, _terrain.WalkDrawCount); Assert.Equal(1, _terrain.WalkVisibleSlotCount); } public void Dispose() { _terrain.Dispose(); _atlas.Dispose(); _device.Dispose(); } /// Minimal hermetic — every read /// misses except Get<Region>(0x13000000), which returns a /// Region with no TerrainInfo so takes its documented /// single-white-fallback-layer branch instead of throwing. private sealed class EmptyRegionDats : IDatReaderWriter { private readonly Region _region = new(); private readonly StubDatabase _portal = new(); private readonly StubDatabase _highRes = new(); private readonly StubDatabase _language = new(); private readonly StubDatabase _cell = new(); public string SourceDirectory => string.Empty; public IDatDatabase Portal => _portal; public IDatDatabase Cell => _cell; public ReadOnlyDictionary CellRegions { get; } = new(new Dictionary()); public IDatDatabase HighRes => _highRes; public IDatDatabase Language => _language; public IDatDatabase Local => _language; 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 { if (typeof(T) == typeof(Region) && fileId == 0x13000000u) return (T)(object)_region; return default; } public bool TryGet( uint fileId, [MaybeNullWhen(false)] out T value) where T : IDBObj { value = Get(fileId); return value is not null; } public void Dispose() { } private sealed class StubDatabase : IDatDatabase { 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 { 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() { } } } }