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 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-02 20:31:59 +02:00
parent e2543d0ef0
commit 8c6563cada

View file

@ -2052,9 +2052,9 @@ namespace AcDream.App.Rendering.Wb
/// <c>DrawMesh</c> @0x0059D4A0 draws them in that order). Runs once per
/// mesh upload; the allocation is not per-frame.
/// </summary>
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<TextureBatchData> 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<TextureBatchData> 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;