feat(render #53): cache-miss populate on first frame for static entities
Restructures Draw's per-entity loop: animated entities still skip the cache entirely, but static entities now collect their classification into _populateScratch and call cache.Populate at the end of the iteration. Cache fast-path (skip slow classification on cache hit) lands in Task 10. This intermediate state is verifiable: behavior unchanged, but the cache is being populated as entities render. Diagnostic-friendly split. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
28513eae88
commit
2f489a83a7
1 changed files with 25 additions and 2 deletions
|
|
@ -121,6 +121,11 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
// of GC pressure on the render thread under the original T17 shape.
|
// of GC pressure on the render thread under the original T17 shape.
|
||||||
private readonly List<(WorldEntity Entity, int MeshRefIndex, uint LandblockId)> _walkScratch = new();
|
private readonly List<(WorldEntity Entity, int MeshRefIndex, uint LandblockId)> _walkScratch = new();
|
||||||
|
|
||||||
|
// Tier 1 cache (#53) — per-entity classification collector. Reused across
|
||||||
|
// frames; cleared once per static entity inside Draw. Animated entities
|
||||||
|
// skip this scratch entirely (collector = null).
|
||||||
|
private readonly List<CachedBatch> _populateScratch = new();
|
||||||
|
|
||||||
// Per-entity-cull AABB radius. Conservative — covers most entities; large
|
// Per-entity-cull AABB radius. Conservative — covers most entities; large
|
||||||
// outliers (long banners, tall columns) are still landblock-culled.
|
// outliers (long banners, tall columns) are still landblock-culled.
|
||||||
private const float PerEntityCullRadius = 5.0f;
|
private const float PerEntityCullRadius = 5.0f;
|
||||||
|
|
@ -385,6 +390,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
Matrix4x4.CreateFromQuaternion(entity.Rotation) *
|
Matrix4x4.CreateFromQuaternion(entity.Rotation) *
|
||||||
Matrix4x4.CreateTranslation(entity.Position);
|
Matrix4x4.CreateTranslation(entity.Position);
|
||||||
|
|
||||||
|
bool isAnimated = animatedEntityIds?.Contains(entity.Id) == true;
|
||||||
|
|
||||||
// Compute palette-override hash ONCE per entity (perf #4).
|
// Compute palette-override hash ONCE per entity (perf #4).
|
||||||
// Reused across every (part, batch) lookup so the FNV-1a fold
|
// Reused across every (part, batch) lookup so the FNV-1a fold
|
||||||
// over SubPalettes runs once instead of N times. Zero when the
|
// over SubPalettes runs once instead of N times. Zero when the
|
||||||
|
|
@ -410,6 +417,14 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
}
|
}
|
||||||
if (anyVao == 0) anyVao = renderData.VAO;
|
if (anyVao == 0) anyVao = renderData.VAO;
|
||||||
|
|
||||||
|
// Cache-miss path (animated entities skip cache entirely).
|
||||||
|
// Static entities collect into _populateScratch on the first frame
|
||||||
|
// they're visible, so the cache has fresh data for the next frame.
|
||||||
|
// Task 10 will add the cache-hit fast path that skips slow
|
||||||
|
// classification when an entry already exists.
|
||||||
|
var collector = isAnimated ? null : _populateScratch;
|
||||||
|
collector?.Clear();
|
||||||
|
|
||||||
bool drewAny = false;
|
bool drewAny = false;
|
||||||
if (renderData.IsSetup && renderData.SetupParts.Count > 0)
|
if (renderData.IsSetup && renderData.SetupParts.Count > 0)
|
||||||
{
|
{
|
||||||
|
|
@ -422,17 +437,25 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
entityWorld, meshRef.PartTransform, partTransform);
|
entityWorld, meshRef.PartTransform, partTransform);
|
||||||
|
|
||||||
var restPose = partTransform * meshRef.PartTransform;
|
var restPose = partTransform * meshRef.PartTransform;
|
||||||
ClassifyBatches(partData, partGfxObjId, model, entity, meshRef, palHash, metaTable, restPose);
|
ClassifyBatches(partData, partGfxObjId, model, entity, meshRef, palHash, metaTable, restPose, collector);
|
||||||
drewAny = true;
|
drewAny = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var model = meshRef.PartTransform * entityWorld;
|
var model = meshRef.PartTransform * entityWorld;
|
||||||
ClassifyBatches(renderData, gfxObjId, model, entity, meshRef, palHash, metaTable, restPose: meshRef.PartTransform);
|
ClassifyBatches(renderData, gfxObjId, model, entity, meshRef, palHash, metaTable, restPose: meshRef.PartTransform, collector: collector);
|
||||||
drewAny = true;
|
drewAny = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (collector is not null && collector.Count > 0)
|
||||||
|
{
|
||||||
|
// Populate cache for static entity on cache-miss.
|
||||||
|
// Each entity classifies once at first visibility; subsequent
|
||||||
|
// frames will hit the fast path (added in Task 10).
|
||||||
|
_cache.Populate(entity.Id, landblockId, collector.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
if (diag && drewAny) _entitiesDrawn++;
|
if (diag && drewAny) _entitiesDrawn++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue