From 205379c6d6bc25669b5f93d692766721c79e7ef5 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 7 Aug 2026 08:37:43 +0200 Subject: [PATCH] =?UTF-8?q?fix(streaming):=20#339=20=E2=80=94=20a=20packed?= =?UTF-8?q?=20EnvCell=20geom=20id=20no=20longer=20misroutes=20into=20the?= =?UTF-8?q?=2032-bit=20prepare=20arm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reveal hang (three live occurrences: portal-space, login twice) was an unhandled OverflowException on the render frame's readiness evaluation: EnsureRenderDataReady found a packed 64-bit EnvCell geometry id OWNED but DESCRIPTOR-LESS — the release path removes the descriptor while render data parks on the LRU, IncrementRefCount restores ownership on a revisit, and the scheduler's PrepareEnvCellGeomMeshDataAsync has not yet re-registered — and fell through to the Setup/GfxObj arm, whose checked((uint)id) cast threw. After that the reveal was never evaluated again. The fix corrects the TYPE DISPATCH rather than suppressing anything: packed ids (bit 33, GetEnvCellGeomId) answer "not yet" in the acquire-to-prepare window — the true answer, since the scheduler re-registers on the same landblock build — and PrepareMeshDataAsync's blind cast becomes a typed, loud invariant failure naming the id kind and the issue, so a future caller repeating the confusion gets a diagnosis instead of three live hangs. Validation: the crash was DETERMINISTIC at login cell 0xA8B4002F (two consecutive hard failures); with the fix the same login revealed cleanly and a full session — 16,585 entities, five portal generations, the user-passed Session-B dungeon gate — ran with zero overflows and zero guard fires. Clean-room suite: 11,253 passed / 6 skipped / 0 failed. Found because the Session-B gate launch finally captured the stack the earlier #339 hangs never printed. #343 (the wounded loop's Reset-in- render-loop shutdown) and #344 (the mid-teleport world-frame race the same evening surfaced) are filed separately and unfixed. Co-Authored-By: Claude Opus 5 --- docs/ISSUES.md | 16 ++++++++- .../Rendering/Wb/ObjectMeshManager.cs | 36 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) 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);