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>
744 lines
25 KiB
C#
744 lines
25 KiB
C#
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.");
|
|
}
|
|
}
|