fix(streaming): #339 — a packed EnvCell geom id no longer misroutes into the 32-bit prepare arm
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run

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 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-07 08:37:43 +02:00
parent 42de5f18ff
commit 205379c6d6
2 changed files with 51 additions and 1 deletions

View file

@ -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);