fix #131: unattached emitters had NO particle pass under interior roots

The user's capture run + a code read pinned it in one step: every
particle pass under an interior root is id-filtered (the landscape
slice's Scene pass, the per-cell pass, and the dynamics pass all
require AttachedObjectId != 0 plus owner-set membership). An UNATTACHED
emitter - AttachedObjectId == 0: portal swirls, campfires, ground
effects anchored at a position - drew NOWHERE when the viewer root was
interior. The outdoor root has the dedicated T3 pass for exactly this
class (its own comment records that "unattached ones had NO pass on
outdoor-node frames"); the identical hole on interior-root frames was
never plugged. Walking out flips to the outdoor root and the T3 pass
picks the swirl up - "appears when I walk out again", verbatim.

The [outstage] capture corroborated the rest of the chain healthy
under the interior root: outside-stage routing correct, cone PASS for
the portal-family dynamics, 57 attached emitters matched and drawn
through the doorway. Only the unattached class was orphaned.

Fix: RetailPViewDrawContext.DrawUnattachedSceneParticles - invoked ONCE
per interior-root frame at the END of the landscape stage:
- pre-clear, because drawn after the depth clear + seals an outdoor
  emitter beyond the door plane z-fails against the seal's door-plane
  stamp;
- after the #124 look-in sub-pass, so swirls blend over far-building
  interiors;
- once per frame, not per slice - alpha particles must not double-draw
  (the #121 lesson);
- mutually exclusive with the outdoor T3 pass by root kind (interior
  invokes this; outdoor keeps T3).

Residual (documented in the issue): unattached INDOOR emitters now draw
pre-clear and get overpainted by the room's shells - the same
invisibility they had before this fix; the proper per-emitter cell
classification is a future port.

[outstage-pt] probe extended with the unattached emitter count (the
probe's blind spot was exactly where the bug hid).

Suites: App 259+1skip / Core 1439+2skip / UI 420 / Net 294 green.
Awaiting the user gate: the swirl through the doorway. #132 (candle
flame vs through-opening background) remains open - different
mechanism, background-dependent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-12 19:04:12 +02:00
parent eeb1c59ded
commit 1d3f9a8c97
3 changed files with 70 additions and 24 deletions

View file

@ -4583,35 +4583,44 @@ or distance.
## #131 — Portal swirl invisible when viewed from inside a building through the doorway
**Status:** OPEN
**Status:** FIX SHIPPED — awaiting user visual gate
**Severity:** MEDIUM (portals are landmark objects; the through-door view is common)
**Filed:** 2026-06-12 (user report, #124 gate session)
**Component:** render — outside-stage dynamics' particles under interior roots (#118/#121 family)
**Component:** render — UNATTACHED emitters have no pass under interior roots
**Symptom (user, axiom):** "the portal swirl is missing, when I look out
from inside a house. Appears when I walk out again."
**Mechanism frame:** under an interior root an outdoor dynamic routes to
the OUTSIDE stage (`_outsideStageDynamics`, #118) and its particles'
ONLY path is the landscape slice's Scene pass
(`_outdoorSceneParticleEntityIds`); the last-pass particle callback
deliberately excludes outside-stage entities (#121: "already drew in
the slice"). If any link fails (slice cone verdict, the id set, emitter
matching, draw order vs the slice's blend state), the swirl draws
NOWHERE exactly when indoors — and reappears outdoors where
DrawDynamicsLast + DrawDynamicsParticles take over. Matches the report
exactly.
**Root cause (confirmed by read + the [outstage] capture):** every
particle pass under an interior root is id-FILTERED: the landscape
slice's Scene pass and the cell/dynamics passes all require
`emitter.AttachedObjectId != 0` and membership in an owner set. An
UNATTACHED emitter (`AttachedObjectId == 0` — portal swirls, campfires,
ground effects anchored at a position) therefore draws NOWHERE when the
root is interior. The outdoor root has the dedicated T3 pass for
exactly this class (its own comment: "unattached ones had NO pass on
outdoor-node frames") — the identical hole on interior-root frames was
never plugged. Walk out → the T3 pass picks the swirl up → "appears
when I walk out again". The capture corroborated the rest of the chain
healthy: outside-stage routing + cone PASS for the dynamics, 57
attached emitters matched and drawn through the doorway.
**Desk-exonerated (2026-06-12):** key conventions are uniform
(`ParticleEntityKey` = ServerGuid-first at all three filter sites);
`DynamicDrawsInOutsideStage` routes outdoor dynamics correctly;
`EntitySphere` uses the vertex-derived bounds.
**Fix (2026-06-12):** `DrawUnattachedSceneParticles` — invoked ONCE per
interior-root frame at the end of the landscape stage (pre-clear; drawn
later they would z-fail against the doorway seal), after the #124
look-ins so swirls blend over far interiors, NOT per slice (alpha
particles must not double-draw — the #121 lesson). Mutually exclusive
with the outdoor T3 pass by root kind. Residual (documented): unattached
INDOOR emitters now draw pre-clear and are overpainted by the room's
shells — same invisibility as before this fix; the proper per-emitter
cell classification is a future port.
**Apparatus (shipped, env-gated):** `ACDREAM_PROBE_OUTSTAGE=1`
`[outstage]` (per-slice routing + cone verdict per outside-stage
dynamic, print-on-change) + `[outstage-pt]` (slice Scene-particle id
set + live attached-emitter matched count). Capture: stand inside,
look at the portal through the door.
**Apparatus (kept, env-gated):** `ACDREAM_PROBE_OUTSTAGE=1`
`[outstage]` (per-slice routing + cone verdicts) + `[outstage-pt]`
(slice id set, attached matched count, unattached count).
**Gate:** stand inside, look out the doorway at the town portal — the
swirl renders through the door.
---