perf(render) #429: allocation-exact streamed-mesh completion
UploadGfxObjMeshData built every completed mesh's index data three-plus times over in LINQ transients (per-batch Indices.ToArray copies plus an unsized SelectMany growth) on the render thread, up to the per-frame upload budget. The conversion now fills one exact-size retained CPUIndices array (the same one the B.4b pick path keeps) and hands the shared arena (offset, count) segments of it; CPUPositions fills by a direct pre-sized loop; the Sum/Any/FirstOrDefault transients are gone. GlobalMeshBuffer.UploadMesh takes the segment form — the staged bytes per batch are unchanged. Gate: a warmed completion must allocate near its retained-copy size (MeshPipelineDeviceSeamTests). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
5b9d0260bb
commit
0330fcd0d1
3 changed files with 183 additions and 34 deletions
|
|
@ -207,7 +207,10 @@ public sealed class MeshPipelineDeviceSeamTests
|
|||
vertices[2].Position = new System.Numerics.Vector3(7f, 8f, 9f);
|
||||
ushort[] indices = [0, 1, 2];
|
||||
|
||||
GlobalMeshAllocation allocation = arena.UploadMesh(vertices, [indices]);
|
||||
GlobalMeshAllocation allocation = arena.UploadMesh(
|
||||
vertices,
|
||||
indices,
|
||||
[(0, indices.Length)]);
|
||||
|
||||
Assert.Equal(3, allocation.Vertices.Length);
|
||||
Assert.Equal(3, allocation.Indices.Length);
|
||||
|
|
@ -229,6 +232,75 @@ public sealed class MeshPipelineDeviceSeamTests
|
|||
System.Runtime.InteropServices.MemoryMarshal.Cast<byte, ushort>(indexBytes).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #429 allocation gate (I1 style). Completing a prepared mesh on the
|
||||
/// render thread must allocate near its retained pick-copy size
|
||||
/// (CPUPositions + CPUIndices), not multiples of it. The regression this
|
||||
/// pins: the upload conversion ran LINQ chains — a per-batch
|
||||
/// <c>Indices.ToArray()</c> plus an unsized <c>SelectMany().ToArray()</c>
|
||||
/// — that materialized every index three-plus times in transient garbage
|
||||
/// per completed mesh, on the render thread, up to the per-frame upload
|
||||
/// budget.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AWarmedMeshCompletionAllocatesNearItsRetainedCopySize()
|
||||
{
|
||||
using var device = new RecordingGpuDevice();
|
||||
using ObjectMeshManager manager = Build(device, modernPath: true);
|
||||
|
||||
// Warm: an identically shaped mesh grows the arena, the atlas family,
|
||||
// and every pool the completion path touches.
|
||||
Assert.NotNull(manager.UploadMeshData(
|
||||
CreateLargeMeshData(0x0100AA01u, surfaceSeed: 0x08000000u)));
|
||||
|
||||
ObjectMeshData meshData =
|
||||
CreateLargeMeshData(0x0100AA02u, surfaceSeed: 0x08001000u);
|
||||
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||
ObjectRenderData? uploaded = manager.UploadMeshData(meshData);
|
||||
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
|
||||
Assert.NotNull(uploaded);
|
||||
long retained =
|
||||
(long)uploaded!.CPUIndices.Length * sizeof(ushort)
|
||||
+ (long)uploaded.CPUPositions.Length * 3 * sizeof(float);
|
||||
// Sanity: the fixture is actually index-heavy enough to discriminate.
|
||||
Assert.True(retained >= 480_000, $"fixture retained only {retained} bytes");
|
||||
// The LINQ regression allocates over 3x the index bytes and fails
|
||||
// this bound by more than a megabyte.
|
||||
long bound = retained + retained / 2 + 128 * 1024;
|
||||
Assert.True(
|
||||
allocated < bound,
|
||||
$"A warmed mesh completion allocated {allocated} bytes "
|
||||
+ $"(retained copies {retained}, bound {bound}).");
|
||||
}
|
||||
|
||||
private static ObjectMeshData CreateLargeMeshData(ulong id, uint surfaceSeed)
|
||||
{
|
||||
const int vertexCount = 1024;
|
||||
const int batchCount = 4;
|
||||
const int indicesPerBatch = 60_000;
|
||||
var data = new ObjectMeshData
|
||||
{
|
||||
ObjectId = id,
|
||||
Vertices = new VertexPositionNormalTexture[vertexCount],
|
||||
};
|
||||
var batches = new System.Collections.Generic.List<TextureBatchData>(batchCount);
|
||||
for (int b = 0; b < batchCount; b++)
|
||||
{
|
||||
var indices = new System.Collections.Generic.List<ushort>(indicesPerBatch);
|
||||
for (int i = 0; i < indicesPerBatch; i++)
|
||||
indices.Add((ushort)((i + b) % vertexCount));
|
||||
batches.Add(new TextureBatchData
|
||||
{
|
||||
Key = new TextureKey { SurfaceId = surfaceSeed + (uint)b },
|
||||
TextureData = new byte[8 * 8 * 4],
|
||||
Indices = indices,
|
||||
});
|
||||
}
|
||||
data.TextureBatches[(8, 8, TextureFormat.RGBA8)] = batches;
|
||||
return data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The production Vulkan implementation of the seam, checked against the
|
||||
/// same surface. Its two capability flags answer true because what they
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue