diff --git a/tests/AcDream.App.Tests/Rendering/Wb/MeshPipelineDeviceSeamTests.cs b/tests/AcDream.App.Tests/Rendering/Wb/MeshPipelineDeviceSeamTests.cs index 48cdb2c5..98b66f65 100644 --- a/tests/AcDream.App.Tests/Rendering/Wb/MeshPipelineDeviceSeamTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Wb/MeshPipelineDeviceSeamTests.cs @@ -232,6 +232,64 @@ public sealed class MeshPipelineDeviceSeamTests System.Runtime.InteropServices.MemoryMarshal.Cast(indexBytes).ToArray()); } + /// + /// Campaign OVERHAUL S1 (G1 FAIL 2026-09-02). Cell-shell batches upload in + /// ascending source surface index (retail's built-EnvCell subset order, + /// ConstructMesh @0x0059DFA0 / DrawMesh @0x0059D4A0), which differs from + /// the (Width,Height,Format) storage order the index segments used to be + /// filled in. The first fix ordered only the batch list, so batch i took + /// segment i's index range from a different batch: magenta walls, + /// stretched textures, missing faces in every dungeon. This pins that each + /// uploaded batch's FirstIndex points at ITS OWN indices in the arena, with + /// a fixture whose storage order is the reverse of its surface order. + /// + [Fact] + public void CellShellBatchesKeepTheirOwnIndexRangesWhenReorderedBySurfaceIndex() + { + using var device = new RecordingGpuDevice(); + using ObjectMeshManager manager = Build(device, modernPath: true); + GlobalMeshBuffer arena = manager.GlobalBuffer!; + + static AcDream.Content.TextureBatchData CellBatch(int slot, uint surfaceId, int size, ushort[] indices) => new() + { + Key = new AcDream.Content.TextureKey { SurfaceId = surfaceId }, + TextureData = new byte[size * size * 4], + Indices = [.. indices], + IsCellShell = true, + SourceSurfaceIndex = slot, + CullMode = DatReaderWriter.Enums.CullMode.Clockwise, + }; + + // Two storage groups (two texture sizes) so dictionary insertion order + // puts slot 5 first; retail order is slot 2 first. + var mesh = new AcDream.Content.ObjectMeshData + { + ObjectId = 0x1_0000_0000UL | 0xF4180104UL, + Vertices = new VertexPositionNormalTexture[6], + TextureBatches = + { + [(8, 8, Chorizite.Core.Render.Enums.TextureFormat.RGBA8)] = [CellBatch(5, 0x08000005u, 8, [0, 1, 2])], + [(16, 16, Chorizite.Core.Render.Enums.TextureFormat.RGBA8)] = [CellBatch(2, 0x08000002u, 16, [3, 4, 5])], + }, + }; + + ObjectRenderData data = Assert.IsType(manager.UploadMeshData(mesh)); + + Assert.Equal(2, data.Batches.Count); + Assert.Equal(0x08000002u, data.Batches[0].Key.SurfaceId); // ascending surface index wins + Assert.Equal(0x08000005u, data.Batches[1].Key.SurfaceId); + + foreach (ObjectRenderBatch batch in data.Batches) + { + ushort[] expected = batch.Key.SurfaceId == 0x08000002u ? [3, 4, 5] : [0, 1, 2]; + var bytes = new byte[batch.IndexCount * sizeof(ushort)]; + arena.IndexStore!.Read((long)batch.FirstIndex * sizeof(ushort), bytes); + Assert.Equal( + expected, + System.Runtime.InteropServices.MemoryMarshal.Cast(bytes).ToArray()); + } + } + /// /// #429 allocation gate (I1 style). Completing a prepared mesh on the /// render thread must allocate near its retained pick-copy size