checkpoint(render): portal-haze root cause — emitters need own-cell membership (S2 chunk 6 contract)

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 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-03 05:49:45 +02:00
parent 7ec72765d7
commit 2d20ee917b
4 changed files with 71 additions and 3 deletions

File diff suppressed because one or more lines are too long

View file

@ -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 | | A10 | `_cellStatics`/`_cellDynamics` + `GetCellCount`/`CopyCellTo`/`GetCellStaticCount`/`CopyCell*To` had zero production callers | deleted | `ArchRenderSceneTests` updated |
| A11/A12, R5R7 | live list handed out; probe-flag coupling; unloaded-neighbour rows; circumsphere cheap reject; particle branch | documented (AD-117 items 24; S5 tail) | — | | A11/A12, R5R7 | live list handed out; probe-flag coupling; unloaded-neighbour rows; circumsphere cheap reject; particle branch | documented (AD-117 items 24; 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, Verified-correct list from both reviews (kept for S5): Contract A dispatch,
`find_bbox_cell_list` shape, `AddPartsShadow` order/clip-plane rule, `find_bbox_cell_list` shape, `AddPartsShadow` order/clip-plane rule,
movement from the transition's array, keep-when-empty, child inheritance movement from the transition's array, keep-when-empty, child inheritance

View file

@ -21,6 +21,9 @@ public sealed class LiveEntityPresentationController : IDisposable
public const uint UnHideScriptType = 0x75u; public const uint UnHideScriptType = 0x75u;
public const uint HiddenScriptType = 0x76u; public const uint HiddenScriptType = 0x76u;
private static readonly bool HiddenTraceEnabled =
Environment.GetEnvironmentVariable("ACDREAM_DUMP_PLAYSCRIPT") == "1";
private readonly LiveEntityRuntime _liveEntities; private readonly LiveEntityRuntime _liveEntities;
private readonly ShadowObjectRegistry _shadows; private readonly ShadowObjectRegistry _shadows;
private readonly Func<uint, uint, float, bool> _playTyped; private readonly Func<uint, uint, float, bool> _playTyped;

View file

@ -1,6 +1,7 @@
# Portal-exit haze probe route (2026-09-03, owner finding at the G2 check). # 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 # Teleport, let the client ENTER portal space first (sleep), then wait for
# UnHide (0x75) script's emitters are in frame if they draw at all. # 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 wait world-visible 180000
sleep 3000 sleep 3000
@ -8,6 +9,7 @@ screenshot h00-before 15000
# Facility Hub pose (interior arrival) # Facility Hub pose (interior arrival)
command /teleloc 8A02015E 60.251431 -38.848198 -5.995000 0.995004 0.000000 0.000000 -0.099833 command /teleloc 8A02015E 60.251431 -38.848198 -5.995000 0.995004 0.000000 0.000000 -0.099833
sleep 1500
wait world-visible 90000 wait world-visible 90000
screenshot h01-arrive-0ms 15000 screenshot h01-arrive-0ms 15000
sleep 400 sleep 400
@ -18,9 +20,11 @@ sleep 1000
screenshot h04-arrive-2000ms 15000 screenshot h04-arrive-2000ms 15000
sleep 2000 sleep 2000
screenshot h05-arrive-4000ms 15000 screenshot h05-arrive-4000ms 15000
sleep 4000
# Holtburg house pose (a second arrival, outdoor landblock) # 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 command /teleloc A9B4013F 134.555893 11.423566 94.005005 0.996917 0.000000 0.000000 -0.078459
sleep 1500
wait world-visible 90000 wait world-visible 90000
screenshot h11-arrive-0ms 15000 screenshot h11-arrive-0ms 15000
sleep 400 sleep 400
@ -29,5 +33,6 @@ sleep 600
screenshot h13-arrive-1000ms 15000 screenshot h13-arrive-1000ms 15000
sleep 1000 sleep 1000
screenshot h14-arrive-2000ms 15000 screenshot h14-arrive-2000ms 15000
sleep 3000
close-client close-client