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:
parent
85239fb373
commit
28e1cf8029
25 changed files with 2679 additions and 844 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
|
|
@ -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(");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; } = [];
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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\"",
|
||||
|
|
|
|||
744
tests/AcDream.App.Tests/Rendering/WorldSceneRendererTests.cs
Normal file
744
tests/AcDream.App.Tests/Rendering/WorldSceneRendererTests.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue