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
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