fix(render): feed look-in cells to particle and light visibility
Look-in rooms (cells reached through building portals) drew their geometry but never entered the visible-cell set consumed by the particle gate and the point-light candidacy scope, so their emitters stayed frozen and their lanterns dark until the player entered or the camera left the building. Retail has no look-in split: ShouldDrawParticles @0x0050FE60 gates on cell->IsInView(), and a cell installed by DrawBuilding @0x0059F2A0 -> PView::ConstructView @0x005A57B0 is drawn by the same DrawCells traversal as any flooded cell, so it is in view identically (per-cell in_view stamped by PView::InitCell @0x005A4B70). RetailPViewFrameResult now carries InViewCells (main flood + look-in cells, the existing prepareCells union; the borrowed-scratch contract is unchanged because RecycleLookInFrames runs at the start of the next DrawInside). WorldSceneRenderer feeds InViewCells to ParticleVisibilityController and the point-light scope. DrawableCells is untouched for seals, the outside-stage predicate, diagnostics, and the packed-product referee. The legacy 4-arg Reset defaults InViewCells = DrawableCells for standalone callers. User-gated 2026-08-29 at the Sanctuary Cathedral: adjacent look-in rooms show particles and lantern lights from the root cell without entry or an outside camera; recall/waterfall/Holtburg/paperdoll unregressed. The pre-existing #132-family candle-behind-opening report remains open and is unchanged by this fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
f7aa8e0eb7
commit
85530c0b7e
4 changed files with 82 additions and 9 deletions
|
|
@ -251,10 +251,13 @@ public sealed class RetailPViewRenderer
|
||||||
RenderProjectionCounts sourceCounts = frameViewBorrowed
|
RenderProjectionCounts sourceCounts = frameViewBorrowed
|
||||||
? frameView.SourceDigest.Counts
|
? frameView.SourceDigest.Counts
|
||||||
: LegacySourceCounts(partition!);
|
: LegacySourceCounts(partition!);
|
||||||
|
// prepareCells is exactly "main flood ∪ look-in cells" — the cells
|
||||||
|
// this traversal actually reached, i.e. retail's in-view set.
|
||||||
RetailPViewFrameResult result = _frameResultScratch.Reset(
|
RetailPViewFrameResult result = _frameResultScratch.Reset(
|
||||||
pvFrame,
|
pvFrame,
|
||||||
clipAssembly,
|
clipAssembly,
|
||||||
drawableCells,
|
drawableCells,
|
||||||
|
prepareCells,
|
||||||
counts,
|
counts,
|
||||||
sourceCounts,
|
sourceCounts,
|
||||||
partition);
|
partition);
|
||||||
|
|
@ -1910,6 +1913,23 @@ public sealed class RetailPViewFrameResult
|
||||||
public PortalVisibilityFrame PortalFrame { get; private set; } = null!;
|
public PortalVisibilityFrame PortalFrame { get; private set; } = null!;
|
||||||
public ClipFrameAssembly ClipAssembly { get; private set; } = null!;
|
public ClipFrameAssembly ClipAssembly { get; private set; } = null!;
|
||||||
public HashSet<uint> DrawableCells { get; private set; } = null!;
|
public HashSet<uint> DrawableCells { get; private set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Every cell this completed view actually reached: the main flood
|
||||||
|
/// (<see cref="DrawableCells"/>) plus the building look-in cells. This is
|
||||||
|
/// retail's per-cell <c>in_view</c> answer for effect consumers —
|
||||||
|
/// <c>CPhysicsObj::ShouldDrawParticles</c> @0x0050FE60 gates on
|
||||||
|
/// <c>cell->IsInView()</c>, and a cell entered through a building portal
|
||||||
|
/// (<c>PView::ConstructView</c> @0x005A57B0, installed by
|
||||||
|
/// <c>RenderDeviceD3D::DrawBuilding</c> @0x0059F2A0) is drawn by the same
|
||||||
|
/// <c>PView::DrawCells</c> traversal as a flooded cell, so retail marks it
|
||||||
|
/// in view identically. acdream's look-in adaptation keeps those cells out
|
||||||
|
/// of <see cref="DrawableCells"/> (seals / outside-stage predicate stay
|
||||||
|
/// main-flood scoped, #124); particle and light visibility must consume
|
||||||
|
/// THIS set or look-in rooms render with frozen emitters and dark lights.
|
||||||
|
/// </summary>
|
||||||
|
public HashSet<uint> InViewCells { get; private set; } = null!;
|
||||||
|
|
||||||
internal RenderFrameDiagnosticCounts DiagnosticCounts { get; private set; }
|
internal RenderFrameDiagnosticCounts DiagnosticCounts { get; private set; }
|
||||||
internal RenderProjectionCounts SourceCounts { get; private set; }
|
internal RenderProjectionCounts SourceCounts { get; private set; }
|
||||||
internal InteriorEntityPartition.Result? DiagnosticPartition
|
internal InteriorEntityPartition.Result? DiagnosticPartition
|
||||||
|
|
@ -1919,6 +1939,7 @@ public sealed class RetailPViewFrameResult
|
||||||
PortalVisibilityFrame portalFrame,
|
PortalVisibilityFrame portalFrame,
|
||||||
ClipFrameAssembly clipAssembly,
|
ClipFrameAssembly clipAssembly,
|
||||||
HashSet<uint> drawableCells,
|
HashSet<uint> drawableCells,
|
||||||
|
HashSet<uint> inViewCells,
|
||||||
RenderFrameDiagnosticCounts diagnosticCounts,
|
RenderFrameDiagnosticCounts diagnosticCounts,
|
||||||
RenderProjectionCounts sourceCounts,
|
RenderProjectionCounts sourceCounts,
|
||||||
InteriorEntityPartition.Result? diagnosticPartition)
|
InteriorEntityPartition.Result? diagnosticPartition)
|
||||||
|
|
@ -1926,6 +1947,7 @@ public sealed class RetailPViewFrameResult
|
||||||
PortalFrame = portalFrame;
|
PortalFrame = portalFrame;
|
||||||
ClipAssembly = clipAssembly;
|
ClipAssembly = clipAssembly;
|
||||||
DrawableCells = drawableCells;
|
DrawableCells = drawableCells;
|
||||||
|
InViewCells = inViewCells;
|
||||||
DiagnosticCounts = diagnosticCounts;
|
DiagnosticCounts = diagnosticCounts;
|
||||||
SourceCounts = sourceCounts;
|
SourceCounts = sourceCounts;
|
||||||
DiagnosticPartition = diagnosticPartition;
|
DiagnosticPartition = diagnosticPartition;
|
||||||
|
|
@ -1941,6 +1963,7 @@ public sealed class RetailPViewFrameResult
|
||||||
portalFrame,
|
portalFrame,
|
||||||
clipAssembly,
|
clipAssembly,
|
||||||
drawableCells,
|
drawableCells,
|
||||||
|
drawableCells,
|
||||||
RetailPViewRenderer.LegacyDiagnosticCounts(
|
RetailPViewRenderer.LegacyDiagnosticCounts(
|
||||||
diagnosticPartition),
|
diagnosticPartition),
|
||||||
RetailPViewRenderer.LegacySourceCounts(
|
RetailPViewRenderer.LegacySourceCounts(
|
||||||
|
|
|
||||||
|
|
@ -254,8 +254,14 @@ internal sealed class WorldSceneRenderer : IPreparedWorldSceneFramePhase
|
||||||
camera.Camera.View,
|
camera.Camera.View,
|
||||||
_diagnostics.CameraCellResolution));
|
_diagnostics.CameraCellResolution));
|
||||||
|
|
||||||
_particleVisibility.MarkVisibleCells(pviewResult.DrawableCells);
|
// Effect visibility consumes InViewCells (main flood ∪ look-in
|
||||||
_frames.ObserveDrawableCells(pviewResult.DrawableCells);
|
// cells), not the flood-only DrawableCells: retail's
|
||||||
|
// ShouldDrawParticles @0x0050FE60 asks cell->IsInView(), and a
|
||||||
|
// look-in cell drawn via DrawBuilding @0x0059F2A0 is in view
|
||||||
|
// exactly like a flooded cell. Flood-only scoping froze
|
||||||
|
// emitters and darkened lights in visible adjacent rooms.
|
||||||
|
_particleVisibility.MarkVisibleCells(pviewResult.InViewCells);
|
||||||
|
_frames.ObserveDrawableCells(pviewResult.InViewCells);
|
||||||
_diagnostics.EmitPViewInput(
|
_diagnostics.EmitPViewInput(
|
||||||
pviewResult.PortalFrame,
|
pviewResult.PortalFrame,
|
||||||
camera.ViewProjection,
|
camera.ViewProjection,
|
||||||
|
|
|
||||||
|
|
@ -271,8 +271,10 @@ public sealed class LightManager
|
||||||
/// geometrically closer than the player's own room's torches and win the cap,
|
/// geometrically closer than the player's own room's torches and win the cap,
|
||||||
/// leaving the visible room dark. Scoping candidacy to the frame's actual
|
/// leaving the visible room dark. Scoping candidacy to the frame's actual
|
||||||
/// visible cells (the render already computes this — callers pass last frame's
|
/// visible cells (the render already computes this — callers pass last frame's
|
||||||
/// <c>RetailPViewFrameResult.DrawableCells</c>, one frame of latency, to avoid
|
/// <c>RetailPViewFrameResult.InViewCells</c>, the main flood PLUS building
|
||||||
/// re-threading a mid-render callback) removes those from contention before the
|
/// look-in cells, one frame of latency, to avoid re-threading a mid-render
|
||||||
|
/// callback; flood-only scoping darkened look-in rooms' lanterns) removes
|
||||||
|
/// non-visible cells from contention before the
|
||||||
/// cap ever applies. The distance-sort anchor stays the PLAYER either way — this
|
/// cap ever applies. The distance-sort anchor stays the PLAYER either way — this
|
||||||
/// parameter only narrows candidacy, it does not change the sort (the #176
|
/// parameter only narrows candidacy, it does not change the sort (the #176
|
||||||
/// correction: CAMERA anchoring, not cell scoping itself, caused the earlier
|
/// correction: CAMERA anchoring, not cell scoping itself, caused the earlier
|
||||||
|
|
|
||||||
|
|
@ -251,6 +251,29 @@ public sealed class WorldSceneRendererTests
|
||||||
Assert.Equal(4, rig.PView.LastInput.RenderRadius);
|
Assert.Equal(4, rig.PView.LastInput.RenderRadius);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PViewWorld_PublishesLookInCellsToParticleAndLightVisibility()
|
||||||
|
{
|
||||||
|
var root = new LoadedCell
|
||||||
|
{
|
||||||
|
CellId = 0x01010001u,
|
||||||
|
IsOutdoorNode = false,
|
||||||
|
};
|
||||||
|
var rig = new Rig(portalVisible: false, waitingForLogin: false, clipRoot: root);
|
||||||
|
|
||||||
|
rig.Renderer.Render(default);
|
||||||
|
|
||||||
|
// Retail gates effects on cell->IsInView() (ShouldDrawParticles
|
||||||
|
// @0x0050FE60), and a cell entered through a building portal
|
||||||
|
// (DrawBuilding @0x0059F2A0 -> PView::ConstructView @0x005A57B0) is
|
||||||
|
// drawn by the same traversal as a flooded cell. The particle gate and
|
||||||
|
// the light-candidate scope must therefore receive InViewCells (flood
|
||||||
|
// plus look-ins), not the flood-only DrawableCells.
|
||||||
|
Assert.Contains(0x01010003u, rig.Visibility.MarkedCells);
|
||||||
|
Assert.NotNull(rig.Frames.ObservedCells);
|
||||||
|
Assert.Contains(0x01010003u, rig.Frames.ObservedCells!);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PViewWorld_ReusesOneSynchronousFrameInputAcrossFrames()
|
public void PViewWorld_ReusesOneSynchronousFrameInputAcrossFrames()
|
||||||
{
|
{
|
||||||
|
|
@ -536,7 +559,8 @@ public sealed class WorldSceneRendererTests
|
||||||
Frames = new FrameBuilder(Calls, frame);
|
Frames = new FrameBuilder(Calls, frame);
|
||||||
Selection = new SelectionFrame(Calls);
|
Selection = new SelectionFrame(Calls);
|
||||||
var alpha = new AlphaFrame(Calls);
|
var alpha = new AlphaFrame(Calls);
|
||||||
var visibility = new ParticleVisibility(Calls);
|
Visibility = new ParticleVisibility(Calls);
|
||||||
|
var visibility = Visibility;
|
||||||
PView = new PViewRenderer(Calls);
|
PView = new PViewRenderer(Calls);
|
||||||
Passes = new PassExecutor(Calls);
|
Passes = new PassExecutor(Calls);
|
||||||
var diagnostics = new Diagnostics(Calls);
|
var diagnostics = new Diagnostics(Calls);
|
||||||
|
|
@ -573,6 +597,8 @@ public sealed class WorldSceneRendererTests
|
||||||
|
|
||||||
public SelectionFrame Selection { get; }
|
public SelectionFrame Selection { get; }
|
||||||
|
|
||||||
|
public ParticleVisibility Visibility { get; }
|
||||||
|
|
||||||
public PViewRenderer PView { get; }
|
public PViewRenderer PView { get; }
|
||||||
|
|
||||||
public PassExecutor Passes { get; }
|
public PassExecutor Passes { get; }
|
||||||
|
|
@ -621,8 +647,13 @@ public sealed class WorldSceneRendererTests
|
||||||
return frame;
|
return frame;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ObserveDrawableCells(IReadOnlySet<uint> drawableCells) =>
|
public IReadOnlySet<uint>? ObservedCells { get; private set; }
|
||||||
|
|
||||||
|
public void ObserveDrawableCells(IReadOnlySet<uint> drawableCells)
|
||||||
|
{
|
||||||
calls.Add("frame:observe-cells");
|
calls.Add("frame:observe-cells");
|
||||||
|
ObservedCells = new HashSet<uint>(drawableCells);
|
||||||
|
}
|
||||||
|
|
||||||
public void ClearDrawableCells() => calls.Add("frame:clear-cells");
|
public void ClearDrawableCells() => calls.Add("frame:clear-cells");
|
||||||
}
|
}
|
||||||
|
|
@ -684,8 +715,13 @@ public sealed class WorldSceneRendererTests
|
||||||
private sealed class ParticleVisibility(List<string> calls) :
|
private sealed class ParticleVisibility(List<string> calls) :
|
||||||
IWorldSceneParticleVisibility
|
IWorldSceneParticleVisibility
|
||||||
{
|
{
|
||||||
public void MarkVisibleCells(HashSet<uint> cellIds) =>
|
public HashSet<uint> MarkedCells { get; } = [];
|
||||||
|
|
||||||
|
public void MarkVisibleCells(HashSet<uint> cellIds)
|
||||||
|
{
|
||||||
calls.Add("visibility:mark");
|
calls.Add("visibility:mark");
|
||||||
|
MarkedCells.UnionWith(cellIds);
|
||||||
|
}
|
||||||
|
|
||||||
public void CompleteFrame() => calls.Add("visibility:complete");
|
public void CompleteFrame() => calls.Add("visibility:complete");
|
||||||
|
|
||||||
|
|
@ -701,11 +737,17 @@ public sealed class WorldSceneRendererTests
|
||||||
public PViewRenderer(List<string> calls)
|
public PViewRenderer(List<string> calls)
|
||||||
{
|
{
|
||||||
_calls = calls;
|
_calls = calls;
|
||||||
|
// Distinct flood-only vs in-view sets: 0x01010003 is a look-in
|
||||||
|
// cell that is drawn but never part of the main flood.
|
||||||
|
var interiorPartition = new InteriorEntityPartition.Result();
|
||||||
_interiorResult = new RetailPViewFrameResult().Reset(
|
_interiorResult = new RetailPViewFrameResult().Reset(
|
||||||
new PortalVisibilityFrame(),
|
new PortalVisibilityFrame(),
|
||||||
new ClipFrameAssembly(),
|
new ClipFrameAssembly(),
|
||||||
[],
|
[0x01010001u],
|
||||||
new InteriorEntityPartition.Result());
|
[0x01010001u, 0x01010003u],
|
||||||
|
RetailPViewRenderer.LegacyDiagnosticCounts(interiorPartition),
|
||||||
|
RetailPViewRenderer.LegacySourceCounts(interiorPartition),
|
||||||
|
interiorPartition);
|
||||||
var outdoorPortalFrame = new PortalVisibilityFrame();
|
var outdoorPortalFrame = new PortalVisibilityFrame();
|
||||||
outdoorPortalFrame.OutsideView.Add(new ViewPolygon(
|
outdoorPortalFrame.OutsideView.Add(new ViewPolygon(
|
||||||
[
|
[
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue