diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 5bc9d311..749895e4 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -356,7 +356,21 @@ after which the reveal is never evaluated again. Demote-then-revisit ordering explains the intermittency and why the same destination passed twice earlier: the window only exists after an evict/re-acquire cycle. -**Status: mechanism established; ROOT-CAUSE FIX NOT YET DESIGNED.** The fix +**Status: FIXED 2026-08-07 evening, LIVE-VALIDATED the same night.** The fix +is the type-dispatch correction, not a suppression: `EnsureRenderDataReady` +now answers "not yet" for a packed id in the acquire→prepare window (the +scheduler's `PrepareEnvCellGeomMeshDataAsync` re-registers the descriptor on +the same landblock build, so not-ready is the true answer, not a dodge), and +`PrepareMeshDataAsync` converts the blind checked cast into a typed, loud +invariant failure naming the id kind and this issue. Validation: the crash +was DETERMINISTIC at login cell 0xA8B4002F (two consecutive hard failures); +with the fix, the same login revealed cleanly and a full play session +(16,585 entities, five portal generations, the Session-B dungeon gate) ran +with zero overflows and zero guard fires. The original design note below is +retained; the "band-aid" candidates it names remain rejected — this fix +corrects the dispatch, it does not swallow the error. + +**Original status: mechanism established; ROOT-CAUSE FIX NOT YET DESIGNED.** The fix is NOT "catch the exception" and NOT "skip 64-bit ids" (band-aids both): the acquire/re-request ordering must make owned-implies-descriptor an invariant again, or EnsureRenderDataReady must legitimately re-request the diff --git a/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs b/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs index 0b6d7cd9..b83bd585 100644 --- a/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs +++ b/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs @@ -1071,6 +1071,25 @@ namespace AcDream.App.Rendering.Wb EnvCellGeomRequest? envCell = _envCellDescriptors.TryGetValue(id, out EnvCellGeomRequest descriptor) ? descriptor : null; + // #339: the same type contract, stated loudly at the second + // entry. A packed EnvCell geometry id (bit 33) must arrive + // with its descriptor; reaching the Setup/GfxObj arm with one + // is caller type confusion, and the bare checked-cast + // OverflowException it used to produce cost three live hangs + // before it was diagnosable. + if (envCell is null && id > uint.MaxValue) + { + var confused = new InvalidOperationException( + $"Packed EnvCell geometry id 0x{id:X16} reached the " + + "Setup/GfxObj preparation arm without a registered " + + "descriptor (#339). Callers must route packed ids " + + "through PrepareEnvCellGeomMeshDataAsync or treat " + + "them as not-ready until the scheduler registers " + + "them."); + tcs.TrySetException(confused); + _terminalPreparationFailures.Add(id); + return task; + } PreparedAssetRequest asset = envCell is { } env ? PreparedAssetRequest.EnvCellGeometry( env.SourceCellId, @@ -1327,6 +1346,23 @@ namespace AcDream.App.Rendering.Wb req.CellStructure, req.Surfaces); } + else if (id > uint.MaxValue) + { + // #339 (2026-08-07): a PACKED EnvCell geometry id (bit 33 set, + // GetEnvCellGeomId) whose descriptor is not registered — the + // acquire→prepare window of a demote-then-revisit cycle: + // release removed the descriptor, IncrementRefCount restored + // ownership on the revisit, and the scheduler's + // PrepareEnvCellGeomMeshDataAsync (which re-registers it with + // full data on every landblock build) has not run yet. The + // honest readiness answer is simply "not yet" — the prepare + // arrives on the same build. Falling through to the 32-bit + // Setup/GfxObj arm was a TYPE CONFUSION whose checked cast + // threw OverflowException on the render frame's readiness + // evaluation, after which the reveal was never evaluated + // again: the login/portal hang the user hit three times. + return false; + } else { _ = PrepareMeshDataAsync(id, isSetup: false);