From 8c6563cada8f4128c79777a739e5a57735f279b3 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 2 Sep 2026 20:31:59 +0200 Subject: [PATCH] fix(render): pair cell-shell index segments with their reordered batches The S1 review round ordered cell-shell batches by source surface index in ObjectMeshManager.UploadGfxObjMeshData but left the index segments handed to the arena in TextureBatches dictionary order. FirstIndex is positional over the segments, so every cell shell's batch pointed at another batch's index range: magenta placeholder walls, stretched textures, and missing faces in every dungeon and house (G1 FAIL 2026-09-02). One upload order is now computed once and used by the count, fill, and batch loops alike. Co-Authored-By: Claude Fable 5.1 --- .../Rendering/Wb/ObjectMeshManager.cs | 49 ++++++++++--------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs b/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs index c3a0e74a..f0fd378e 100644 --- a/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs +++ b/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs @@ -2052,9 +2052,9 @@ namespace AcDream.App.Rendering.Wb /// DrawMesh @0x0059D4A0 draws them in that order). Runs once per /// mesh upload; the allocation is not per-frame. /// - private static IEnumerable<((int Width, int Height, TextureFormat Format) Format, TextureBatchData Batch)> OrderedUploadBatches(ObjectMeshData meshData) + internal static List<((int Width, int Height, TextureFormat Format) Format, TextureBatchData Batch)> OrderedUploadBatches(ObjectMeshData meshData) { - var pairs = new List<((int Width, int Height, TextureFormat Format), TextureBatchData)>(); + var pairs = new List<((int Width, int Height, TextureFormat Format) Format, TextureBatchData Batch)>(); bool cellShell = false; foreach (var (format, batches) in meshData.TextureBatches) { @@ -2065,7 +2065,7 @@ namespace AcDream.App.Rendering.Wb } } if (cellShell) - return pairs.OrderBy(p => p.Item2.SourceSurfaceIndex); // stable: OrderBy preserves storage order on ties + return pairs.OrderBy(p => p.Batch.SourceSurfaceIndex).ToList(); // stable: OrderBy preserves storage order on ties return pairs; } @@ -2081,16 +2081,22 @@ namespace AcDream.App.Rendering.Wb // every batch twice (a per-batch ToArray plus an unsized // SelectMany growth) — megabytes of transient garbage per mesh on // the render thread. + // ONE upload order for this mesh. The index segments handed to the + // arena, the per-batch FirstIndex, and the renderBatches list are + // all positional over this same sequence, so it is computed once + // and used by every loop below. (The S1 review round ordered only + // the batch loop and desynchronized every cell shell's geometry + // from its texture; G1 failed on it. Never split these again.) + List<((int Width, int Height, TextureFormat Format) Format, TextureBatchData Batch)> uploadOrder = + OrderedUploadBatches(meshData); + int totalIndexCount = 0; int nonEmptyBatchCount = 0; - foreach (List formatBatches in meshData.TextureBatches.Values) + foreach (var (_, formatBatch) in uploadOrder) { - foreach (TextureBatchData formatBatch in formatBatches) - { - if (formatBatch.Indices.Count == 0) continue; - totalIndexCount = checked(totalIndexCount + formatBatch.Indices.Count); - nonEmptyBatchCount++; - } + if (formatBatch.Indices.Count == 0) continue; + totalIndexCount = checked(totalIndexCount + formatBatch.Indices.Count); + nonEmptyBatchCount++; } var cpuIndices = new ushort[totalIndexCount]; @@ -2098,17 +2104,14 @@ namespace AcDream.App.Rendering.Wb { int fillOffset = 0; int segmentIndex = 0; - foreach (List formatBatches in meshData.TextureBatches.Values) + foreach (var (_, formatBatch) in uploadOrder) { - foreach (TextureBatchData formatBatch in formatBatches) - { - int count = formatBatch.Indices.Count; - if (count == 0) continue; - CollectionsMarshal.AsSpan(formatBatch.Indices) - .CopyTo(cpuIndices.AsSpan(fillOffset, count)); - indexSegments[segmentIndex++] = (fillOffset, count); - fillOffset = checked(fillOffset + count); - } + int count = formatBatch.Indices.Count; + if (count == 0) continue; + CollectionsMarshal.AsSpan(formatBatch.Indices) + .CopyTo(cpuIndices.AsSpan(fillOffset, count)); + indexSegments[segmentIndex++] = (fillOffset, count); + fillOffset = checked(fillOffset + count); } } @@ -2147,9 +2150,9 @@ namespace AcDream.App.Rendering.Wb // (ConstructMesh @0x0059DFA0 attribute-range scan, DrawMesh // @0x0059D4A0 ascending subset loop). So cell-shell batches // are uploaded in that order; ordinary GfxObj meshes keep the - // storage order. This runs once per mesh at upload time, not - // per frame. - foreach (var (format, batch) in OrderedUploadBatches(meshData)) + // storage order. `uploadOrder` is the SAME sequence the index + // segments above were filled from; the two must never diverge. + foreach (var (format, batch) in uploadOrder) { { if (batch.Indices.Count == 0) continue;