diff --git a/docs/ISSUES.md b/docs/ISSUES.md
index 1a25c953..67b42e63 100644
--- a/docs/ISSUES.md
+++ b/docs/ISSUES.md
@@ -4581,6 +4581,67 @@ or distance.
---
+## #131 — Portal swirl invisible when viewed from inside a building through the doorway
+
+**Status:** OPEN
+**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)
+
+**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.
+
+**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.
+
+**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.
+
+---
+
+## #132 — Candle flame disappears when the through-opening background is behind it
+
+**Status:** OPEN
+**Severity:** LOW-MEDIUM
+**Filed:** 2026-06-12 (user report, #124 gate session)
+**Component:** render — cell-particle compositing vs aperture pixels
+
+**Symptom (user, axiom):** "I have a candle, when I look at the candle
+when a wall is behind it it shows, but if I turn a bit and the opening
+through a house is behind it candle light disappears."
+
+**Reading:** BACKGROUND-dependent disappearance — the candle (and its
+owner static) stays in view; only what is behind it changes. That rules
+out viewcone/owner culling (which keys on the candle's own position)
+and points at per-pixel state in the aperture region: depth left by the
+punch/seal/look-in machinery at those pixels, draw order of the cell
+particle pass vs the aperture passes, or blend state. Candidate overlap
+with the #124 look-in sub-pass (new pre-clear content in exactly those
+pixels) — check whether the symptom predates `77cef4c` by looking at a
+candle in front of a doorway WITHOUT a through-house view.
+
+**Next:** repro at the spot + `ACDREAM_PROBE_OUTSTAGE` lines for the
+same frame; then a depth-state walkthrough of the aperture pixels for
+the cell-particle pass.
+
+---
+
# Recently closed
## #113 — Phantom staircase: REOPENED 2026-06-11, folded into the HOLISTIC BUILDING-RENDER PORT
diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs
index 0202ec5b..7877c3e3 100644
--- a/src/AcDream.App/Rendering/GameWindow.cs
+++ b/src/AcDream.App/Rendering/GameWindow.cs
@@ -5094,6 +5094,9 @@ public sealed class GameWindow : IDisposable
private static uint ParticleEntityKey(AcDream.Core.World.WorldEntity entity)
=> entity.ServerGuid != 0 ? entity.ServerGuid : entity.Id;
+ // #131 [outstage-pt] probe state (throwaway — strip when #131 closes).
+ private string? _lastOutStagePtSig;
+
private static System.Numerics.Vector3 SkyPesAnchor(
AcDream.Core.World.SkyObjectData obj,
System.Numerics.Vector3 cameraWorldPos)
@@ -9638,6 +9641,27 @@ public sealed class GameWindow : IDisposable
foreach (var entity in sliceCtx.OutdoorEntities)
_outdoorSceneParticleEntityIds.Add(ParticleEntityKey(entity));
+ // #131 [outstage-pt] probe: the slice Scene-particle id set + how many
+ // live emitters the filter would actually match. Print-on-change.
+ if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeOutStageEnabled
+ && _particleSystem is not null)
+ {
+ int matched = 0, attached = 0;
+ foreach (var (emitter, _) in _particleSystem.EnumerateLive())
+ {
+ if (emitter.AttachedObjectId == 0) continue;
+ attached++;
+ if (_outdoorSceneParticleEntityIds.Contains(emitter.AttachedObjectId)) matched++;
+ }
+ string ptSig = System.FormattableString.Invariant(
+ $"ids={_outdoorSceneParticleEntityIds.Count} attachedEmitters={attached} matched={matched}");
+ if (ptSig != _lastOutStagePtSig)
+ {
+ _lastOutStagePtSig = ptSig;
+ Console.WriteLine("[outstage-pt] " + ptSig);
+ }
+ }
+
DisableClipDistances();
if (_outdoorSceneParticleEntityIds.Count > 0
&& _particleSystem is not null
diff --git a/src/AcDream.App/Rendering/RetailPViewRenderer.cs b/src/AcDream.App/Rendering/RetailPViewRenderer.cs
index 772e77f4..a3b7fc7d 100644
--- a/src/AcDream.App/Rendering/RetailPViewRenderer.cs
+++ b/src/AcDream.App/Rendering/RetailPViewRenderer.cs
@@ -397,6 +397,8 @@ public sealed class RetailPViewRenderer
if (viewcone.SphereVisibleInOutsideSlice(probeSliceIndex, c, r))
_outdoorStaticScratch.Add(e);
}
+ if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeOutStageEnabled)
+ EmitOutStageProbe(probeSliceIndex, viewcone);
probeSliceIndex++;
ctx.DrawLandscapeSlice(new RetailPViewLandscapeSliceContext(slice, _outdoorStaticScratch));
}
@@ -420,6 +422,32 @@ public sealed class RetailPViewRenderer
UseIndoorMembershipOnlyRouting();
}
+ // #131 [outstage] probe state (2026-06-12, throwaway): print-on-change —
+ // which outdoor dynamics were routed to the outside stage and which
+ // survived the slice viewcone. Strip with the probe when #131 closes.
+ private string? _lastOutStageSig;
+
+ private void EmitOutStageProbe(int sliceIndex, ViewconeCuller viewcone)
+ {
+ var sb = new System.Text.StringBuilder(192);
+ sb.Append("slice=").Append(sliceIndex)
+ .Append(" outStage=").Append(_outsideStageDynamics.Count).Append(" [");
+ for (int i = 0; i < _outsideStageDynamics.Count; i++)
+ {
+ var e = _outsideStageDynamics[i];
+ EntitySphere(e, out var c, out float r);
+ bool pass = viewcone.SphereVisibleInOutsideSlice(sliceIndex, c, r);
+ if (i > 0) sb.Append(' ');
+ sb.Append(System.FormattableString.Invariant(
+ $"0x{(e.ServerGuid != 0 ? e.ServerGuid : e.Id):X8}:{(pass ? "PASS" : "CULL")}:r={r:F1}"));
+ }
+ sb.Append(']');
+ string sig = sb.ToString();
+ if (sig == _lastOutStageSig) return;
+ _lastOutStageSig = sig;
+ Console.WriteLine("[outstage] " + sig);
+ }
+
// §4 flap [clip-route] probe state (2026-06-10, throwaway): print-on-change signature +
// monotonic sequence so held-flap vs healthy frames diff cleanly in one capture.
private string? _lastClipRouteSig;
diff --git a/src/AcDream.Core/Rendering/RenderingDiagnostics.cs b/src/AcDream.Core/Rendering/RenderingDiagnostics.cs
index 9c02119b..ba081f71 100644
--- a/src/AcDream.Core/Rendering/RenderingDiagnostics.cs
+++ b/src/AcDream.Core/Rendering/RenderingDiagnostics.cs
@@ -109,6 +109,20 @@ public static class RenderingDiagnostics
public static bool ProbeViewerEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_VIEWER") == "1";
+ ///
+ /// #131 (2026-06-12) outside-stage dynamics probe. When true, the renderer
+ /// emits one [outstage] line per CHANGE of the outside-stage
+ /// routing + per-slice cone verdict set under an interior root (which
+ /// outdoor dynamics were routed to the landscape slice, which survived the
+ /// slice viewcone), and GameWindow emits one [outstage-pt] line per
+ /// change of the slice Scene-particle id set + matched-emitter count.
+ /// Built for the portal-swirl-missing-through-doorway capture. Light:
+ /// silent while the set is stable. Initial state from
+ /// ACDREAM_PROBE_OUTSTAGE=1.
+ ///
+ public static bool ProbeOutStageEnabled { get; set; } =
+ Environment.GetEnvironmentVariable("ACDREAM_PROBE_OUTSTAGE") == "1";
+
///
/// Phase U.4c (2026-05-31) flap-convergence probe. When true, the portal
/// visibility pass emits, EVERY frame the camera root is an indoor cell, a