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