fix(render #53): incomplete-entity flag must persist across same-entity tuples
User reported (cache enabled, post-c55acdc): drudge statue renders fully
but many trees are missing branches. Cache-disabled A/B run rendered trees
correctly. So the bug is in the cache wiring.
Root cause: c55acdc's `currentEntityIncomplete = false;` reset fired
UNCONDITIONALLY at the top of every iteration. For a tree with MeshRefs
[trunk valid, branches null, leaves valid], the tuple sequence is:
- tuple 0 (trunk): no flag set
- tuple 1 (branches): TryGetRenderData null → set flag, continue
- tuple 2 (leaves): unconditional reset → flag = false (WRONG)
- end-of-entity: flag is false, scratch has trunk+leaves batches but NOT
branches → MaybeFlushOnEntityChange populates a PARTIAL cache entry
- cache hits forever serve trunk+leaves with no branches
Drudge happened to render correctly because its missing MeshRef was at the
END of its MeshRefs list — no later tuple reset the flag.
Adds a per-tuple `prevTupleEntityId` tracker for entity-change detection,
updated UNCONDITIONALLY at end of each tuple (including tuples that skip
via null renderData). The flag-reset block now fires ONLY on actual entity
change. Within the same entity, the flag accumulates across tuples.
Also includes ACDREAM_DISABLE_TIER1_CACHE=1 diagnostic env-var added
inline (was stashed previously) for future A/B testing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c55acdc3d5
commit
f928e66119
1 changed files with 32 additions and 5 deletions
|
|
@ -74,6 +74,11 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
// miss-populate / hit-fast-path through the loop.
|
// miss-populate / hit-fast-path through the loop.
|
||||||
private readonly EntityClassificationCache _cache;
|
private readonly EntityClassificationCache _cache;
|
||||||
|
|
||||||
|
// ACDREAM_DISABLE_TIER1_CACHE=1 A/B diagnostic — forces every static
|
||||||
|
// entity through the slow path. Read once in ctor.
|
||||||
|
private readonly bool _tier1CacheDisabled =
|
||||||
|
string.Equals(Environment.GetEnvironmentVariable("ACDREAM_DISABLE_TIER1_CACHE"), "1", StringComparison.Ordinal);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A.5 T22.5: gate for GL_SAMPLE_ALPHA_TO_COVERAGE around the opaque pass.
|
/// A.5 T22.5: gate for GL_SAMPLE_ALPHA_TO_COVERAGE around the opaque pass.
|
||||||
/// Default true matches T20 behavior. Set false for Low/Medium presets that
|
/// Default true matches T20 behavior. Set false for Low/Medium presets that
|
||||||
|
|
@ -423,6 +428,14 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
// some parts missing permanently. Reset on entity change.
|
// some parts missing permanently. Reset on entity change.
|
||||||
bool currentEntityIncomplete = false;
|
bool currentEntityIncomplete = false;
|
||||||
|
|
||||||
|
// Per-tuple entity tracker used purely for entity-change detection.
|
||||||
|
// Updated UNCONDITIONALLY at end of every tuple (including tuples that
|
||||||
|
// skip via null renderData), so the flag-reset block below correctly
|
||||||
|
// distinguishes "new entity" from "same entity, different tuple."
|
||||||
|
// populateEntityId can't be used for this because it's only set after
|
||||||
|
// a successful slow-path classification.
|
||||||
|
uint? prevTupleEntityId = null;
|
||||||
|
|
||||||
foreach (var (entity, partIdx, landblockId) in _walkScratch)
|
foreach (var (entity, partIdx, landblockId) in _walkScratch)
|
||||||
{
|
{
|
||||||
if (diag) _entitiesSeen++;
|
if (diag) _entitiesSeen++;
|
||||||
|
|
@ -454,12 +467,26 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
// the tracker so MaybeFlushOnEntityChange sees the cleaned state
|
// the tracker so MaybeFlushOnEntityChange sees the cleaned state
|
||||||
// and no-ops for this entity. Reset the incomplete flag for the
|
// and no-ops for this entity. Reset the incomplete flag for the
|
||||||
// new entity so each one gets a fresh measurement.
|
// new entity so each one gets a fresh measurement.
|
||||||
if (populateEntityId.HasValue && populateEntityId.Value != entity.Id && currentEntityIncomplete)
|
//
|
||||||
|
// CRITICAL: the flag reset must fire ONLY on entity change, not
|
||||||
|
// every tuple. Resetting per-tuple within the same entity would
|
||||||
|
// undo a null-renderData flag set by a previous tuple of the same
|
||||||
|
// entity → if the missing MeshRef sits in the MIDDLE of the
|
||||||
|
// entity's MeshRefs list, a later valid tuple's reset would
|
||||||
|
// re-mark the entity "complete" and let partial data populate
|
||||||
|
// the cache. Trees with [trunk valid, branches null, leaves
|
||||||
|
// valid] hit this exactly — branches never recover.
|
||||||
|
bool isNewEntity = !prevTupleEntityId.HasValue || prevTupleEntityId.Value != entity.Id;
|
||||||
|
if (isNewEntity)
|
||||||
|
{
|
||||||
|
if (populateEntityId.HasValue && currentEntityIncomplete)
|
||||||
{
|
{
|
||||||
_populateScratch.Clear();
|
_populateScratch.Clear();
|
||||||
populateEntityId = null;
|
populateEntityId = null;
|
||||||
}
|
}
|
||||||
currentEntityIncomplete = false;
|
currentEntityIncomplete = false;
|
||||||
|
}
|
||||||
|
prevTupleEntityId = entity.Id;
|
||||||
|
|
||||||
// Flush-on-entity-change: if the previous entity accumulated any
|
// Flush-on-entity-change: if the previous entity accumulated any
|
||||||
// batches AND this iteration is for a different entity, populate
|
// batches AND this iteration is for a different entity, populate
|
||||||
|
|
@ -489,7 +516,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
// ApplyCacheHit, sets lastHitEntityId, and continues. Subsequent
|
// ApplyCacheHit, sets lastHitEntityId, and continues. Subsequent
|
||||||
// tuples of the same entity short-circuit at the top of the loop
|
// tuples of the same entity short-circuit at the top of the loop
|
||||||
// body via the lastHitEntityId == entity.Id check above.
|
// body via the lastHitEntityId == entity.Id check above.
|
||||||
if (!isAnimated && _cache.TryGet(entity.Id, landblockId, out var cachedEntry))
|
if (!isAnimated && !_tier1CacheDisabled && _cache.TryGet(entity.Id, landblockId, out var cachedEntry))
|
||||||
{
|
{
|
||||||
ApplyCacheHit(cachedEntry!, entityWorld, AppendInstanceToGroup);
|
ApplyCacheHit(cachedEntry!, entityWorld, AppendInstanceToGroup);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue