#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:
parent
120aeff720
commit
7bbb169c6c
2 changed files with 58 additions and 1 deletions
|
|
@ -244,6 +244,13 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
private int _instancesIssued;
|
private int _instancesIssued;
|
||||||
private long _lastLogTick;
|
private long _lastLogTick;
|
||||||
|
|
||||||
|
// #128 self-heal: per-Draw dedup of point-of-use load re-requests
|
||||||
|
// (PrepareMeshDataAsync is idempotent while pending — the dedup just
|
||||||
|
// avoids redundant dictionary probes within one pass) + the once-per-id
|
||||||
|
// [mesh-miss] diagnostic set (never cleared; diag-gated emission).
|
||||||
|
private readonly HashSet<ulong> _missRequested = new();
|
||||||
|
private readonly HashSet<ulong> _missLogged = new();
|
||||||
|
|
||||||
// CPU + GPU timing for [WB-DIAG] under ACDREAM_WB_DIAG=1.
|
// CPU + GPU timing for [WB-DIAG] under ACDREAM_WB_DIAG=1.
|
||||||
private readonly System.Diagnostics.Stopwatch _cpuStopwatch = new();
|
private readonly System.Diagnostics.Stopwatch _cpuStopwatch = new();
|
||||||
private readonly long[] _cpuSamples = new long[256]; // microseconds
|
private readonly long[] _cpuSamples = new long[256]; // microseconds
|
||||||
|
|
@ -728,6 +735,9 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
var vp = camera.View * camera.Projection;
|
var vp = camera.View * camera.Projection;
|
||||||
_shader.SetMatrix4("uViewProjection", vp);
|
_shader.SetMatrix4("uViewProjection", vp);
|
||||||
|
|
||||||
|
// #128 self-heal: fresh re-request dedup per Draw pass.
|
||||||
|
_missRequested.Clear();
|
||||||
|
|
||||||
bool diag = string.Equals(Environment.GetEnvironmentVariable("ACDREAM_WB_DIAG"), "1", StringComparison.Ordinal);
|
bool diag = string.Equals(Environment.GetEnvironmentVariable("ACDREAM_WB_DIAG"), "1", StringComparison.Ordinal);
|
||||||
|
|
||||||
if (diag && !_gpuQueriesInitialized)
|
if (diag && !_gpuQueriesInitialized)
|
||||||
|
|
@ -1054,6 +1064,17 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
// the populate fires with the complete batch set.
|
// the populate fires with the complete batch set.
|
||||||
currentEntityIncomplete = true;
|
currentEntityIncomplete = true;
|
||||||
if (diag) _meshesMissing++;
|
if (diag) _meshesMissing++;
|
||||||
|
// #128 self-heal: a missing-but-referenced mesh re-requests
|
||||||
|
// its load HERE — the one site that touches it every frame —
|
||||||
|
// so a preparation lost to landblock churn (cancelled after
|
||||||
|
// the last registration event) can never stay lost. Deduped
|
||||||
|
// per Draw; PrepareMeshDataAsync is idempotent while pending.
|
||||||
|
if (_missRequested.Add(gfxObjId))
|
||||||
|
{
|
||||||
|
_meshAdapter.EnsureLoaded(gfxObjId);
|
||||||
|
if (diag && _missLogged.Add(gfxObjId))
|
||||||
|
Console.WriteLine($"[mesh-miss] 0x{gfxObjId:X10} re-requested at point of use");
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (anyVao == 0) anyVao = renderData.VAO;
|
if (anyVao == 0) anyVao = renderData.VAO;
|
||||||
|
|
@ -1070,7 +1091,23 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
foreach (var (partGfxObjId, partTransform) in renderData.SetupParts)
|
foreach (var (partGfxObjId, partTransform) in renderData.SetupParts)
|
||||||
{
|
{
|
||||||
var partData = _meshAdapter.TryGetRenderData(partGfxObjId);
|
var partData = _meshAdapter.TryGetRenderData(partGfxObjId);
|
||||||
if (partData is null) continue;
|
if (partData is null)
|
||||||
|
{
|
||||||
|
// #128 self-heal + #53: a missing Setup PART must mark
|
||||||
|
// the entity incomplete (else a partial batch set
|
||||||
|
// caches permanently — the same bug class one level
|
||||||
|
// deeper) and re-request its load like the MeshRef
|
||||||
|
// path above.
|
||||||
|
currentEntityIncomplete = true;
|
||||||
|
if (diag) _meshesMissing++;
|
||||||
|
if (_missRequested.Add(partGfxObjId))
|
||||||
|
{
|
||||||
|
_meshAdapter.EnsureLoaded(partGfxObjId);
|
||||||
|
if (diag && _missLogged.Add(partGfxObjId))
|
||||||
|
Console.WriteLine($"[mesh-miss] 0x{partGfxObjId:X10} (setup part) re-requested at point of use");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var model = ComposePartWorldMatrix(
|
var model = ComposePartWorldMatrix(
|
||||||
entityWorld, meshRef.PartTransform, partTransform);
|
entityWorld, meshRef.PartTransform, partTransform);
|
||||||
|
|
|
||||||
|
|
@ -198,6 +198,26 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
||||||
_meshManager.DecrementRefCount(id);
|
_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>
|
/// <summary>
|
||||||
/// Per-frame drain of the WB pipeline's main-thread work queues. MUST be
|
/// 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
|
/// called once per frame from the render thread. Without this, the staged
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue