refactor(render): extract world scene frame owner

Move fallback and PView world drawing, shared alpha, particles, visibility, selection, and diagnostics behind focused frame owners. Make exceptional frames failure-atomic by restoring the shared GL baseline and preserving primary alpha failures through cleanup.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-22 07:22:09 +02:00
parent 85239fb373
commit 28e1cf8029
25 changed files with 2679 additions and 844 deletions

View file

@ -29,7 +29,7 @@ public sealed class GameWindowRenderLeafCompositionTests
"_renderFrameResources!.Prepare(frameInput);",
"_devToolsFramePresenter?.BeginFrame((float)deltaSeconds);",
"_renderWeatherFrame!.Tick(deltaSeconds);",
"_retailSelectionScene?.BeginFrame();");
"_worldSceneRenderer!.Render(frameInput);");
Assert.DoesNotContain("_wbDrawDispatcher?.BeginFrame(", source);
Assert.DoesNotContain("_wbMeshAdapter?.Tick();", source);
Assert.DoesNotContain("_particleRenderer?.BeginFrame(", source);
@ -126,12 +126,10 @@ public sealed class GameWindowRenderLeafCompositionTests
{
string source = GameWindowSource();
Assert.Contains("bool normalWorldDrawn = false;", source);
AssertAppearsInOrder(
source,
"if (IsLiveModeWaitingForLogin)",
"goto SkipWorldGeometry;",
"normalWorldDrawn = true;",
"_worldSceneRenderer!.Render(frameInput);",
"worldOutcome.NormalWorldDrawn;",
"bool screenshotCaptured = _frameScreenshots?.CapturePending() == true;",
"normalWorldDrawn),",
"screenshotCaptured)));");
@ -158,8 +156,9 @@ public sealed class GameWindowRenderLeafCompositionTests
Assert.Contains(
"new AcDream.App.Rendering.TerrainDrawDiagnosticsController(",
source);
Assert.Contains("_terrainDrawDiagnostics!.Begin();", source);
Assert.Contains("_terrainDrawDiagnostics.Complete();", source);
Assert.Contains("new AcDream.App.Rendering.WorldScenePassExecutor(", source);
Assert.DoesNotContain("_terrainDrawDiagnostics!.Begin();", source);
Assert.DoesNotContain("_terrainDrawDiagnostics.Complete();", source);
}
private static string GameWindowSource() => File.ReadAllText(Path.Combine(

View file

@ -0,0 +1,106 @@
using AcDream.App.Rendering;
using Silk.NET.OpenGL;
namespace AcDream.App.Tests.Rendering;
public sealed class RenderFrameGlStateControllerTests
{
[Fact]
public void RestoreFrameDefaults_ReestablishesTheCompleteFrameGlobalContract()
{
var api = new RecordingApi();
var state = new RenderFrameGlStateController(api);
state.RestoreFrameDefaults();
Assert.Equal(
[
"cap:ScissorTest:False",
"cap:StencilTest:False",
"cap:Blend:False",
"cap:SampleAlphaToMaskSgis:False",
"cap:ClipDistance0:False",
"cap:ClipDistance1:False",
"cap:ClipDistance2:False",
"cap:ClipDistance3:False",
"cap:ClipDistance4:False",
"cap:ClipDistance5:False",
"cap:ClipDistance6:False",
"cap:ClipDistance7:False",
"color-mask:True:True:True:True",
"stencil-mask:255",
"depth-mask:True",
"depth-func:Less",
"cap:DepthTest:True",
"cap:CullFace:False",
"cull:Back",
"front:CW",
"program:0",
"vao:0",
"buffer:ArrayBuffer:0",
],
api.Calls);
}
[Fact]
public void PortalDepthMask_SuccessExitRestoresTheSameCullOffConvention()
{
string source = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Rendering",
"PortalDepthMaskRenderer.cs"));
int restore = source.IndexOf(
"// ---- restore the frame-global convention ----",
StringComparison.Ordinal);
Assert.True(restore >= 0, "Missing portal-depth state restore block.");
string restoreBlock = source[restore..];
Assert.Contains("_gl.Disable(EnableCap.CullFace);", restoreBlock);
Assert.DoesNotContain("_gl.Enable(EnableCap.CullFace);", restoreBlock);
}
private static string FindRepoRoot()
{
DirectoryInfo? directory = new(AppContext.BaseDirectory);
while (directory is not null)
{
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
return directory.FullName;
directory = directory.Parent;
}
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
}
private sealed class RecordingApi : IRenderFrameGlStateApi
{
public List<string> Calls { get; } = [];
public void SetCapability(EnableCap capability, bool enabled) =>
Calls.Add($"cap:{capability}:{enabled}");
public void ColorMask(bool red, bool green, bool blue, bool alpha) =>
Calls.Add($"color-mask:{red}:{green}:{blue}:{alpha}");
public void StencilMask(uint mask) => Calls.Add($"stencil-mask:{mask}");
public void DepthMask(bool enabled) => Calls.Add($"depth-mask:{enabled}");
public void DepthFunc(DepthFunction function) =>
Calls.Add($"depth-func:{function}");
public void CullFace(TriangleFace face) => Calls.Add($"cull:{face}");
public void FrontFace(FrontFaceDirection direction) =>
Calls.Add($"front:{direction}");
public void UseProgram(uint program) => Calls.Add($"program:{program}");
public void BindVertexArray(uint vertexArray) => Calls.Add($"vao:{vertexArray}");
public void BindBuffer(BufferTargetARB target, uint buffer) =>
Calls.Add($"buffer:{target}:{buffer}");
}
}

View file

@ -87,10 +87,8 @@ public sealed class RenderFrameResourceControllerTests
"_particleVisibility.Reset();",
"SkyKeyframe sky = _worldTime.CurrentSky;",
"AtmosphereSnapshot atmosphere = _weather.Snapshot(in sky);",
"_gl.DepthMask(true);",
"_frameGlState.RestoreFrameDefaults();",
"_gl.Clear(",
"_gl.CullFace(TriangleFace.Back);",
"_gl.FrontFace(FrontFaceDirection.CW);",
"_diagnostics.EmitGlStateTripwireIfChanged(");
}

View file

@ -179,6 +179,76 @@ public sealed class RetailAlphaQueueTests
log);
}
[Fact]
public void AbortFrame_DiscardsPayloadAndAllowsTheNextFrameToRender()
{
var log = new List<string>();
var source = new RecordingSource("alpha", log);
var queue = new RetailAlphaQueue();
queue.BeginFrame();
queue.Submit(source, 1, 12f);
queue.AbortFrame();
Assert.False(queue.IsCollecting);
Assert.Equal(0, queue.PendingCount);
Assert.Empty(log);
Assert.Equal(1, source.ResetCount);
queue.BeginFrame();
queue.Submit(source, 2, 12f);
queue.EndFrame();
Assert.Equal(["alpha:2"], log);
Assert.Equal(2, source.ResetCount);
}
[Fact]
public void EndFrame_DrawAndResetFailuresPreserveThePrimaryFailureAndClearTheFrame()
{
var drawSource = new FailureSource("draw failed", "first reset failed");
var secondSource = new FailureSource(null, null);
var queue = new RetailAlphaQueue();
queue.BeginFrame();
queue.Submit(drawSource, 1, 20f);
queue.Submit(secondSource, 2, 10f);
AggregateException failure = Assert.Throws<AggregateException>(queue.EndFrame);
Assert.Collection(
failure.InnerExceptions,
error => Assert.Equal("draw failed", error.Message),
error => Assert.Equal("first reset failed", error.Message));
Assert.Equal(1, drawSource.ResetCount);
Assert.Equal(1, secondSource.ResetCount);
Assert.Equal(0, queue.PendingCount);
Assert.False(queue.IsCollecting);
}
[Fact]
public void EndFrame_MultipleResetFailuresAttemptEverySourceAndClearTheFrame()
{
var first = new FailureSource(null, "first reset failed");
var second = new FailureSource(null, "second reset failed");
var queue = new RetailAlphaQueue();
queue.BeginFrame();
queue.Submit(first, 1, 20f);
queue.Submit(second, 2, 10f);
AggregateException failure = Assert.Throws<AggregateException>(queue.EndFrame);
Assert.Collection(
failure.InnerExceptions,
error => Assert.Equal("first reset failed", error.Message),
error => Assert.Equal("second reset failed", error.Message));
Assert.Equal(1, first.ResetCount);
Assert.Equal(1, second.ResetCount);
Assert.Equal(0, queue.PendingCount);
Assert.False(queue.IsCollecting);
}
[Fact]
public void ComputeViewerDistance_UsesTransformedGfxSortCenter()
{
@ -224,4 +294,28 @@ public sealed class RetailAlphaQueueTests
public void ResetAlphaSubmissions() => ResetCount++;
}
private sealed class FailureSource(
string? drawFailure,
string? resetFailure) : IRetailAlphaDrawSource
{
public int ResetCount { get; private set; }
public void PrepareAlphaDraws(ReadOnlySpan<int> tokens)
{
}
public void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount)
{
if (drawFailure is not null)
throw new InvalidOperationException(drawFailure);
}
public void ResetAlphaSubmissions()
{
ResetCount++;
if (resetFailure is not null)
throw new InvalidOperationException(resetFailure);
}
}
}

View file

@ -229,7 +229,8 @@ public sealed class RetailPViewPassExecutorTests
"GameWindow.cs"));
Assert.Contains("new AcDream.App.Rendering.RetailPViewPassExecutor(", source);
Assert.Contains("new AcDream.App.Rendering.RetailPViewFrameInput", source);
Assert.Contains("new AcDream.App.Rendering.WorldScenePViewRenderer(", source);
Assert.DoesNotContain("new AcDream.App.Rendering.RetailPViewFrameInput", source);
Assert.DoesNotContain("RetailPViewDrawContext", source);
Assert.DoesNotContain("DrawLandscapeSlice =", source);
Assert.DoesNotContain("DrawExitPortalMasks =", source);
@ -399,15 +400,15 @@ public sealed class RetailPViewPassExecutorTests
uint id,
uint serverGuid = 0,
uint? parentCellId = null) => new()
{
Id = id,
ServerGuid = serverGuid,
SourceGfxObjOrSetupId = 0,
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
MeshRefs = [new MeshRef(1u, Matrix4x4.Identity)],
ParentCellId = parentCellId,
};
{
Id = id,
ServerGuid = serverGuid,
SourceGfxObjOrSetupId = 0,
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
MeshRefs = [new MeshRef(1u, Matrix4x4.Identity)],
ParentCellId = parentCellId,
};
private static string MethodBody(string source, string signature)
{
@ -475,6 +476,8 @@ public sealed class RetailPViewPassExecutorTests
private sealed class RecordingExecutor : IRetailPViewPassExecutor, IDisposable
{
public void AbortFrame() => Operations.Add("abort");
private readonly ClipFrame _clipFrame = ClipFrame.NoClip();
public List<string> Operations { get; } = [];

View file

@ -45,6 +45,29 @@ public sealed class RetailSelectionSceneTests
Assert.False(scene.TryGetLighting(entity.ServerGuid, entity.Id, out _));
}
[Fact]
public void AbortFrame_DiscardsBuildingSelectionAndPreservesPublishedFrame()
{
var scene = CreateScene();
var published = Entity(localId: 46u, serverGuid: 0x5000_0046u);
Publish(scene, published);
var rejected = Entity(localId: 47u, serverGuid: 0x5000_0047u);
scene.BeginFrame();
(Matrix4x4 view, Matrix4x4 projection) = Camera();
scene.SetViewFrustum(FrustumPlanes.FromViewProjection(view * projection));
scene.AddVisiblePart(
rejected,
partIndex: 0,
gfxObjId: 0x0100_0001u,
Matrix4x4.CreateTranslation(0f, 0f, -5f));
scene.AbortFrame();
RetailSelectionHit? hit = PickCenter(scene);
Assert.NotNull(hit);
Assert.Equal(published.Id, hit.Value.LocalEntityId);
}
private static RetailSelectionScene CreateScene(
RetailSelectionLightingPulse? pulse = null)
{

View file

@ -64,4 +64,35 @@ public sealed class ParticleVisibilityControllerTests
controller.Apply(particles, 1f);
Assert.False(Assert.Single(particles.EnumerateEmitters()).ViewEligible);
}
[Fact]
public void AbortedFrame_PreservesTheLastCompletedWorldView()
{
var particles = new ParticleSystem(new EmitterDescRegistry(), new Random(1));
int handle = particles.SpawnEmitter(
new EmitterDesc
{
DatId = 0x32000003u,
Type = ParticleType.Still,
MaxDegradeDistance = 100f,
MaxParticles = 1,
},
Vector3.Zero,
attachedObjectId: 42u);
particles.UpdateEmitterOwnerCell(handle, 0x01010001u);
var controller = new ParticleVisibilityController();
controller.BeginFrame(Vector3.Zero);
controller.UseWorldView();
controller.MarkVisibleCells([0x01010001u]);
controller.CompleteFrame();
controller.BeginFrame(new Vector3(1000f, 1000f, 0f));
controller.UseWorldView();
controller.MarkVisibleCells([0x02020001u]);
controller.AbortFrame();
controller.Apply(particles, 1f);
Assert.True(Assert.Single(particles.EnumerateEmitters()).ViewEligible);
}
}

View file

@ -325,25 +325,32 @@ public sealed class WorldRenderFrameBuilderTests
}
[Fact]
public void Game_window_uses_the_typed_builder_and_releases_it_before_render_owners()
public void World_scene_uses_the_typed_builder_and_game_window_releases_it_before_render_owners()
{
string source = GameWindowSource();
string gameWindow = GameWindowSource();
string worldScene = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Rendering",
"WorldSceneRenderer.cs"));
Assert.Equal(
1,
CountOccurrences(source, "_worldRenderFrameBuilder!.Build("));
Assert.DoesNotContain("private void UpdateSunFromSky(", source, StringComparison.Ordinal);
Assert.DoesNotContain("private void UpdateSkyPes(", source, StringComparison.Ordinal);
Assert.DoesNotContain("private static float ParseEnvFloat(", source, StringComparison.Ordinal);
CountOccurrences(worldScene, "_frames.Build("));
Assert.DoesNotContain("private void UpdateSunFromSky(", gameWindow, StringComparison.Ordinal);
Assert.DoesNotContain("private void UpdateSkyPes(", gameWindow, StringComparison.Ordinal);
Assert.DoesNotContain("private static float ParseEnvFloat(", gameWindow, StringComparison.Ordinal);
AssertAppearsInOrder(
source,
"if (_cameraController is not null && !portalViewportVisible)",
"_retailAlphaQueue.BeginFrame();",
"_worldRenderFrameBuilder!.Build(",
"if (IsLiveModeWaitingForLogin)",
"normalWorldDrawn = true;");
worldScene,
"_alpha.BeginFrame();",
"_frames.Build(",
"if (_login.IsWaitingForLogin)",
"_passes.DrawFlatTerrain(",
"NormalWorldDrawn: true");
AssertAppearsInOrder(
source,
gameWindow,
"_worldSceneRenderer = null;",
"_worldRenderFrameBuilder = null;",
"_skyPesFrame = null;",
"new(\"equipped children\"",

View file

@ -0,0 +1,744 @@
using System.Numerics;
using System.Reflection;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Selection;
using AcDream.App.Rendering.Vfx;
using AcDream.Core.Rendering;
using AcDream.Core.World;
namespace AcDream.App.Tests.Rendering;
public sealed class WorldSceneRendererTests
{
[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 LoginWait_DrawsFlatSkyThenCompletesFrameWithoutWorldGeometry()
{
var rig = new Rig(portalVisible: false, waitingForLogin: true, clipRoot: null);
WorldRenderFrameOutcome result = rig.Renderer.Render(default);
Assert.False(result.NormalWorldDrawn);
Assert.Equal(0, result.VisibleLandblocks);
Assert.Equal(0, result.TotalLandblocks);
Assert.Equal(
[
"selection:begin",
"alpha:begin",
"frame:build",
"passes:begin",
"flat:clip",
"flat:sky",
"alpha:end",
"visibility:mark",
"visibility:complete",
"selection:complete",
],
rig.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 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+unattached", 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()
{
string source = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Rendering",
"GameWindow.cs"));
Assert.Contains("new AcDream.App.Rendering.WorldSceneRenderer(", source);
Assert.Equal(1, CountOccurrences(source, "_worldSceneRenderer!.Render(frameInput);"));
Assert.DoesNotContain("_retailPViewRenderer.DrawInside(", source);
Assert.DoesNotContain("_retailAlphaQueue.BeginFrame();", source);
Assert.DoesNotContain("_terrainDrawDiagnostics!.Begin();", source);
Assert.DoesNotContain("SkipWorldGeometry:", source);
}
[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)
{
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);
var selection = new SelectionFrame(Calls);
var alpha = new AlphaFrame(Calls);
var visibility = new ParticleVisibility(Calls);
PView = new PViewRenderer(Calls);
Passes = new PassExecutor(Calls);
var diagnostics = new Diagnostics(Calls);
Renderer = new WorldSceneRenderer(
foundation,
login,
sky,
Frames,
new EntitySource(),
selection,
alpha,
visibility,
PView,
new PViewCells(),
Passes,
new WorldRenderRangeState(4, 12),
diagnostics);
}
public List<string> Calls { get; }
public RenderFrameFoundation Foundation { get; }
public DayGroupData DayGroup { get; }
public float DayFraction { get; }
public FrameBuilder Frames { 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 void ObserveDrawableCells(IReadOnlySet<uint> drawableCells) =>
calls.Add("frame:observe-cells");
public void ClearDrawableCells() => calls.Add("frame:clear-cells");
}
private sealed class EntitySource : IWorldSceneEntitySource
{
public IEnumerable<(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 IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> LandblockBounds =>
Array.Empty<(uint, Vector3, Vector3)>();
}
private sealed class SelectionFrame(List<string> calls) : IWorldSceneSelectionFrame
{
public void BeginFrame() => 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 void MarkVisibleCells(HashSet<uint> cellIds) =>
calls.Add("visibility:mark");
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;
_interiorResult = new RetailPViewFrameResult().Reset(
new PortalVisibilityFrame(),
new ClipFrameAssembly(),
[],
new InteriorEntityPartition.Result());
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 == "none"
? "unattached"
: currentSignature + "+unattached",
_ => 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,
IEnumerable<(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 CountOccurrences(string source, string value)
{
int count = 0;
int cursor = 0;
while ((cursor = source.IndexOf(value, cursor, StringComparison.Ordinal)) >= 0)
{
count++;
cursor += value.Length;
}
return count;
}
private static string FindRepoRoot()
{
DirectoryInfo? directory = new(AppContext.BaseDirectory);
while (directory is not null)
{
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
return directory.FullName;
directory = directory.Parent;
}
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
}
}