#128 STRUCTURAL: missing meshes re-request their load at the POINT OF USE - permanent invisibility becomes impossible

The registration-time re-arm was insufficient and the user proved it
(ran back from the lifestone -> broken stairs + exposed barrel again):
a preparation cancelled by landblock churn AFTER the last registration
event has no later event to re-fire it - crossing blocks loads/unloads
them repeatedly behind the player, so the cancel-after-last-register
window is routinely hit on any cross-country run.

The structural fix: the draw dispatcher touches every
missing-but-referenced mesh every frame (the meshMissing slow path) -
THAT is the one site a retry can never be missed from. Both miss paths
(per-MeshRef and per-Setup-part) now call WbMeshAdapter.EnsureLoaded
(idempotent passthrough to PrepareMeshDataAsync, which early-outs on
existing data and dedups pending tasks), deduped per Draw pass.
Retail-equivalence: retail loads synchronously - geometry is never
permanently absent; this converges the async pipeline to the same
guarantee regardless of cancellation/eviction timing.

Also fixes the #53-one-level-deeper hole found en route: a missing
SETUP PART did not mark the entity incomplete, so a partial batch set
could cache permanently for Setup-shaped render data.

New apparatus: [mesh-miss] once-per-id line under ACDREAM_WB_DIAG=1 -
any future missing mesh names itself instead of needing a live repro.

Suites: App 242+1skip, Core 1422+2skip, UI 420, Net 294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-11 20:30:56 +02:00
parent 120aeff720
commit 7bbb169c6c
2 changed files with 58 additions and 1 deletions

View file

@ -198,6 +198,26 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
_meshManager.DecrementRefCount(id);
}
/// <summary>
/// #128 self-heal (2026-06-11): re-request a mesh load at the POINT OF
/// USE. Registration-time re-arming was insufficient — a preparation
/// cancelled by landblock churn AFTER the last registration event
/// (running across blocks loads/unloads them repeatedly) left the mesh
/// permanently unloadable with no later event to re-fire it. The draw
/// dispatcher touches every missing-but-referenced mesh every frame (the
/// meshMissing slow path) — that is the one place a retry can never be
/// missed. Cheap and idempotent: PrepareMeshDataAsync early-outs on
/// existing render data and returns the in-flight task when pending.
/// Retail-equivalence: retail loads content synchronously — geometry is
/// never permanently absent; this converges our async pipeline to the
/// same guarantee.
/// </summary>
public void EnsureLoaded(ulong id)
{
if (_isUninitialized || _meshManager is null) return;
_meshManager.PrepareMeshDataAsync(id, isSetup: false);
}
/// <summary>
/// Per-frame drain of the WB pipeline's main-thread work queues. MUST be
/// called once per frame from the render thread. Without this, the staged