diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 0894eb2c..36b10937 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -4670,8 +4670,23 @@ Background-dependence explained exactly. **Fix:** the landscape stage's two-phase split (see #131 FIX 2): all scene particles moved to the LATE phase, after the look-ins. -**Gate:** the candle at the original spot — flame stays visible when -the through-opening is behind it. +**Gate 1 result (user):** indoors FIXED ("now the candle light is +visible when I'm in the house when it is in front of the opening") — +but the OUTDOOR sibling surfaced ("when I go out it is not showing +unless I turn so the angle doesn't put it in front of the opening"): +under an OUTDOOR root the merged building interiors draw AFTER the +landscape stage, so a slice-drawn flame is overpainted by the punched +aperture's interior — the residual AP-34 had already recorded. + +**Fix 2 (outdoor):** outdoor roots skip the slice Scene pass; attached +outdoor-static scene emitters draw in the POST-FRAME pass alongside the +T3 unattached pass (depth complete there — flames composite correctly +against interiors). The owner-id filter carries over; cell-pass and +dynamics-pass emitters keep their own passes (owners never in the +outdoor-static set → no double-draw). + +**Gate:** both sides — indoors with the opening behind the candle, and +outdoors at the angle that previously erased it. --- diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index 0178bfd7..5c26f137 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -129,7 +129,7 @@ accepted-divergence entries (#96, #49, #50). | AP-31 | Scenery placement drift + the 0xA9B1 road-edge tree — WB-upstream divergences from retail, ACCEPTED (**#49/#50**, 2026-05-11) | `src/AcDream.Core/World/SceneryGenerator.cs` (via `WbSceneryAdapter`) | Piecemeal patching against WB upstream is net-negative (the `e279c46` road-check attempt over-suppressed scenery elsewhere, reverted `677a726`); visible impact = a handful of trees a few meters off | The same WB-upstream class could hide a *larger* placement divergence elsewhere; revisit only via a coherent ACME-style per-vertex filter port | `CLandBlock::get_land_scenes`; ACME GameScene.cs:1074 per-vertex road filter | | AP-32 | Cell shells DRAW +0.02 m above the dat EnvCell origin (`ShellDrawLiftZ`, z-fight vs coplanar terrain); retail draws at the origin verbatim. Split invariant: PHYSICS + visibility graph UNLIFTED (f35cb8b, **#119**-residual), every DRAW-space consumer of portal/cell geometry LIFTED (OutsideView color gate via `Build(drawLiftZ)`, seal/punch fans — **#130**) | `src/AcDream.App/Rendering/GameWindow.cs:5604` (const at `PortalVisibilityBuilder.ShellDrawLiftZ`) | Shell floors coplanar with terrain z-fight in our z-buffered frame; the 2 cm lift is the documented stand-in | A new draw-space consumer of portal/cell polygons that forgets the lift re-opens a 2 cm seam at horizontal aperture edges (the #130 top-edge strip, ~7 px at 2.4 m); a visibility consumer that picks up the LIFTED transform re-opens the #119-residual horizontal-portal side-cull | retail draws cell geometry at the dat EnvCell origin (no lift) | | AP-33 | Interior-root look-in statics (**#124** sub-pass) draw WHOLE — no per-part viewcone check; retail viewconeCheck's each part vs the installed view. Look-in DYNAMICS are not drawn at all (deferred; retail draws objects per overlapped cell in the landscape stage) | `src/AcDream.App/Rendering/RetailPViewRenderer.cs` (`DrawBuildingLookIns`) | The main viewcone has no entries for look-in cells; over-include is the safe direction (z-correct, repainted outside apertures by the root's shells); look-in cell counts are small (~1-3 cells) | Statics: a few wasted draws only. Dynamics: an NPC inside a far building seen through two openings is invisible where retail shows it | `viewconeCheck` 0x0054c250; nested `DrawCells` objects pc:432878 | -| AP-34 | Landscape-stage alpha deferral is a TWO-PHASE slice split (statics-early / dynamics+particles+weather-late around the **#124** look-ins), not retail's single deferred alpha flush. Residual: under OUTDOOR roots slice particles still draw before merged building interiors (the unreported outdoor sibling of **#131**/**#132**); building exteriors' own translucent batches draw early | `src/AcDream.App/Rendering/RetailPViewRenderer.cs` (`DrawLandscapeThroughOutsideView` late loop) | The MDI dispatcher draws translucency inside each Draw call; a faithful FlushAlphaList port needs a global deferred alpha list across all landscape draws — the split covers the interior-root cases the user can see (#131 portal swirl, #132 candle flame) | Translucent landscape content drawn early and screen-overlapped by content drawn later in the stage gets overpainted (no depth self-protection) — the portal-swirl/candle-flame class re-appears in the residual configurations | `D3DPolyRender::FlushAlphaList` (DrawCells pc:432722) | +| AP-34 | Landscape-stage alpha deferral is a TWO-PHASE slice split (statics-early / dynamics+particles+weather-late around the **#124** look-ins) + outdoor-root attached scene emitters moved to the post-frame pass, not retail's single deferred alpha flush. Residual: building exteriors' / outside-stage dynamics' own translucent MESH batches still draw within their stage draw call (before later stage content) | `src/AcDream.App/Rendering/RetailPViewRenderer.cs` (`DrawLandscapeThroughOutsideView` late loop) + `GameWindow` post-frame Scene pass | The MDI dispatcher draws translucency inside each Draw call; a faithful FlushAlphaList port needs a global deferred alpha list across all landscape draws — the split covers the user-visible cases (#131 portal swirl, #132 candle flame indoors + outdoors) | Translucent landscape content drawn early and screen-overlapped by content drawn later in the stage gets overpainted (no depth self-protection) — the portal-swirl/candle-flame class re-appears in the residual configurations | `D3DPolyRender::FlushAlphaList` (DrawCells pc:432722) | --- diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 5a0f7c74..a60ed6a8 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -5096,6 +5096,7 @@ public sealed class GameWindow : IDisposable // #131 [outstage-pt] probe state (throwaway — strip when #131 closes). private string? _lastOutStagePtSig; + private readonly HashSet _outStageUnmatchedScratch = new(); private static System.Numerics.Vector3 SkyPesAnchor( AcDream.Core.World.SkyObjectData obj, @@ -7841,7 +7842,8 @@ public sealed class GameWindow : IDisposable renderSky, renderWeather: playerSeenOutside, kf, - environOverrideActive), + environOverrideActive, + isOutdoorRoot: clipRoot.IsOutdoorNode), // T1: retail's depth discipline (PView::DrawCells, Ghidra 0x005a4840). // INTERIOR roots: one FULL depth clear between the outside stage and // the interior stage, then SEALS re-stamp every outside-leading @@ -8002,20 +8004,26 @@ public sealed class GameWindow : IDisposable && _particleSystem is not null && _particleRenderer is not null) { // T3 (BR-5): unattached emitters (campfires, ground effects — - // AttachedObjectId == 0) under the OUTDOOR root. The unified - // path's attached emitters draw via the landscape slice + the - // per-cell callbacks; unattached ones had NO pass on - // outdoor-node frames (the unattached-particles-dropped- - // outdoors divergence, adjusted-confirmed). The outdoor root's - // outside view is full-screen (cone pass-all); depth test - // composites them against the world. + // AttachedObjectId == 0) under the OUTDOOR root. The outdoor + // root's outside view is full-screen (cone pass-all); depth + // test composites them against the world. + // #132 outdoor sibling: ATTACHED outdoor-static scene emitters + // (lantern/candle flames) moved here too — drawn in the + // landscape slice they were overpainted by merged building + // interiors (drawn later) whenever a punched aperture sat + // behind them. Post-frame, depth is complete and the flames + // composite correctly. The owner-id set is the late slice's + // (full-screen cone outdoors). Cell-pass and dynamics-pass + // emitters keep their own passes (no double-draw: their owners + // are never in the outdoor-static id set). sigSceneParticles = sigSceneParticles == "none" ? "unattached" : sigSceneParticles + "+unattached"; _particleRenderer.Draw( _particleSystem, camera, camPos, AcDream.Core.Vfx.ParticleRenderPass.Scene, - emitter => emitter.AttachedObjectId == 0); + emitter => emitter.AttachedObjectId == 0 + || _outdoorSceneParticleEntityIds.Contains(emitter.AttachedObjectId)); } // Bug A fix (post-#26 worktree, 2026-04-26): weather sky @@ -9697,7 +9705,8 @@ public sealed class GameWindow : IDisposable bool renderSky, bool renderWeather, AcDream.Core.World.SkyKeyframe kf, - bool environOverrideActive) + bool environOverrideActive, + bool isOutdoorRoot) { var slice = lateCtx.Slice; bool scissor = BeginDoorwayScissor(true, slice.NdcAabb); @@ -9724,19 +9733,28 @@ public sealed class GameWindow : IDisposable _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. + // live emitters the filter would actually match, plus the distinct + // UNMATCHED attached owner ids (the portal-identification handle — + // an emitter whose owner never lands in the set draws nowhere + // indoors). Print-on-change. if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeOutStageEnabled && _particleSystem is not null) { int matched = 0, attached = 0, unattached = 0; + _outStageUnmatchedScratch.Clear(); foreach (var (emitter, _) in _particleSystem.EnumerateLive()) { if (emitter.AttachedObjectId == 0) { unattached++; continue; } attached++; if (_outdoorSceneParticleEntityIds.Contains(emitter.AttachedObjectId)) matched++; + else if (_outStageUnmatchedScratch.Count < 12) + _outStageUnmatchedScratch.Add(emitter.AttachedObjectId); } + var unm = new System.Text.StringBuilder(96); + foreach (uint id in _outStageUnmatchedScratch) + unm.Append(System.FormattableString.Invariant($" 0x{id:X8}")); string ptSig = System.FormattableString.Invariant( - $"ids={_outdoorSceneParticleEntityIds.Count} attachedEmitters={attached} matched={matched} unattached={unattached}"); + $"ids={_outdoorSceneParticleEntityIds.Count} attachedEmitters={attached} matched={matched} unattached={unattached} unmatchedIds=[{unm}]"); if (ptSig != _lastOutStagePtSig) { _lastOutStagePtSig = ptSig; @@ -9744,7 +9762,17 @@ public sealed class GameWindow : IDisposable } } - if (_outdoorSceneParticleEntityIds.Count > 0 + // #132 outdoor sibling: under an OUTDOOR root the merged building + // interiors draw AFTER this stage (DrawEnvCellShells) — a flame drawn + // here is overpainted whenever a punched aperture sits behind it + // (user-confirmed at the outdoor candle). Outdoor roots therefore + // SKIP the slice Scene pass and draw attached scene particles in the + // post-frame pass alongside the T3 unattached pass (the id set built + // above carries over — the outdoor root has a single full-screen + // slice). Interior roots draw here: the look-ins already ran and the + // post-clear seal discipline owns the rest of the frame. + if (!isOutdoorRoot + && _outdoorSceneParticleEntityIds.Count > 0 && _particleSystem is not null && _particleRenderer is not null) { diff --git a/src/AcDream.App/Rendering/RetailPViewRenderer.cs b/src/AcDream.App/Rendering/RetailPViewRenderer.cs index 2b39b1e6..f23d419c 100644 --- a/src/AcDream.App/Rendering/RetailPViewRenderer.cs +++ b/src/AcDream.App/Rendering/RetailPViewRenderer.cs @@ -488,7 +488,7 @@ public sealed class RetailPViewRenderer 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}")); + $"0x{(e.ServerGuid != 0 ? e.ServerGuid : e.Id):X8}(s{e.SourceGfxObjOrSetupId:X8}):{(pass ? "PASS" : "CULL")}:r={r:F1}")); } sb.Append(']'); string sig = sb.ToString();