From 2d20ee917b145d7209c686aeda3fe2f27e4009d1 Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 3 Sep 2026 05:49:45 +0200 Subject: [PATCH] =?UTF-8?q?checkpoint(render):=20portal-haze=20root=20caus?= =?UTF-8?q?e=20=E2=80=94=20emitters=20need=20own-cell=20membership=20(S2?= =?UTF-8?q?=20chunk=206=20contract)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instrumented arrivals show the Hidden-state cloud's emitters spawn in the arrival cell, become view-eligible when the world appears, and are never drawn: the walk draws an owner's emitters only through the owner's registry rows, and a hidden owner's shadow is suspended. Retail's CPhysicsObj::add_particle_shadow_to_cell (0x00514a70) gives an emitter one shadow in its own cell, drawn at that cell's turn regardless of the parent's hidden state. S2 chunk 6 (packet §10) ports that: per-cell emitter draw membership, owner-union particle path deleted. Route timing fixed so the arrival screenshots anchor on the reveal. Co-Authored-By: Claude Fable 5.1 --- ...-09-01-campaign-overhaul-world-solidity.md | 2 +- .../s2-membership-ownership-map.md | 60 +++++++++++++++++++ .../World/LiveEntityPresentationController.cs | 3 + tools/overhaul-selfgate/route-portal-haze.txt | 9 ++- 4 files changed, 71 insertions(+), 3 deletions(-) diff --git a/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md b/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md index 84d4f3e7..1e4f2be9 100644 --- a/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md +++ b/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md @@ -659,7 +659,7 @@ Update immediately when a slice changes state. Chat is not the ledger. | v1 OH1 | SUPERSEDED | research contracts committed at `5cd4fd2c`/`5d907ae9` (kept, binding) | T0–T3 evidence grammar parked on `quarantine/oh1-evidence-grammar-2026-09-02` | reviews found the grammar a false oracle three times | n/a | n/a | see §15 | | S1 Geometry | **CLOSED — G1 PASSED 2026-09-02** | `oh2-cellstruct-surface-contract.md` (§3.5 arbitrated on the binary) | chunk A `acf17246`; chunk B `b681717c`; hygiene `0840d5fb`; review fixes `e2543d0e`; G1 regression fix `8c6563ca` (segment/batch pairing) + its pin test (next commit) | lead verified both chunks against the named pseudo-C and the paired binary; two Opus lens reviews, 8 findings all verified by the lead and fixed; the review round's own upload-order change desynchronized cell-shell index segments from batches (first G1 launch: magenta/stretched/missing walls in every dungeon) and was root-caused and fixed by the lead before the re-gate | build 0/0; Content 213/213 (incl. PreparedPackage on the recipe-8 pak); Core 4,948/4,948 (one load-sensitive allocation test outside S1 passes alone); Bake 21/21; Launcher.Core 365/365; Runtime 1,884/1,884; App hermetic 6,757/6,757 | **G1 PASS**: Facility Hub stairs/doorways, Holtburg house interior, ordinary dungeon, town portal network all whole with correct materials on `8c6563ca` + recipe 8; cathedral shows the ORIGINAL symptom family (neighbor-cell walls drawn through portals over the room), explicitly outside G1 and carried to S3/S4 | dev pak recipe 8 (597,369,112 B; `acdream.recipe7.pak` kept); TOC delta explained; corpus scan old-only 3,197 (all untextured), new-only 0, unexplained 0 | | Capture | READY | §7 + `tools/walk-oracle/oh/README.md` | scripts committed `ebaa41df` + `14d8fe64`; lead resolved DBObj::m_DID=+0x28 and the cdecl alpha-function conventions; recon attach re-confirms | — | recon dt/x/uf dumps at session start | owner session ~1 h | before S3 | -| S2 World graph | CODE-COMPLETE — G2 OPEN: owner check 2026-09-03 passed 2 of 3 poses, cathedral leak unchanged (S3/S4), two new owner findings under investigation | `s2-membership-ownership-map.md` (`e7ad25a7`) + Contracts A/B; closeout evidence §8/§9 | chunk 1a `059b8883`; chunk 1b `5a2792d6`; chunk 2 `707d2803` (render statics borrow the registry's retail array; render flood deleted); chunk 3 `afbd2410` (one flood per registration feeds collision rows and render entries; staged SetPosition carries both products); chunk 4 `75ea269d` (movement publishes from the transition's cells; children inherit at registration); chunk 5 + closeout `c94a1a40` (borrowed per-cell views, render-only registration, sweeps/fallbacks deleted; reflood forwards the part array; NON-COLLIDING DAT STATICS register render-only from both publishers; the dual-review fix batch: Suspend clears the retail product, attach/detach advance the mutation revision, an attached child never floods on its own, RemoveLandblock/RetireOwnerFromLandblock prune retail rows, render-only owners move with their destination cell (retail's zero-sphere `find_cell_list` 0x0052b4e0, verified statically 2026-09-03 — AD-117 item 1 dropped), empty part array == null, per-move closures removed, EnvCell shells out of the entity-id index, index predicate compares the id, dead per-cell scene indices deleted) | lead corrected 1a's route decision before landing; chunk 2 verified by the lead; chunk 5's agent report missed the non-colliding-static population (found by the self-gate PIXEL DIFF, not by eye: Facility wall panels grey); S2 dual review (arch + retail lens, Opus) produced 12 + 7 findings, every one lead-verified against the source before an edit; the four BLOCKING/MAJOR lifetime gaps were real | at `c94a1a40`: Core 4,984/4,984; Content 214/214; Runtime 1,884/1,884; App hermetic lane 6,760/6,760; App InstalledDat 217 pass / 1 skip / 2 pre-existing #383; App Windows 1/1 | G2 PROVISIONAL PASS. Self-gate runs (kit `ea546f38`, pwsh): `logs/selfgate-20260902-231426-baseline-g1-binary` (reference), `…231642-s2-chunk3`, `…233845-s2-chunk4` (identical), `logs/selfgate-20260903-002632-s2-chunk5-reflood` (FAIL: Facility wall panels grey, left-wall mean RGB 29,26,23 vs baseline 65,17,21 — missing non-colliding statics), `logs/selfgate-20260903-010022-s2-review-fixes-3` (candidate: panels back at 79,20,25 = chunk-3/4; cathedral 13.6% / Facility 15.0% / house 13.4% px differ vs baseline, the chunk-3/4 band; graceful logout, zero exceptions). Membership-probe run `logs/selfgate-20260903-003013-s2-chunk5-membership-probe`: `[walk-membership]` fired only on two transient Holtburg login frames, never in Facility Hub or the cathedral. Connected R6 soak on the chunk-4 build `6717a3e5` (`logs/connected-r6-soak-20260902-234016.report.json`): 9 destinations in 511 s, zero invariant failures, graceful exit — route PASS (its `-CollisionShadowEvery` referee is the obsolete I5 graph-vs-flat comparator; retire in S5). Connected R6 soak on the FINAL S2 build (`logs/connected-r6-soak-20260903-010403.report.json`, binary matches source, exit 0, graceful=True): 9 canonical checkpoints, 0 failures, 10 warnings (server-side population drift + DAT VFX table messages), 522 s — route PASS. OWNER G2 CHECK 2026-09-03 (client at `4b0b29e4`): Facility Hub PASS, Holtburg house PASS; cathedral: the original leak unchanged, NOT worse (S3/S4 target, G2 bar met); TWO NEW OWNER FINDINGS block G2 closure — (a) parts of buildings leak through terrain outdoors in Holtburg — owner screenshot shows doorway-sized fragments of the houses on the terrace BELOW showing through the hill; mechanism identified: `PortalDepthMaskRenderer` draws every visited cell's `OtherCellId == 0xFFFF` portal fan (`RetailPViewPassExecutor` `DrawDepthFan`) with depth compare `Always` + depth write, so a building doorway BEHIND terrain still punches far depth through the hill and the interior draws through the hole; retail's far punch cannot be `Always` against terrain — the exact z-func is S4's first question (the #117 punch stencil is already on S4's delete list; the retail-world contract §5.4 wording must be re-read at the capture). Pre-existing (not an S2 regression), assigned to S4; (b) the purple portal-exit haze on the character no longer appears on arrival (the character just appears) — under investigation, root cause + fix owed before G2 closes. Morning handoff: `docs/research/2026-09-01-overhaul/2026-09-03-g2-morning-handoff.md` | owner double-checks G2 (handoff checklist); then the owner capture session (the S3 packet §7 names the two traces); then S3 | +| S2 World graph | CODE-COMPLETE — G2 OPEN: owner check 2026-09-03 passed 2 of 3 poses, cathedral leak unchanged (S3/S4), two new owner findings under investigation | `s2-membership-ownership-map.md` (`e7ad25a7`) + Contracts A/B; closeout evidence §8/§9 | chunk 1a `059b8883`; chunk 1b `5a2792d6`; chunk 2 `707d2803` (render statics borrow the registry's retail array; render flood deleted); chunk 3 `afbd2410` (one flood per registration feeds collision rows and render entries; staged SetPosition carries both products); chunk 4 `75ea269d` (movement publishes from the transition's cells; children inherit at registration); chunk 5 + closeout `c94a1a40` (borrowed per-cell views, render-only registration, sweeps/fallbacks deleted; reflood forwards the part array; NON-COLLIDING DAT STATICS register render-only from both publishers; the dual-review fix batch: Suspend clears the retail product, attach/detach advance the mutation revision, an attached child never floods on its own, RemoveLandblock/RetireOwnerFromLandblock prune retail rows, render-only owners move with their destination cell (retail's zero-sphere `find_cell_list` 0x0052b4e0, verified statically 2026-09-03 — AD-117 item 1 dropped), empty part array == null, per-move closures removed, EnvCell shells out of the entity-id index, index predicate compares the id, dead per-cell scene indices deleted) | lead corrected 1a's route decision before landing; chunk 2 verified by the lead; chunk 5's agent report missed the non-colliding-static population (found by the self-gate PIXEL DIFF, not by eye: Facility wall panels grey); S2 dual review (arch + retail lens, Opus) produced 12 + 7 findings, every one lead-verified against the source before an edit; the four BLOCKING/MAJOR lifetime gaps were real | at `c94a1a40`: Core 4,984/4,984; Content 214/214; Runtime 1,884/1,884; App hermetic lane 6,760/6,760; App InstalledDat 217 pass / 1 skip / 2 pre-existing #383; App Windows 1/1 | G2 PROVISIONAL PASS. Self-gate runs (kit `ea546f38`, pwsh): `logs/selfgate-20260902-231426-baseline-g1-binary` (reference), `…231642-s2-chunk3`, `…233845-s2-chunk4` (identical), `logs/selfgate-20260903-002632-s2-chunk5-reflood` (FAIL: Facility wall panels grey, left-wall mean RGB 29,26,23 vs baseline 65,17,21 — missing non-colliding statics), `logs/selfgate-20260903-010022-s2-review-fixes-3` (candidate: panels back at 79,20,25 = chunk-3/4; cathedral 13.6% / Facility 15.0% / house 13.4% px differ vs baseline, the chunk-3/4 band; graceful logout, zero exceptions). Membership-probe run `logs/selfgate-20260903-003013-s2-chunk5-membership-probe`: `[walk-membership]` fired only on two transient Holtburg login frames, never in Facility Hub or the cathedral. Connected R6 soak on the chunk-4 build `6717a3e5` (`logs/connected-r6-soak-20260902-234016.report.json`): 9 destinations in 511 s, zero invariant failures, graceful exit — route PASS (its `-CollisionShadowEvery` referee is the obsolete I5 graph-vs-flat comparator; retire in S5). Connected R6 soak on the FINAL S2 build (`logs/connected-r6-soak-20260903-010403.report.json`, binary matches source, exit 0, graceful=True): 9 canonical checkpoints, 0 failures, 10 warnings (server-side population drift + DAT VFX table messages), 522 s — route PASS. OWNER G2 CHECK 2026-09-03 (client at `4b0b29e4`): Facility Hub PASS, Holtburg house PASS; cathedral: the original leak unchanged, NOT worse (S3/S4 target, G2 bar met); TWO NEW OWNER FINDINGS block G2 closure — (a) parts of buildings leak through terrain outdoors in Holtburg — owner screenshot shows doorway-sized fragments of the houses on the terrace BELOW showing through the hill; mechanism identified: `PortalDepthMaskRenderer` draws every visited cell's `OtherCellId == 0xFFFF` portal fan (`RetailPViewPassExecutor` `DrawDepthFan`) with depth compare `Always` + depth write, so a building doorway BEHIND terrain still punches far depth through the hill and the interior draws through the hole; retail's far punch cannot be `Always` against terrain — the exact z-func is S4's first question (the #117 punch stencil is already on S4's delete list; the retail-world contract §5.4 wording must be re-read at the capture). Pre-existing (not an S2 regression), assigned to S4; (b) the purple portal-exit haze on the character no longer appears on arrival — ROOT CAUSE FOUND (instrumented runs `logs/selfgate-20260903-053731-haze-candidate`, `…-054153-haze-candidate-2`, chunk-4 control `…/bisect-chunk4/logs/selfgate-20260903-054328-haze-chunk4`): the server keeps the player Hidden until acdream sends LoginComplete at reveal `complete` (retail-correct); the Hidden-state script's emitters spawn in the arrival cell at `materialized`, are view-eligible at `world-visible`, and are never DRAWN (frame `h02-arrive-400ms`: room, no character, no cloud) because the walk draws an owner's emitters only through the owner's registry rows and a hidden owner's shadow is suspended; the deleted chunk-5 fallback used to carry them. Retail gives every emitter its OWN cell membership (`add_particle_shadow_to_cell` 0x00514a70, one shadow in its own cell, no clip planes, drawn at that cell's turn regardless of the parent's hidden state) — the branch AD-117 item 4 had deferred as 'no production path'. Fix = S2 chunk 6 (packet §10): per-cell emitter draw membership, owner-union particle path deleted; IN PROGRESS. Morning handoff: `docs/research/2026-09-01-overhaul/2026-09-03-g2-morning-handoff.md` | owner double-checks G2 (handoff checklist); then the owner capture session (the S3 packet §7 names the two traces); then S3 | | S3 Walk | PACKET DRAFTED 2026-09-03 — implementation BLOCKED on the §7 capture session | `s3-walk-ownership-map.md` (retail table, current owners, four chunks, review lenses, gate, the three exact captures to ask for) + built-mesh/world contracts | — | — | — | folded into G3 | owner capture session first (§7 of the packet names the three traces); chunk 2 may start on the decomp oracle alone | | S4 Depth + alpha | PLANNED | depth/alpha contracts + captures | — | — | — | G3 | retires AP-34 | | S5 Consumers + closeout | PLANNED | landscape contract; AP-232 | — | — | — | G4 | retires AP-117/AP-232; deletes probes | diff --git a/docs/research/2026-09-01-overhaul/s2-membership-ownership-map.md b/docs/research/2026-09-01-overhaul/s2-membership-ownership-map.md index 577c5f65..12c3a436 100644 --- a/docs/research/2026-09-01-overhaul/s2-membership-ownership-map.md +++ b/docs/research/2026-09-01-overhaul/s2-membership-ownership-map.md @@ -717,6 +717,66 @@ lead-verified against the source before any edit).** | A10 | `_cellStatics`/`_cellDynamics` + `GetCellCount`/`CopyCellTo`/`GetCellStaticCount`/`CopyCell*To` had zero production callers | deleted | `ArchRenderSceneTests` updated | | A11/A12, R5–R7 | live list handed out; probe-flag coupling; unloaded-neighbour rows; circumsphere cheap reject; particle branch | documented (AD-117 items 2–4; S5 tail) | — | +## 10. Chunk 6 — particle emitter membership (owner G2 finding, 2026-09-03) + +**Finding.** After a portal the purple cloud around the arriving character no +longer shows. Instrumented run `logs/selfgate-20260903-054153-haze-candidate-2` +(`ACDREAM_DUMP_PLAYSCRIPT=1`, temporary `[pes-spawn]`/`[pes-vis]` traces): +the server keeps the player HIDDEN until acdream sends LoginComplete at reveal +`complete` (retail-correct); the Hidden-state script's 14 emitters spawn in +the arrival cell right after `materialized`, become view-eligible at +`world-visible`, and are never drawn (frame `h02-arrive-400ms`: room visible, +no character, no cloud), then the UnHide script stops them at `complete`. +Cause: the walk draws an owner's emitters only when the owner's registry +rows are in the cell (`WalkFrameDriver` `CellParticles`/`StaticParticles` +owner unions → `DrawForOwners`), and a hidden owner's shadow is suspended. +Before chunk 5 the deleted parent-cell fallback bucketed the hidden player +anyway, which is why the cloud used to draw. The ledger's "particle emitters +never reach the registry — no production path" (AD-117 item 4) was wrong: +this is that path. + +**Retail truth.** `CPhysicsObj::add_particle_shadow_to_cell` 0x00514a70: an +emitter (`state & 0x1000`) owns exactly ONE shadow object in ITS OWN current +cell (`this->cell`), `CObjCell::add_shadow_object(cell, shadow, 1)` and +`CPartArray::AddPartsShadow(part_array, this->cell, 1)` (no clip planes); the +`add_shadows_to_cells` branch at 0x00514aed skips the CELLARRAY flood for it. +It is drawn at that cell's object turn like any object, independent of its +parent's hidden state — that IS the cloud around a hidden, portalling player. + +**Contract (behavior change).** An emitter's draw membership is its own cell: +1. `ParticleSystem` keeps a per-pass `cell → renderable handles` index, + maintained where `RefreshRenderableIndex` adds/removes and where + `UpdateEmitterOwnerCell` changes the cell (an emitter is in exactly one + cell; `OwnerCellId` is the owner's pose-registry cell, which is kept while + the owner is hidden). New API `CopyRenderableEmittersInCell(pass, cellId, + destination)`; no per-frame allocation after warmup. +2. `ParticleRenderer.DrawForCell(camera, cameraWorldPos, pass, cellId, clipSlot)` + beside `DrawForOwners`. +3. The walk draws particles BY CELL at the existing turns: interior + `CellParticles` (cell id), landscape `StaticParticles` (land cell id; + building-shell turns draw the building's interior cells' emitters at their + own cell turns instead), the post-replay interior flood-cell pass + (`RetailPViewRenderer` `_cellParticleOwnerScratch`), and look-in cells + (`DrawBuildingLookInDynamics`). Every owner-union path for particles + (`UnionOwners`/`UnionNewOwners` over `GetCellStatics`/`GetCellDynamics`/ + `GetOutdoorStatics`/`GetOutdoorDynamics` for particle purposes, + `_outdoorParticleOwnersDrawnThisFrame`, `_particleClassifications.Outdoor`/ + `Visible`, `RetailPViewCellSliceContext.ParticleOwnerIds`, + `RetailPViewLandscapeStaticParticleContext.ParticleOwnerIds`) is deleted — + an emitter is in one cell, so no cross-cell dedupe is needed. + `DrawUnattachedSceneParticles` (owner 0) is unchanged this chunk. +4. `ApplyRetailView`'s eligibility (owner cell in the visible set + range) + stays as the retail `ShouldDrawParticles` gate. +5. Register: AD-117 item 4 becomes a PORT note (own-cell membership tracked by + the particle system's cell index rather than a registry row — the emitter + is not a physics object in acdream; record that residual in the row). +6. Tests: cell index add/remove/move and "hidden owner's emitter still + enumerated by cell" (Core); executor/walk draws by cell (App, replace the + owner-id fixtures); the probe route `route-portal-haze.txt` frame + `h02-arrive-400ms` must show the cloud (lead runs it). +Temporary probes (`[pes-spawn]`, `[pes-vis]`, the `HiddenTraceEnabled` field) +are deleted in the same commit as the fix. + Verified-correct list from both reviews (kept for S5): Contract A dispatch, `find_bbox_cell_list` shape, `AddPartsShadow` order/clip-plane rule, movement from the transition's array, keep-when-empty, child inheritance diff --git a/src/AcDream.App/World/LiveEntityPresentationController.cs b/src/AcDream.App/World/LiveEntityPresentationController.cs index 4949dffb..18d8cb82 100644 --- a/src/AcDream.App/World/LiveEntityPresentationController.cs +++ b/src/AcDream.App/World/LiveEntityPresentationController.cs @@ -21,6 +21,9 @@ public sealed class LiveEntityPresentationController : IDisposable public const uint UnHideScriptType = 0x75u; public const uint HiddenScriptType = 0x76u; + private static readonly bool HiddenTraceEnabled = + Environment.GetEnvironmentVariable("ACDREAM_DUMP_PLAYSCRIPT") == "1"; + private readonly LiveEntityRuntime _liveEntities; private readonly ShadowObjectRegistry _shadows; private readonly Func _playTyped; diff --git a/tools/overhaul-selfgate/route-portal-haze.txt b/tools/overhaul-selfgate/route-portal-haze.txt index 726ba409..30270db9 100644 --- a/tools/overhaul-selfgate/route-portal-haze.txt +++ b/tools/overhaul-selfgate/route-portal-haze.txt @@ -1,6 +1,7 @@ # Portal-exit haze probe route (2026-09-03, owner finding at the G2 check). -# Teleport, then shoot at short intervals after the world reveals so the -# UnHide (0x75) script's emitters are in frame if they draw at all. +# Teleport, let the client ENTER portal space first (sleep), then wait for +# the world to come back and shoot at short intervals so the UnHide (0x75) +# script's emitters are in frame if they draw at all. wait world-visible 180000 sleep 3000 @@ -8,6 +9,7 @@ screenshot h00-before 15000 # Facility Hub pose (interior arrival) command /teleloc 8A02015E 60.251431 -38.848198 -5.995000 0.995004 0.000000 0.000000 -0.099833 +sleep 1500 wait world-visible 90000 screenshot h01-arrive-0ms 15000 sleep 400 @@ -18,9 +20,11 @@ sleep 1000 screenshot h04-arrive-2000ms 15000 sleep 2000 screenshot h05-arrive-4000ms 15000 +sleep 4000 # Holtburg house pose (a second arrival, outdoor landblock) command /teleloc A9B4013F 134.555893 11.423566 94.005005 0.996917 0.000000 0.000000 -0.078459 +sleep 1500 wait world-visible 90000 screenshot h11-arrive-0ms 15000 sleep 400 @@ -29,5 +33,6 @@ sleep 600 screenshot h13-arrive-1000ms 15000 sleep 1000 screenshot h14-arrive-2000ms 15000 +sleep 3000 close-client