feat(rendering): establish current-path scene referee

Capture deterministic, landblock-aware fingerprints from the accepted partition only during lifecycle automation. This gives the F/G shadow scene an independent oracle without changing normal draw decisions or adding disabled-path per-entity cost.
This commit is contained in:
Erik 2026-07-24 21:13:11 +02:00
parent 5ecaa5612d
commit b2b67341ac
9 changed files with 845 additions and 5 deletions

View file

@ -30,6 +30,27 @@ namespace AcDream.App.Rendering;
/// </summary>
public static class InteriorEntityPartition
{
internal enum ProjectionClass : byte
{
OutdoorStatic,
CellStatic,
Dynamic,
}
internal interface IObserver
{
void BeginFrame();
void Observe(
uint landblockId,
WorldEntity entity,
ProjectionClass projectionClass);
void Complete(Result result);
void AbortFrame();
}
public sealed class Result
{
public Dictionary<uint, List<WorldEntity>> ByCell { get; } = new();
@ -143,6 +164,79 @@ public static class InteriorEntityPartition
result.PruneEmptyCellBuckets();
}
/// <summary>
/// Current-path referee overload. The observer sees the exact branch that
/// populated each accepted partition entry, including its owning
/// landblock. A null observer is the production fast path and performs no
/// diagnostic allocation or hashing.
/// </summary>
internal static void Partition(
Result result,
HashSet<uint> visibleCells,
IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
IReadOnlyList<WorldEntity> Entities,
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> landblockEntries,
IObserver? observer)
{
if (observer is null)
{
Partition(result, visibleCells, landblockEntries);
return;
}
observer.BeginFrame();
try
{
result.ClearForReuse();
foreach (var entry in landblockEntries)
{
foreach (var e in entry.Entities)
{
if (e.MeshRefs.Count == 0) continue;
// Retail contract: every server-spawned entity is a
// DYNAMIC and draws in the last pass, regardless of cell.
if (e.ServerGuid != 0)
{
result.Dynamics.Add(e);
observer.Observe(
entry.LandblockId,
e,
ProjectionClass.Dynamic);
}
else if (e.ParentCellId is uint cell && IsIndoorCellId(cell))
{
if (!visibleCells.Contains(cell))
continue;
if (!result.ByCell.TryGetValue(cell, out var list))
result.ByCell[cell] = list = new List<WorldEntity>();
list.Add(e);
observer.Observe(
entry.LandblockId,
e,
ProjectionClass.CellStatic);
}
else
{
result.OutdoorStatic.Add(e);
observer.Observe(
entry.LandblockId,
e,
ProjectionClass.OutdoorStatic);
}
}
}
result.PruneEmptyCellBuckets();
observer.Complete(result);
}
catch
{
observer.AbortFrame();
throw;
}
}
/// <summary>Shared indoor classification — keep DrawDynamicsLast, the
/// outside-stage assignment (#118), and the partition in lockstep.</summary>
public static bool IsIndoorCellId(uint cellId)