fix(render): eliminate EnvCell identity collisions

This commit is contained in:
Erik 2026-07-24 13:44:56 +02:00
parent 999201cca7
commit b7b9aaa9dd
7 changed files with 129 additions and 23 deletions

View file

@ -86,7 +86,7 @@ accepted-divergence entries (#96, #49, #50).
| AD-21 | Null-clipRoot legacy outdoor safety path (no portal visibility, no punches/seals, no-clip terrain) for pre-spawn / login / legacy cameras; in-world retail always has a viewer_cell root | `src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs` (`RuntimeWorldFrameRootSource`, `WorldRenderFrame.ClipRoot`); `src/AcDream.App/Rendering/WorldSceneRenderer.cs` (null-root safety draw) | Result is null ONLY when neither an interior root nor the synthetic outdoor node exists; kept so the login screen shows the live sky | If viewer-root resolution ever returns null in-world (membership bug, fly-camera edge), the frame silently degrades — interiors stop drawing through doorways; the old two-branch FLAP reappears for those frames | `SmartBox::RenderNormalMode` decomp:92635 |
| AD-22 | Async streamed mesh loading with bounded CPU replay residency, per-frame upload budgets, and point-of-use self-heal (`EnsureLoaded` re-request in the dispatcher's mesh-missing path, **#128**); retail loads synchronously — geometry is never absent | `src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs`; `src/AcDream.App/Rendering/Wb/MeshUploadCaches.cs`; `src/AcDream.App/Rendering/Wb/MeshUploadFrameBudget.cs` | Immutable preparation descriptors and the bounded CPU cache can re-stage an evicted mesh; dispatcher self-heal makes absence transient while upload budgets prevent a portal arrival from monopolizing a frame | A future consumer that neither retains an owner nor reaches the self-heal/replay path can remain invisible; under heavy admission pressure a valid mesh can pop in later than retail's synchronous path | retail synchronous content load; `docs/architecture/worldbuilder-inventory.md` portal-readiness and bounded-residency seams |
| AD-23 | Live entities with `ServerGuid != 0` and null `ParentCellId` are culled (ClipSlotCull) while indoor clip routing is active; retail objects are always cell-resident (synchronous add-to-cell at creation) | `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs:484` | Phase U.4 policy: parentless = unresolved indoors, equivalent to retail's not-in-any-visible-cell ⇒ not drawn, *given membership resolves promptly* | An entity whose membership lags (late CreateObject hydration, resolver hiccup) blinks invisible while the player is indoors, even in plain sight | retail per-cell object lists in PView traversal |
| AD-24 | EnvCell shell geometry hash-deduplicated ((environmentId, structure, surface overrides) → 31-multiplier hash) and instanced; retail draws each CEnvCell's own structure directly | `src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs:276` | Verbatim WB EnvCellRenderManager port (Phase A8); dedup is what makes the single-VAO MDI cell pipeline cheap; intended visuals identical | A hash collision between distinct tuples renders the wrong interior shell in some room with NO diagnostic firing — wrong walls/floor in a dungeon room | retail `PView::DrawCells` → per-cell drawing_bsp (cited at :319) |
| AD-24 | EnvCell shell geometry content-deduplicated and instanced; retail draws each CEnvCell's own structure directly | `src/AcDream.Core/Rendering/Wb/EnvCellGeometryIdentity.cs` | Phase A8 retained WB's 31× hash; the 2026-07-24 full-DAT gate proved a real collision (`0x00030175`/`0x01BC0105`), so App+Bake now share a namespaced FNV-1a tuple identity and the bake rejects any full-tuple collision | A future collision outside the installed full-DAT gate could still merge different shells at runtime; the stronger 59-bit payload makes this extremely unlikely, and every bake fails loudly rather than publishing it | retail `PView::DrawCells` → per-cell drawing_bsp (cited at the former renderer `:319`) |
| AD-25 | **REMOTE-DR sweep only** (the player half retired 2026-07-07 by the #182 verbatim rebuild): the remote dead-reckoning post-resolve still reflects velocity with the airborne-before-AND-after suppression; retail bounces unless grounded→grounded-and-not-sledding. The PLAYER path now runs the ported `handle_all_collisions` (`PhysicsObjUpdate`) with retail's `should_reflect` rule — the micro-bounce spiral it guarded is gone (contact is committed BEFORE the reflect and the small-velocity-zero is ungated) | `src/AcDream.App/Physics/RemotePhysicsUpdater.cs` (remote sweep post-resolve, #173 block) | The remote DR sweep hasn't been rebuilt yet (it has no fsf/SetPositionInternal chain); the old airborne-only suppression keeps remote landings from micro-bouncing on the remote landing-snap gate | Remote landing-reflection behavior (slope-landing momentum) won't reproduce; retire when the remote-DR sweep gets the same UpdateObjectInternal rebuild as the player | `handle_all_collisions` pc:282699-282715; ACE PhysicsObj.cs:2656-2721 |
| AD-27 | Use/PickUp action fired on natural moveto completion via the `MoveToComplete` client-addition seam (retail's `CleanUpAndCallWeenie` contains no weenie call in this build and notifies nothing on arrival); retail sends the action once (server MoveToChain callback completes it). The companion `MoveToCancelled` seam only withdraws the waiting client presentation/action. | `src/AcDream.App/Interaction/SelectionInteractionController.cs` (`OnNaturalMoveToComplete`/`OnMoveToCancelled`); `src/AcDream.App/Rendering/GameWindow.cs` (player MoveTo seam binding); `src/AcDream.Core/Physics/Motion/MoveToManager.cs` (`MoveToComplete`/`MoveToCancelled`) | ACE's server-side chain may have timed out by the time our body arrives; the close-range deferred send hits ACE's WithinUseRadius fast-path. R4-V5 re-anchored from the deleted B.6 `AutoWalkArrived` event — same fires-on-arrival-only contract; cancellation/error only clears retained intent. | If the server's chain has NOT timed out, the action executes twice — door toggles open-then-closed, use-once interactions double-fire; protocol noise on non-ACE servers | ACE CreateMoveToChain / WithinUseRadius; `MoveToManager::CleanUpAndCallWeenie` 00529650 §7e (no weenie call) |
| AD-28 | Chat transcript (`UiText`) and input (`UiChatInput`) are two separate widget classes placed inside their dat-authored container panels; retail's `ChatInterface` uses a single mode-flagged `UIElement_Text` (Type-12) that switches between read and edit mode | `src/AcDream.App/UI/Layout/ChatWindowController.cs:135` (transcript) + `:150` (input) | `UIElement_Text` is inside keystone.dll with no PDB/decomp; a two-widget split is functionally equivalent (read-only scroll, editable input) and is the structural adaptation required by our UiElement architecture | A future consumer expecting a single widget for both read/write (e.g. a plugin calling the chat API and getting one widget back) must be written to the two-widget contract | `UIElement_Text` (Type-12) @ keystone.dll; `gmMainChatUI::PostInit` @0x4ce130 |

View file

@ -101,6 +101,19 @@ behavior change, no divergence-register row.
running with no GL context, so baked pak output and live-client output stay
byte-identical.
**MP1b EnvCell content identity correction (2026-07-24).** The extracted
WorldBuilder `EnvCellRenderManager.GetEnvCellGeomId` 31× polynomial is not a
safe unique resource key. A guarded full retail-DAT catalog found the concrete
collision `0x00030175` versus `0x01BC0105`: different environment/surface
tuples both map to `0x00000002020E8C13` and contain different polygons.
`AcDream.Core.Rendering.Wb.EnvCellGeometryIdentity` now owns one namespaced
FNV-1a identity shared by the App streaming build and `acdream-bake`; the
legacy calculation remains executable only for the conformance test that proves
the collision. The bake additionally compares the complete source tuple before
aliasing and fails on any collision. This is an acdream resource-ownership seam,
not a second DAT interpreter; `DatCollection` and `MeshExtractor` remain the
only reader/extractor path.
**Retail VFX hook compatibility seam (2026-07-14).** Chorizite.DatReaderWriter
2.1.7 models `CreateBlockingParticleHook` as the common hook header only, while
retail inherits the complete `CreateParticleHook` payload. The narrow readers in

View file

@ -21,11 +21,14 @@ bounded artifact whose size and time scale with unique geometry, not with all
1. `DatCollection` remains the only DAT reader.
2. `MeshExtractor` remains the only mesh/texture interpreter used by both the
live path and the bake.
3. Geometry identity is the already-shipped WorldBuilder calculation:
`environmentId`, then `cellStructure`, then the ordered surface IDs through
the `hash = hash * 31 + value` chain, with bit 33 set.
4. The pure identity helper moves to `AcDream.Core.Rendering.Wb`; App and Bake
call the same implementation. No second or adapted hash is allowed.
3. Geometry identity is one deterministic hash of the complete typed tuple:
`environmentId`, `cellStructure`, surface count, then the ordered surface
IDs. App and Bake call the same Core implementation.
4. The pure identity helper lives in
`AcDream.Core.Rendering.Wb.EnvCellGeometryIdentity`. It uses FNV-1a in a
dedicated high-bit namespace and retains bit 33 for compatibility with
existing synthetic-geometry diagnostics. WorldBuilder's original
`hash = hash * 31 + value` calculation remains as a conformance helper only.
5. Every EnvCell TOC key is still
`PakKey.Compose(PakAssetType.EnvCellMesh, fileId)`.
6. Aliases share `offset`, `length`, and `crc32`. The pak format and reader do
@ -42,16 +45,30 @@ bounded artifact whose size and time scale with unique geometry, not with all
11. The pak itself and raw bake logs are machine artifacts and are not
committed. A compact, path-free bake report is committed.
**Full-DAT correction (2026-07-24):** the first guarded full-catalog run proved
that the original fixed contract was impossible. Installed retail DAT cells
`0x00030175` (`environment=0x277`, structure `0`, surfaces `0x013B,0x0034`)
and `0x01BC0105` (`environment=0x276`, structure `0`, surfaces
`0x04FC,0x0034`) both produce WorldBuilder ID `0x00000002020E8C13` despite
having visibly different geometry. The difference is algebraic:
one environment step contributes `31³`, exactly canceled by the first-surface
difference `0x04FC - 0x013B = 31²`. The collision guard stopped the bake before
writing an artifact. B1 was therefore corrected at the root: the old polynomial
is retained and tested as historical evidence, while runtime and bake now share
the stronger namespaced identity. The full-catalog collision gate remains
mandatory.
## Implementation checkpoints
### B1 — One geometry identity seam
- Extract the pure WorldBuilder identity calculation from the App transaction
builder into `AcDream.Core.Rendering.Wb.EnvCellGeometryIdentity`.
- Extract one pure identity calculation from the App transaction builder into
`AcDream.Core.Rendering.Wb.EnvCellGeometryIdentity`.
- Retain the existing App entry points as delegates so production behavior and
callers do not change.
- Add Core conformance tests for empty surfaces, ordered surfaces, the bit-33
namespace, and App/Bake shared values.
- Add Core conformance tests for empty surfaces, ordered surfaces, the
dedicated namespace, App/Bake shared values, and the installed-DAT collision
in the legacy WorldBuilder polynomial.
**Gate:** existing App geometry-ID tests and new Core tests pass with the exact
same values.
@ -133,4 +150,3 @@ dotnet test AcDream.slnx -c Release
Then run the full bake from the Release binary, capture its compact report, and
validate the output with `PakReader`. No visual gate is required because this
slice changes no runtime path.