perf(bake): stream catalog and bound validation residency

This commit is contained in:
Erik 2026-07-24 14:05:30 +02:00
parent b7b9aaa9dd
commit 90bf6bbf45
7 changed files with 217 additions and 84 deletions

View file

@ -53,51 +53,94 @@ public sealed class EnvCellBakeCatalog
ArgumentNullException.ThrowIfNull(sources);
ArgumentNullException.ThrowIfNull(computeIdentity);
var byGeometry = new Dictionary<ulong, MutableGroup>();
var fileIds = new HashSet<uint>();
int cellCount = 0;
var builder = new EnvCellBakeCatalogBuilder(computeIdentity);
foreach (var source in sources)
builder.Add(source);
return builder.Build();
}
foreach (var source in sources.OrderBy(source => source.FileId))
internal static EnvCellBakeCatalog Create(
IReadOnlyList<EnvCellGeometryGroup> groups,
int cellCount) =>
new(groups, cellCount);
}
/// <summary>
/// Incremental catalog builder used by the full bake so 729,888 EnvCell
/// records and their surface lists are never retained at once.
/// </summary>
public sealed class EnvCellBakeCatalogBuilder
{
private readonly Func<uint, ushort, IReadOnlyList<ushort>, ulong> _computeIdentity;
private readonly Dictionary<ulong, MutableGroup> _byGeometry = new();
private readonly HashSet<uint> _fileIds = new();
private int _cellCount;
private bool _built;
public EnvCellBakeCatalogBuilder()
: this(EnvCellGeometryIdentity.Compute)
{
}
public EnvCellBakeCatalogBuilder(
Func<uint, ushort, IReadOnlyList<ushort>, ulong> computeIdentity)
{
ArgumentNullException.ThrowIfNull(computeIdentity);
_computeIdentity = computeIdentity;
}
public void Add(EnvCellBakeSource source)
{
if (_built)
throw new InvalidOperationException("the EnvCell bake catalog is already complete");
if (!_fileIds.Add(source.FileId))
throw new InvalidDataException($"duplicate EnvCell file id 0x{source.FileId:X8}");
ulong geometryId = _computeIdentity(
source.EnvironmentId,
source.CellStructure,
source.Surfaces);
if (_byGeometry.TryGetValue(geometryId, out var existing))
{
if (!fileIds.Add(source.FileId))
throw new InvalidDataException($"duplicate EnvCell file id 0x{source.FileId:X8}");
var surfaces = source.Surfaces.ToArray();
ulong geometryId = computeIdentity(
source.EnvironmentId,
source.CellStructure,
surfaces);
if (byGeometry.TryGetValue(geometryId, out var existing))
if (existing.EnvironmentId != source.EnvironmentId ||
existing.CellStructure != source.CellStructure ||
!SurfacesEqual(existing.Surfaces, source.Surfaces))
{
if (existing.EnvironmentId != source.EnvironmentId ||
existing.CellStructure != source.CellStructure ||
!existing.Surfaces.AsSpan().SequenceEqual(surfaces))
{
throw new InvalidDataException(
$"EnvCell geometry identity collision 0x{geometryId:X16}: " +
$"cell 0x{existing.FileIds[0]:X8} and cell 0x{source.FileId:X8} " +
"have different environment/cell-structure/surface tuples");
}
existing.FileIds.Add(source.FileId);
throw new InvalidDataException(
$"EnvCell geometry identity collision 0x{geometryId:X16}: " +
$"cell 0x{existing.FileIds[0]:X8} and cell 0x{source.FileId:X8} " +
"have different environment/cell-structure/surface tuples");
}
else
{
byGeometry.Add(
existing.FileIds.Add(source.FileId);
}
else
{
_byGeometry.Add(
geometryId,
new MutableGroup(
geometryId,
new MutableGroup(
geometryId,
source.EnvironmentId,
source.CellStructure,
surfaces,
new List<uint> { source.FileId }));
}
cellCount++;
source.EnvironmentId,
source.CellStructure,
source.Surfaces.ToArray(),
new List<uint> { source.FileId }));
}
var groups = byGeometry.Values
_cellCount++;
}
public EnvCellBakeCatalog Build()
{
if (_built)
throw new InvalidOperationException("the EnvCell bake catalog is already complete");
_built = true;
foreach (var group in _byGeometry.Values)
group.FileIds.Sort();
var groups = _byGeometry.Values
.Select(group => new EnvCellGeometryGroup(
group.GeometryId,
group.EnvironmentId,
@ -107,7 +150,21 @@ public sealed class EnvCellBakeCatalog
.OrderBy(group => group.FileIds[0])
.ToArray();
return new EnvCellBakeCatalog(groups, cellCount);
return EnvCellBakeCatalog.Create(groups, _cellCount);
}
private static bool SurfacesEqual(
ReadOnlySpan<ushort> expected,
IReadOnlyList<ushort> actual)
{
if (expected.Length != actual.Count)
return false;
for (int i = 0; i < expected.Length; i++)
{
if (expected[i] != actual[i])
return false;
}
return true;
}
private sealed record MutableGroup(