acdream/tests/AcDream.App.Tests/Rendering/WorldSceneRendererTests.cs
Erik 85530c0b7e 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>
2026-08-29 11:44:19 +02:00

969 lines
34 KiB
C#

using System.Numerics;
using System.Reflection;
using AcDream.App.Composition;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Packs;
using AcDream.App.Rendering.Selection;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Streaming;
using AcDream.App.Tests.Architecture;
using AcDream.Core.Rendering;
using AcDream.Core.World;
using AcDream.Runtime;
using AcDream.Runtime.World;
namespace AcDream.App.Tests.Rendering;
public sealed class WorldSceneRendererTests
{
[Fact]
public void EnhancedPreparation_BuildsCanonicalWorldOnceBeforeExecution()
{
var rig = new Rig(portalVisible: false, waitingForLogin: false, clipRoot: null);
PreparedWorldSceneFrame prepared = rig.Renderer.PrepareEnhanced(default);
WorldRenderFrameOutcome result = rig.Renderer.RenderPreparedEnhanced(
default,
in prepared);
Assert.True(result.NormalWorldDrawn);
Assert.True(prepared.ShouldRender);
Assert.Equal(1, rig.Calls.Count(value => value == "frame:build"));
Assert.True(rig.Calls.IndexOf("frame:build") < rig.Calls.IndexOf("selection:begin"));
Assert.Equal(prepared.World.Camera.Frustum, rig.Selection.PreparedViewFrustum);
Assert.Equal(rig.Entities.ResidentWindow, prepared.World.ResidentStreamingWindow);
Assert.Equal((0, 0), rig.Entities.LastResidentWindowCenter);
}
[Fact]
public void EnhancedPreparation_AttachesTheSelectedAuthoredCelestialSource()
{
var rig = new Rig(portalVisible: false, waitingForLogin: false, clipRoot: null);
rig.DayGroup.SkyObjects =
[
new SkyObjectData
{
GfxObjId = AuthoredCelestialShadowSourceResolver.SunGfxObjId,
AuthoredSortCenter = Vector3.UnitX,
BeginTime = 0f,
EndTime = 0f,
BeginAngle = 90f,
EndAngle = 90f,
},
];
PreparedWorldSceneFrame prepared = rig.Renderer.PrepareEnhanced(default);
Assert.True(prepared.ShouldRender);
Assert.Equal(
AuthoredCelestialShadowSourceKind.Sun,
prepared.World.CelestialShadowSource.Kind);
Assert.Equal(0, prepared.World.CelestialShadowSource.ObjectIndex);
Assert.Equal(
AuthoredCelestialShadowSourceResolver.SunGfxObjId,
prepared.World.CelestialShadowSource.GfxObjId);
Assert.InRange(
Vector3.Distance(
Vector3.UnitZ,
prepared.World.CelestialShadowSource.SurfaceToLightDirection),
0f,
1e-5f);
rig.Renderer.CancelPreparedEnhanced(in prepared);
}
[Fact]
public void EnhancedPreparation_SkippedWorldStillPublishesEmptySelectionFrame()
{
var rig = new Rig(portalVisible: true, waitingForLogin: false, clipRoot: null);
PreparedWorldSceneFrame prepared = rig.Renderer.PrepareEnhanced(default);
WorldRenderFrameOutcome result = rig.Renderer.RenderPreparedEnhanced(
default,
in prepared);
Assert.False(prepared.ShouldRender);
Assert.Equal(default, result);
Assert.Equal(["selection:begin", "selection:complete"], rig.Calls);
}
[Fact]
public void CancelledEnhancedPrepass_DoesNotPoisonTheNextFrame()
{
var rig = new Rig(portalVisible: false, waitingForLogin: false, clipRoot: null);
PreparedWorldSceneFrame failed = rig.Renderer.PrepareEnhanced(default);
// Models an exception in the shadow prepass before a world pass opens.
rig.Renderer.CancelPreparedEnhanced(in failed);
PreparedWorldSceneFrame recovered = rig.Renderer.PrepareEnhanced(default);
WorldRenderFrameOutcome result = rig.Renderer.RenderPreparedEnhanced(
default,
in recovered);
Assert.True(result.NormalWorldDrawn);
Assert.Equal(2, rig.Calls.Count(value => value == "frame:build"));
}
[Fact]
public void PortalViewport_PublishesEmptySelectionFrameAndSkipsWorldOwners()
{
var rig = new Rig(portalVisible: true, waitingForLogin: false, clipRoot: null);
WorldRenderFrameOutcome result = rig.Renderer.Render(default);
Assert.Equal(default, result);
Assert.Equal(["selection:begin", "selection:complete"], rig.Calls);
}
[Fact]
public void QuiescedGeneration_PublishesEmptySelectionFrameAndSkipsEveryWorldOwner()
{
var transit = new RuntimeWorldTransitState();
var availability = new WorldGenerationAvailabilityState(transit);
RuntimeWorldTransitTestDriver.BeginPortal(
transit,
0x11340021u);
var rig = new Rig(
portalVisible: false,
waitingForLogin: false,
clipRoot: null,
availability: availability);
WorldRenderFrameOutcome result = rig.Renderer.Render(default);
Assert.Equal(default, result);
Assert.Equal(["selection:begin", "selection:complete"], rig.Calls);
}
[Fact]
public void LoginWait_IsOwnedByTheFrameGate_NotByARendererShortCircuit()
{
// Gate-fix round (2026-08-17): the sky-only waiting skip is DELETED.
// Live pre-world frames present the portal-viewport frame shape —
// LocalPlayerTeleportRenderStateSource folds IsWaitingForLogin into
// PortalViewportVisible (retail's pre-player gameplay screen draws
// NO world: black behind the UI, never a sky-only backdrop), so in
// production this renderer NEVER runs while waiting. One gate
// computes the frame's visibility and this phase only enforces it: a
// waiting flag without the portal cover (unreachable in production)
// draws the ordinary flat world rather than re-implementing a second
// gate here.
var rig = new Rig(portalVisible: false, waitingForLogin: true, clipRoot: null);
WorldRenderFrameOutcome result = rig.Renderer.Render(default);
Assert.True(result.NormalWorldDrawn);
Assert.True(rig.Frames.WaitingForLogin);
Assert.Contains("flat:terrain", rig.Calls);
// The production waiting frame: the portal cover is set, the world
// is skipped whole, and only the selection frame brackets run.
var covered = new Rig(portalVisible: true, waitingForLogin: true, clipRoot: null);
Assert.Equal(default, covered.Renderer.Render(default));
Assert.Equal(["selection:begin", "selection:complete"], covered.Calls);
}
[Fact]
public void FlatWorld_PreservesTerrainEntityParticleWeatherAndCompletionOrder()
{
var rig = new Rig(portalVisible: false, waitingForLogin: false, clipRoot: null);
WorldRenderFrameOutcome result = rig.Renderer.Render(default);
Assert.Equal(new WorldRenderFrameOutcome(3, 8, true), result);
Assert.Equal(
[
"selection:begin",
"alpha:begin",
"frame:build",
"passes:begin",
"flat:clip",
"flat:sky",
"flat:terrain",
"flat:entities",
"frame:clear-cells",
"passes:disable-clip",
"particles:global",
"flat:weather",
"diagnostics:signature:OutdoorRoot",
"diagnostics:draw",
"alpha:end",
"visibility:mark",
"visibility:complete",
"selection:complete",
],
rig.Calls);
Assert.Equal(rig.Foundation, rig.Frames.Foundation);
Assert.False(rig.Frames.WaitingForLogin);
Assert.Same(rig.DayGroup, rig.Frames.ActiveDayGroup);
Assert.Equal(rig.Foundation, rig.Passes.SkyFoundation);
Assert.Equal(rig.Foundation, rig.Passes.WeatherFoundation);
Assert.Same(rig.DayGroup, rig.Passes.SkyDayGroup);
Assert.Same(rig.DayGroup, rig.Passes.WeatherDayGroup);
Assert.Equal(rig.DayFraction, rig.Passes.SkyDayFraction);
Assert.Equal(rig.DayFraction, rig.Passes.WeatherDayFraction);
}
[Fact]
public void PViewWorld_UsesOnePViewProductAndPublishesItsDrawableCells()
{
var root = new LoadedCell
{
CellId = 0x01010001u,
IsOutdoorNode = false,
};
var rig = new Rig(portalVisible: false, waitingForLogin: false, clipRoot: root);
WorldRenderFrameOutcome result = rig.Renderer.Render(default);
Assert.True(result.NormalWorldDrawn);
Assert.Equal(
[
"selection:begin",
"alpha:begin",
"frame:build",
"passes:begin",
"pview:draw",
"visibility:mark",
"frame:observe-cells",
"diagnostics:pview",
"passes:disable-clip",
"particles:none",
"diagnostics:signature:RetailPViewInside",
"diagnostics:draw",
"alpha:end",
"visibility:mark",
"visibility:complete",
"selection:complete",
],
rig.Calls);
Assert.DoesNotContain("flat:clip", rig.Calls);
Assert.DoesNotContain("flat:terrain", rig.Calls);
Assert.DoesNotContain("flat:entities", rig.Calls);
Assert.NotNull(rig.PView.LastInput);
Assert.Same(rig.DayGroup, rig.PView.LastInput!.ActiveDayGroup);
Assert.Equal(rig.DayFraction, rig.PView.LastInput.DayFraction);
Assert.Equal(rig.Foundation.Sky, rig.PView.LastInput.SkyKeyframe);
Assert.True(rig.PView.LastInput.RenderWeather);
Assert.Equal(root.CellId, rig.PView.LastInput.ViewerCellId);
Assert.Equal(0x01010002u, rig.PView.LastInput.PlayerCellId);
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]
public void PViewWorld_ReusesOneSynchronousFrameInputAcrossFrames()
{
var root = new LoadedCell
{
CellId = 0x01010001u,
IsOutdoorNode = false,
};
var rig = new Rig(
portalVisible: false,
waitingForLogin: false,
clipRoot: root);
rig.Renderer.Render(default);
RetailPViewFrameInput first = Assert.IsType<RetailPViewFrameInput>(
rig.PView.LastInput);
rig.Calls.Clear();
rig.Renderer.Render(default);
Assert.Same(first, rig.PView.LastInput);
Assert.Same(root, rig.PView.LastInput!.RootCell);
Assert.Equal(rig.DayFraction, rig.PView.LastInput.DayFraction);
Assert.Contains("pview:draw", rig.Calls);
}
[Fact]
public void OutdoorPView_UsesPViewOwnersForPostWorldParticlesWithoutFlatWeather()
{
var root = new LoadedCell
{
CellId = 0x01010001u,
IsOutdoorNode = true,
};
var rig = new Rig(portalVisible: false, waitingForLogin: false, clipRoot: root);
WorldRenderFrameOutcome result = rig.Renderer.Render(default);
Assert.True(result.NormalWorldDrawn);
Assert.Contains("particles:pviewScoped", rig.Calls);
Assert.Same(rig.PView.OutdoorSceneParticleEntityIds, rig.Passes.ParticleOwners);
Assert.Equal([0xCAFEu], rig.Passes.ParticleOwners);
Assert.DoesNotContain("flat:weather", rig.Calls);
}
[Fact]
public void SealedInteriorPView_DisablesWeatherAndPropagatesEnvironmentOverride()
{
var root = new LoadedCell
{
CellId = 0x01010001u,
IsOutdoorNode = false,
};
var rig = new Rig(
portalVisible: false,
waitingForLogin: false,
clipRoot: root,
playerSeenOutside: false,
environOverride: EnvironOverride.BlueFog);
rig.Renderer.Render(default);
Assert.NotNull(rig.PView.LastInput);
Assert.False(rig.PView.LastInput!.RenderWeather);
Assert.True(rig.PView.LastInput.EnvironOverrideActive);
}
[Fact]
public void PViewFailure_AbortsPortalStateAndRecoversOnTheNextFrame()
{
var root = new LoadedCell
{
CellId = 0x01010001u,
IsOutdoorNode = false,
};
var rig = new Rig(portalVisible: false, waitingForLogin: false, clipRoot: root);
rig.PView.ThrowOnDraw = true;
Assert.Throws<InvalidOperationException>(() => rig.Renderer.Render(default));
Assert.Equal(
[
"selection:begin",
"alpha:begin",
"frame:build",
"passes:begin",
"pview:draw",
"pview:abort",
"passes:abort",
"visibility:abort",
"alpha:abort",
"selection:abort",
],
rig.Calls);
rig.Calls.Clear();
rig.PView.ThrowOnDraw = false;
Assert.True(rig.Renderer.Render(default).NormalWorldDrawn);
Assert.Contains("selection:complete", rig.Calls);
}
[Fact]
public void DrawFailure_DoesNotPublishPartialAlphaSelectionOrParticleFrames()
{
var rig = new Rig(portalVisible: false, waitingForLogin: false, clipRoot: null);
rig.Passes.ThrowOnTerrain = true;
Assert.Throws<InvalidOperationException>(() => rig.Renderer.Render(default));
Assert.Equal(
[
"selection:begin",
"alpha:begin",
"frame:build",
"passes:begin",
"flat:clip",
"flat:sky",
"flat:terrain",
"passes:abort",
"visibility:abort",
"alpha:abort",
"selection:abort",
],
rig.Calls);
rig.Calls.Clear();
rig.Passes.ThrowOnTerrain = false;
WorldRenderFrameOutcome recovered = rig.Renderer.Render(default);
Assert.True(recovered.NormalWorldDrawn);
Assert.Contains("alpha:end", rig.Calls);
Assert.Contains("selection:complete", rig.Calls);
}
[Fact]
public void AbortFailure_IsAggregatedAfterEveryOtherFrameOwnerIsAborted()
{
var rig = new Rig(portalVisible: false, waitingForLogin: false, clipRoot: null);
rig.Passes.ThrowOnTerrain = true;
rig.Passes.ThrowOnAbort = true;
AggregateException failure = Assert.Throws<AggregateException>(
() => rig.Renderer.Render(default));
Assert.Contains(
failure.InnerExceptions,
error => error is InvalidOperationException
&& error.Message == "terrain failed");
Assert.Contains(
failure.InnerExceptions,
error => error is InvalidOperationException
&& error.Message == "pass abort failed");
Assert.Contains("visibility:abort", rig.Calls);
Assert.Contains("alpha:abort", rig.Calls);
Assert.Contains("selection:abort", rig.Calls);
rig.Calls.Clear();
rig.Passes.ThrowOnTerrain = false;
rig.Passes.ThrowOnAbort = false;
Assert.True(rig.Renderer.Render(default).NormalWorldDrawn);
}
[Fact]
public void GameWindow_DelegatesWorldRenderingWithoutOwningDrawBranches()
{
MethodInfo compose = typeof(FrameRootCompositionPhase).GetMethod(
"ComposeCore",
BindingFlags.Instance | BindingFlags.NonPublic)!;
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(compose);
int worldRenderer = RequiredCallIndex(
calls,
typeof(WorldSceneRenderer),
".ctor");
int frameOrchestrator = RequiredCallIndex(
calls,
typeof(RenderFrameOrchestrator),
".ctor");
Assert.True(worldRenderer < frameOrchestrator);
Assert.Single(
calls,
call => call.Target.DeclaringType == typeof(WorldSceneRenderer)
&& call.Target.Name == ".ctor");
Assert.Single(
calls,
call => call.Target.DeclaringType == typeof(RenderFrameOrchestrator)
&& call.Target.Name == ".ctor");
FieldInfo[] windowFields = typeof(GameWindow).GetFields(
BindingFlags.Instance
| BindingFlags.Public
| BindingFlags.NonPublic
| BindingFlags.DeclaredOnly);
Assert.DoesNotContain(
windowFields,
field => field.FieldType == typeof(WorldSceneRenderer)
|| field.FieldType == typeof(RetailPViewRenderer)
|| field.FieldType == typeof(TerrainDrawDiagnosticsController)
|| field.FieldType == typeof(RenderFrameOrchestrator));
FieldInfo worldPhase = Assert.Single(
typeof(RenderFrameOrchestrator).GetFields(
BindingFlags.Instance | BindingFlags.NonPublic),
field => field.FieldType == typeof(IWorldSceneFramePhase));
Assert.Equal("_world", worldPhase.Name);
}
[Fact]
public void ExtractedWorldOwners_RetainNoWindowDelegateOrBorrowedPViewProduct()
{
Type[] ownerTypes =
[
typeof(WorldSceneRenderer),
typeof(WorldScenePassExecutor),
typeof(WorldSceneDiagnosticsController),
];
Type[] borrowedTypes =
[
typeof(RetailPViewFrameResult),
typeof(PortalVisibilityFrame),
typeof(ClipFrameAssembly),
];
foreach (Type owner in ownerTypes)
{
foreach (FieldInfo field in owner.GetFields(
BindingFlags.Instance
| BindingFlags.Public
| BindingFlags.NonPublic))
{
Assert.False(
typeof(GameWindow).IsAssignableFrom(field.FieldType),
$"{owner.Name}.{field.Name} retains GameWindow.");
Assert.False(
typeof(Delegate).IsAssignableFrom(field.FieldType),
$"{owner.Name}.{field.Name} retains a delegate callback.");
Assert.DoesNotContain(field.FieldType, borrowedTypes);
}
}
}
private sealed class Rig
{
public Rig(
bool portalVisible,
bool waitingForLogin,
LoadedCell? clipRoot,
bool? playerSeenOutside = null,
EnvironOverride environOverride = EnvironOverride.None,
IWorldGenerationAvailability? availability = null)
{
Calls = [];
DayGroup = new DayGroupData { Name = "sentinel-day-group" };
DayFraction = 0.375f;
Foundation = new RenderFrameFoundation(
portalVisible,
new SkyKeyframe(
0.25f,
35f,
45f,
new Vector3(0.1f, 0.2f, 0.3f),
0.8f,
new Vector3(0.4f, 0.5f, 0.6f),
0.7f,
new Vector3(0.7f, 0.8f, 0.9f),
0.2f),
new AtmosphereSnapshot(
WeatherKind.Clear,
1f,
new Vector3(0.1f, 0.2f, 0.3f),
1f,
100f,
FogMode.Linear,
0f,
environOverride));
var foundation = new FoundationSource(Foundation);
var login = new LoginSource(waitingForLogin);
var sky = new SkySource(DayGroup, DayFraction);
var frame = CreateFrame(
clipRoot,
playerSeenOutside ?? clipRoot is not null);
Frames = new FrameBuilder(Calls, frame);
Selection = new SelectionFrame(Calls);
var alpha = new AlphaFrame(Calls);
Visibility = new ParticleVisibility(Calls);
var visibility = Visibility;
PView = new PViewRenderer(Calls);
Passes = new PassExecutor(Calls);
var diagnostics = new Diagnostics(Calls);
Entities = new EntitySource();
Renderer = new WorldSceneRenderer(
foundation,
login,
sky,
Frames,
Entities,
Selection,
alpha,
visibility,
PView,
new PViewCells(),
Passes,
new WorldRenderRangeState(4, 12),
diagnostics,
availability);
}
public List<string> Calls { get; }
public RenderFrameFoundation Foundation { get; }
public DayGroupData DayGroup { get; }
public float DayFraction { get; }
public FrameBuilder Frames { get; }
public EntitySource Entities { get; }
public SelectionFrame Selection { get; }
public ParticleVisibility Visibility { get; }
public PViewRenderer PView { get; }
public PassExecutor Passes { get; }
public WorldSceneRenderer Renderer { get; }
}
private sealed class FoundationSource(RenderFrameFoundation foundation) :
IRenderFrameFoundationSource
{
public RenderFrameFoundation Foundation { get; } = foundation;
}
private sealed class LoginSource(bool waiting) : IRenderLoginStateSource
{
public bool IsWaitingForLogin { get; } = waiting;
}
private sealed class SkySource(
DayGroupData activeDayGroup,
float dayFraction) : IWorldSceneSkyStateSource
{
public DayGroupData? ActiveDayGroup { get; } = activeDayGroup;
public float DayFraction { get; } = dayFraction;
}
private sealed class FrameBuilder(List<string> calls, WorldRenderFrame frame) :
IWorldRenderFrameBuilder
{
public RenderFrameFoundation Foundation { get; private set; }
public bool WaitingForLogin { get; private set; }
public DayGroupData? ActiveDayGroup { get; private set; }
public WorldRenderFrame Build(
in RenderFrameFoundation foundation,
bool waitingForLogin,
DayGroupData? activeDayGroup)
{
calls.Add("frame:build");
Foundation = foundation;
WaitingForLogin = waitingForLogin;
ActiveDayGroup = activeDayGroup;
return frame;
}
public IReadOnlySet<uint>? ObservedCells { get; private set; }
public void ObserveDrawableCells(IReadOnlySet<uint> drawableCells)
{
calls.Add("frame:observe-cells");
ObservedCells = new HashSet<uint>(drawableCells);
}
public void ClearDrawableCells() => calls.Add("frame:clear-cells");
}
private sealed class EntitySource : IWorldSceneEntitySource
{
public ResidentStreamingWindowFact ResidentWindow { get; } = new(
Revision: 77,
CenterX: 0,
CenterY: 0,
CompleteRadiusLandblocks: 2,
PublishedLandblockCount: 25,
HasPublishedCenter: true);
public (int X, int Y)? LastResidentWindowCenter { get; private set; }
public IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
IReadOnlyList<WorldEntity> Entities,
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntries =>
Array.Empty<(uint, Vector3, Vector3, IReadOnlyList<WorldEntity>,
IReadOnlyDictionary<uint, WorldEntity>?)>();
public IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> LandblockBounds =>
Array.Empty<(uint, Vector3, Vector3)>();
public ResidentStreamingWindowFact CaptureResidentStreamingWindow(
int centerX,
int centerY)
{
LastResidentWindowCenter = (centerX, centerY);
return ResidentWindow;
}
}
private sealed class SelectionFrame(List<string> calls) : IWorldSceneSelectionFrame
{
public FrustumPlanes? PreparedViewFrustum { get; private set; }
public void BeginFrame(FrustumPlanes? preparedViewFrustum = null)
{
PreparedViewFrustum = preparedViewFrustum;
calls.Add("selection:begin");
}
public void CompleteFrame() => calls.Add("selection:complete");
public void AbortFrame() => calls.Add("selection:abort");
}
private sealed class AlphaFrame(List<string> calls) : IWorldSceneAlphaFrame
{
public void BeginFrame() => calls.Add("alpha:begin");
public void EndFrame() => calls.Add("alpha:end");
public void AbortFrame() => calls.Add("alpha:abort");
}
private sealed class ParticleVisibility(List<string> calls) :
IWorldSceneParticleVisibility
{
public HashSet<uint> MarkedCells { get; } = [];
public void MarkVisibleCells(HashSet<uint> cellIds)
{
calls.Add("visibility:mark");
MarkedCells.UnionWith(cellIds);
}
public void CompleteFrame() => calls.Add("visibility:complete");
public void AbortFrame() => calls.Add("visibility:abort");
}
private sealed class PViewRenderer : IWorldScenePViewRenderer
{
private readonly List<string> _calls;
private readonly RetailPViewFrameResult _interiorResult;
private readonly RetailPViewFrameResult _outdoorResult;
public PViewRenderer(List<string> 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(
new PortalVisibilityFrame(),
new ClipFrameAssembly(),
[0x01010001u],
[0x01010001u, 0x01010003u],
RetailPViewRenderer.LegacyDiagnosticCounts(interiorPartition),
RetailPViewRenderer.LegacySourceCounts(interiorPartition),
interiorPartition);
var outdoorPortalFrame = new PortalVisibilityFrame();
outdoorPortalFrame.OutsideView.Add(new ViewPolygon(
[
new Vector2(-1f, -1f),
new Vector2(1f, -1f),
new Vector2(1f, 1f),
new Vector2(-1f, 1f),
]));
_outdoorResult = new RetailPViewFrameResult().Reset(
outdoorPortalFrame,
ClipFrameAssembler.Assemble(ClipFrame.NoClip(), outdoorPortalFrame),
[],
new InteriorEntityPartition.Result());
}
public IReadOnlySet<uint> OutdoorSceneParticleEntityIds { get; } =
new HashSet<uint> { 0xCAFEu };
public RetailPViewFrameInput? LastInput { get; private set; }
public bool ThrowOnDraw { get; set; }
public RetailPViewFrameResult DrawInside(RetailPViewFrameInput input)
{
_calls.Add("pview:draw");
LastInput = input;
if (ThrowOnDraw)
throw new InvalidOperationException("pview failed");
return input.RootCell.IsOutdoorNode ? _outdoorResult : _interiorResult;
}
public void AbortFrame() => _calls.Add("pview:abort");
}
private sealed class PViewCells : IRetailPViewCellSource
{
public LoadedCell? Find(uint cellId) => null;
}
private sealed class PassExecutor(List<string> calls) : IWorldScenePassExecutor
{
public bool ThrowOnTerrain { get; set; }
public bool ThrowOnAbort { get; set; }
public HashSet<uint>? TerrainVisibleCellIds { get; } = [0x01010001u];
public RenderFrameFoundation? SkyFoundation { get; private set; }
public RenderFrameFoundation? WeatherFoundation { get; private set; }
public DayGroupData? SkyDayGroup { get; private set; }
public DayGroupData? WeatherDayGroup { get; private set; }
public float SkyDayFraction { get; private set; }
public float WeatherDayFraction { get; private set; }
public IReadOnlySet<uint>? ParticleOwners { get; private set; }
public void BeginFrame() => calls.Add("passes:begin");
public void PrepareFlatWorldClip() => calls.Add("flat:clip");
public void DrawFlatSky(
in WorldCameraFrame camera,
in RenderFrameFoundation foundation,
DayGroupData? activeDayGroup,
float dayFraction)
{
calls.Add("flat:sky");
SkyFoundation = foundation;
SkyDayGroup = activeDayGroup;
SkyDayFraction = dayFraction;
}
public void DrawFlatTerrain(in WorldCameraFrame camera, uint? playerLandblockId)
{
calls.Add("flat:terrain");
if (ThrowOnTerrain)
throw new InvalidOperationException("terrain failed");
}
public void DrawFlatEntities(
in WorldCameraFrame camera,
IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
IReadOnlyList<WorldEntity> Entities,
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> entries,
uint? playerLandblockId,
HashSet<uint> animatedEntityIds) => calls.Add("flat:entities");
public string DrawPostWorldParticles(
LoadedCell? clipRoot,
ClipFrameAssembly? clipAssembly,
in WorldCameraFrame camera,
IReadOnlySet<uint> outdoorOwnerIds,
string currentSignature)
{
ParticleOwners = outdoorOwnerIds;
string kind = clipRoot switch
{
null => "global",
{ IsOutdoorNode: true } => currentSignature,
_ => currentSignature,
};
calls.Add($"particles:{kind}");
return kind;
}
public void DrawFlatWeather(
in WorldCameraFrame camera,
in RenderFrameFoundation foundation,
DayGroupData? activeDayGroup,
float dayFraction)
{
calls.Add("flat:weather");
WeatherFoundation = foundation;
WeatherDayGroup = activeDayGroup;
WeatherDayFraction = dayFraction;
}
public void DisableClipDistances() => calls.Add("passes:disable-clip");
public void AbortFrame()
{
calls.Add("passes:abort");
if (ThrowOnAbort)
throw new InvalidOperationException("pass abort failed");
}
}
private sealed class Diagnostics(List<string> calls) : IWorldSceneDiagnostics
{
public CameraCellResolution CameraCellResolution => CameraCellResolution.None;
public void EmitPViewInput(
PortalVisibilityFrame portalFrame,
Matrix4x4 viewProjection,
LoadedCell clipRoot,
Vector3 cameraPosition,
Vector3 playerPosition) => calls.Add("diagnostics:pview");
public void EmitRenderSignature(
string branch,
LoadedCell? clipRoot,
LoadedCell? viewerRoot,
LoadedCell? playerRoot,
uint viewerCellId,
uint playerCellId,
bool playerIndoorGate,
bool cameraInsideCell,
bool renderSky,
bool drawSkyThisFrame,
bool terrainDrawn,
TerrainClipMode terrainClipMode,
bool skyDrawn,
bool depthClear,
bool outdoorSceneryDrawn,
int liveDynamicDrawnCount,
string sceneParticles,
RetailPViewFrameResult? pviewResult,
Vector3 cameraPosition,
Vector3 playerPosition) =>
calls.Add($"diagnostics:signature:{branch}");
public WorldSceneDiagnosticOutcome DrawAndPublish(
in WorldCameraFrame camera,
IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> bounds)
{
calls.Add("diagnostics:draw");
return new WorldSceneDiagnosticOutcome(3, 8);
}
}
private static WorldRenderFrame CreateFrame(
LoadedCell? clipRoot,
bool playerSeenOutside)
{
var camera = new FlyCamera { Position = new Vector3(1f, 2f, 3f) };
var cameraFrame = new WorldCameraFrame(
camera,
camera.Projection,
camera.View * camera.Projection,
default,
Matrix4x4.Identity,
camera.Position);
var roots = new WorldRootFrame(
PlayerRoot: null,
PlayerSeenOutside: playerSeenOutside,
ViewerCellId: clipRoot?.CellId ?? 0u,
ViewerEyePosition: camera.Position,
PlayerViewPosition: camera.Position,
ViewerRoot: clipRoot,
CameraInsideCell: clipRoot is not null,
RootSeenOutside: true,
PlayerInsideCell: false,
PlayerLandblockId: null,
RenderCenterLandblockX: 0,
RenderCenterLandblockY: 0,
PlayerCellId: 0x01010002u,
PlayerIndoorGate: false);
return new WorldRenderFrame(
cameraFrame,
roots,
new WorldBuildingFrame(null, Array.Empty<LoadedCell>()),
[]);
}
private static int RequiredCallIndex(
IReadOnlyList<CompiledCall> calls,
Type declaringType,
string methodName)
{
int index = CompiledCallGraph.IndexOf(calls, declaringType, methodName);
Assert.True(index >= 0, $"Missing compiled call: {declaringType.Name}.{methodName}");
return index;
}
}