test: replace render leaf source freezes
This commit is contained in:
parent
3c492aedc2
commit
5e56045077
3 changed files with 378 additions and 214 deletions
|
|
@ -1301,3 +1301,43 @@ Verification:
|
|||
attributed methods, and reduces direct/total source readers from 44/70 to
|
||||
41/64. The remaining 64 reconcile to the 22 approved retained
|
||||
policies/contracts and 42 staged replacements.
|
||||
|
||||
## Batch AB render-leaf and frame-handoff source-freeze replacement
|
||||
|
||||
Batch AB converts all nine approved `GameWindowRenderLeafCompositionTests`
|
||||
source readers. No product source changes and no test is removed.
|
||||
|
||||
Private presentation, resource preparation, world/private/diagnostic routing,
|
||||
GPU measurement, and immutable outcome construction now follow compiled typed
|
||||
calls in their production order. The portal-tunnel check follows the compiled
|
||||
session transfer/bind/publication path, both local teleport factory targets,
|
||||
the session-before-frame phase references in `OnLoad`, and the exact frame-root
|
||||
borrower construction order. `OnRender` now proves its single
|
||||
`GameFrameGraphSlot.Render` handoff and absence of the five displaced leaf
|
||||
field reads directly from the built method.
|
||||
|
||||
The broad legacy-owner blacklist is narrowed to what it actually protects:
|
||||
reflected `GameWindow` fields and methods reject the displaced owners, while
|
||||
compiled composition metadata positively locates paperdoll, frame-resource,
|
||||
weather, private-presentation, orchestrator, frame-pacing, and mouse-capture
|
||||
ownership behind their focused types. Shutdown ordering uses the compiled
|
||||
operation/stage labels. Terrain diagnostics are constructed with the scene
|
||||
executor but not driven directly by the frame root.
|
||||
|
||||
The optional paperdoll contract is stronger than the old source-expression
|
||||
match: compiled branch destinations prove that either a missing viewport or a
|
||||
missing inventory frame jumps past `PaperdollFramePresenter` construction.
|
||||
`CompiledCallGraph` gained a reusable branch-target reader for this kind of
|
||||
optional-dependency guard.
|
||||
|
||||
Verification:
|
||||
|
||||
- all nine focused render-leaf methods pass;
|
||||
- the complete locked Release build covers all 44 projects with zero warnings
|
||||
and zero errors;
|
||||
- the no-retry complete hermetic Release gate remains 14,346/14,346 with zero
|
||||
skips or failures across all 12 test assemblies; and
|
||||
- the regenerated 1,254-file inventory parses every file, remains at 11,414
|
||||
attributed methods, and reduces direct/total source readers from 41/64 to
|
||||
39/55. The remaining 55 reconcile to the 22 approved retained
|
||||
policies/contracts and 33 staged replacements.
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ internal readonly record struct CompiledFieldReference(
|
|||
int Offset,
|
||||
OpCode OpCode,
|
||||
FieldInfo Field);
|
||||
internal readonly record struct CompiledBranch(
|
||||
int Offset,
|
||||
OpCode OpCode,
|
||||
int TargetOffset);
|
||||
|
||||
/// <summary>
|
||||
/// Reads compiled call/new-object edges from a method body. Architecture tests
|
||||
|
|
@ -146,6 +150,40 @@ internal static class CompiledCallGraph
|
|||
return references;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads short and long branch destinations. Tests use this to prove that
|
||||
/// an optional dependency guard jumps around a construction edge without
|
||||
/// freezing the source expression that produced the branch.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<CompiledBranch> ReadBranches(MethodBase method)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(method);
|
||||
byte[] il = method.GetMethodBody()?.GetILAsByteArray()
|
||||
?? throw new InvalidOperationException(
|
||||
$"{method.DeclaringType?.FullName}.{method.Name} has no compiled body.");
|
||||
var branches = new List<CompiledBranch>();
|
||||
|
||||
for (int cursor = 0; cursor < il.Length;)
|
||||
{
|
||||
int instructionOffset = cursor;
|
||||
OpCode opCode = ReadOpCode(il, ref cursor);
|
||||
if (opCode.OperandType == OperandType.ShortInlineBrTarget)
|
||||
{
|
||||
int target = cursor + sizeof(sbyte) + unchecked((sbyte)il[cursor]);
|
||||
branches.Add(new CompiledBranch(instructionOffset, opCode, target));
|
||||
}
|
||||
else if (opCode.OperandType == OperandType.InlineBrTarget)
|
||||
{
|
||||
int target = cursor + sizeof(int) + BitConverter.ToInt32(il, cursor);
|
||||
branches.Add(new CompiledBranch(instructionOffset, opCode, target));
|
||||
}
|
||||
|
||||
cursor += OperandSize(opCode.OperandType, il, cursor);
|
||||
}
|
||||
|
||||
return branches;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads types named by compiled type operands such as casts, boxing, and
|
||||
/// <c>isinst</c>. This lets tests retain an exact type boundary without
|
||||
|
|
|
|||
|
|
@ -1,303 +1,389 @@
|
|||
using System.Reflection;
|
||||
using AcDream.App.Composition;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.Tests.Architecture;
|
||||
using AcDream.App.UI;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
||||
public sealed class GameWindowRenderLeafCompositionTests
|
||||
{
|
||||
private const BindingFlags Declared = BindingFlags.Instance
|
||||
| BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic
|
||||
| BindingFlags.DeclaredOnly;
|
||||
|
||||
[Fact]
|
||||
public void ProductionRender_PreservesPrivatePresentationAndCaptureOrder()
|
||||
{
|
||||
string presentation = Source("PrivatePresentationRenderer.cs");
|
||||
string orchestrator = Source("RenderFrameOrchestrator.cs");
|
||||
|
||||
AssertAppearsInOrder(
|
||||
MethodInfo presentation = RequiredMethod(
|
||||
typeof(PrivatePresentationRenderer),
|
||||
nameof(PrivatePresentationRenderer.Render));
|
||||
AssertCallOrder(
|
||||
presentation,
|
||||
"_portal.Draw(",
|
||||
"_entityViewports?.Render();",
|
||||
"_gameplayUi?.Render(",
|
||||
"_devTools?.Render(",
|
||||
"_screenshots?.CapturePending(");
|
||||
AssertAppearsInOrder(
|
||||
(typeof(IPrivatePortalViewport), nameof(IPrivatePortalViewport.Draw)),
|
||||
(typeof(IPrivateEntityViewportFrame), nameof(IPrivateEntityViewportFrame.Render)),
|
||||
(typeof(IRetainedGameplayUiFrame), nameof(IRetainedGameplayUiFrame.Render)),
|
||||
(typeof(IDevToolsFrameLifecycle), nameof(IDevToolsFrameLifecycle.Render)),
|
||||
(typeof(IPrivateFrameScreenshot), nameof(IPrivateFrameScreenshot.CapturePending)));
|
||||
|
||||
MethodInfo orchestrator = RequiredMethod(
|
||||
typeof(RenderFrameOrchestrator),
|
||||
nameof(RenderFrameOrchestrator.Render));
|
||||
AssertCallOrder(
|
||||
orchestrator,
|
||||
"_world.Render(input);",
|
||||
"_presentation.Render(input, world);",
|
||||
"_diagnostics.Publish(input, outcome);");
|
||||
(typeof(IWorldSceneFramePhase), nameof(IWorldSceneFramePhase.Render)),
|
||||
(typeof(IPrivatePresentationFramePhase), nameof(IPrivatePresentationFramePhase.Render)),
|
||||
(typeof(IRenderFrameDiagnosticsPhase), nameof(IRenderFrameDiagnosticsPhase.Publish)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProductionRender_Prepares_resources_then_devtools_weather_and_world()
|
||||
{
|
||||
string source = Source("RenderFramePreparationController.cs");
|
||||
|
||||
AssertAppearsInOrder(
|
||||
source,
|
||||
"_resources.Prepare(input);",
|
||||
"_devTools?.BeginFrame((float)input.DeltaSeconds);",
|
||||
"_weather.Tick(input.DeltaSeconds);");
|
||||
MethodInfo prepare = RequiredMethod(
|
||||
typeof(RenderFramePreparationController),
|
||||
nameof(RenderFramePreparationController.Prepare));
|
||||
AssertCallOrder(
|
||||
prepare,
|
||||
(typeof(IRenderFrameResourcePhase), nameof(IRenderFrameResourcePhase.Prepare)),
|
||||
(typeof(IDevToolsFrameLifecycle), nameof(IDevToolsFrameLifecycle.BeginFrame)),
|
||||
(typeof(IRenderWeatherFramePhase), nameof(IRenderWeatherFramePhase.Tick)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Composition_transfers_portal_tunnel_before_constructing_frame_borrowers()
|
||||
{
|
||||
string phase = File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Composition",
|
||||
"SessionPlayerComposition.cs"));
|
||||
string source = GameWindowSource();
|
||||
string framePhase = FrameRootSource();
|
||||
MethodInfo complete = RequiredMethod(
|
||||
typeof(SessionPlayerCompositionPhase),
|
||||
"CompleteSessionPlayer");
|
||||
AssertCallOrder(
|
||||
complete,
|
||||
(typeof(TransferableResourceSlot<>), "Transfer"),
|
||||
(typeof(DeferredLocalPlayerTeleportNetworkSink), "BindOwned"),
|
||||
(typeof(IGameWindowSessionPlayerPublication), "PublishSessionPlayer"));
|
||||
|
||||
AssertAppearsInOrder(
|
||||
phase,
|
||||
"d.PortalTunnelFallback.Transfer(",
|
||||
"new LocalPlayerTeleportController(",
|
||||
"new LocalPlayerTeleportPresentation(portalTunnel)",
|
||||
"d.TeleportSink.BindOwned(localTeleport)",
|
||||
"_publication.PublishSessionPlayer(result);");
|
||||
AssertAppearsInOrder(
|
||||
source,
|
||||
"new SessionPlayerCompositionPhase(",
|
||||
"new FrameRootCompositionPhase(");
|
||||
AssertAppearsInOrder(
|
||||
framePhase,
|
||||
"new LocalPlayerTeleportRenderStateSource(",
|
||||
"new RenderFrameResourceController(",
|
||||
"new PrivatePresentationRenderer(",
|
||||
"new RenderFrameOrchestrator(");
|
||||
MethodBase tunnelFactory = ReferencedMethodConstructing(
|
||||
complete,
|
||||
typeof(LocalPlayerTeleportPresentation));
|
||||
Assert.NotNull(ReferencedMethodConstructing(
|
||||
tunnelFactory,
|
||||
typeof(LocalPlayerTeleportController)));
|
||||
|
||||
MethodInfo onLoad = RequiredMethod(typeof(GameWindow), "OnLoad");
|
||||
IReadOnlyList<CompiledCall> loadReferences =
|
||||
CompiledCallGraph.ReadMethodReferences(onLoad);
|
||||
CompiledCall sessionPhase = Assert.Single(
|
||||
loadReferences,
|
||||
reference => Constructs(
|
||||
reference.Target,
|
||||
typeof(SessionPlayerCompositionPhase)));
|
||||
CompiledCall framePhase = Assert.Single(
|
||||
loadReferences,
|
||||
reference => Constructs(
|
||||
reference.Target,
|
||||
typeof(FrameRootCompositionPhase)));
|
||||
Assert.True(sessionPhase.Offset < framePhase.Offset);
|
||||
|
||||
MethodInfo composeFrame = RequiredMethod(
|
||||
typeof(FrameRootCompositionPhase),
|
||||
"ComposeCore");
|
||||
AssertCallOrder(
|
||||
composeFrame,
|
||||
(typeof(LocalPlayerTeleportRenderStateSource), ".ctor"),
|
||||
(typeof(RenderFrameResourceController), ".ctor"),
|
||||
(typeof(PrivatePresentationRenderer), ".ctor"),
|
||||
(typeof(RenderFrameOrchestrator), ".ctor"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProductionComposition_RemovesLegacyLeafOwnership()
|
||||
{
|
||||
string source = GameWindowSource();
|
||||
|
||||
string[] removed =
|
||||
FieldInfo[] windowFields = typeof(GameWindow).GetFields(Declared);
|
||||
HashSet<string> removedFieldNames =
|
||||
[
|
||||
"_imguiBootstrap",
|
||||
"_panelHost",
|
||||
"_paperdollDollDirty",
|
||||
"_lastRenderSignature",
|
||||
"_lastVisibleLandblocks",
|
||||
"_perfAccum",
|
||||
"_entityUploadTiming",
|
||||
"_weatherAccum",
|
||||
];
|
||||
HashSet<string> removedFieldTypes =
|
||||
[
|
||||
nameof(WorldRenderDiagnostics),
|
||||
nameof(WorldRenderFrameBuilder),
|
||||
nameof(SkyPesFrameController),
|
||||
nameof(RetailPViewRenderer),
|
||||
nameof(RetailPViewPassExecutor),
|
||||
nameof(RetailPViewCellSource),
|
||||
nameof(TerrainDrawDiagnosticsController),
|
||||
];
|
||||
Assert.DoesNotContain(
|
||||
windowFields,
|
||||
field => removedFieldNames.Contains(field.Name)
|
||||
|| removedFieldTypes.Contains(field.FieldType.Name));
|
||||
|
||||
HashSet<string> removedMethods =
|
||||
[
|
||||
"RefreshPaperdollDoll",
|
||||
"ApplyPaperdollPose",
|
||||
"ResolvePaperdollPoseDid",
|
||||
"EnumerateDebugPanel",
|
||||
"ResetPanelLayout",
|
||||
"SetPanelLayout",
|
||||
"_lastRenderSignature",
|
||||
"private void EmitRenderSignatureIfChanged(",
|
||||
"private void EmitRetailPViewDiagnostics(",
|
||||
"EmitGlStateTripwireIfChanged();",
|
||||
"EmitClipRouteScissorProbe(scissor",
|
||||
"_lastVisibleLandblocks",
|
||||
"_perfAccum",
|
||||
"_entityUploadTiming",
|
||||
"_weatherAccum",
|
||||
"EmitRenderSignatureIfChanged",
|
||||
"EmitRetailPViewDiagnostics",
|
||||
"EmitGlStateTripwireIfChanged",
|
||||
"EmitClipRouteScissorProbe",
|
||||
"TryGetLoginWorldCell",
|
||||
"ApplyFramePacingPreference",
|
||||
"RefreshActiveMonitorFramePacing",
|
||||
"private void OnFrameRendered(",
|
||||
"private AcDream.App.Rendering.WorldRenderDiagnostics? _worldRenderDiagnostics",
|
||||
"private AcDream.App.Rendering.WorldRenderFrameBuilder?",
|
||||
"private AcDream.App.Rendering.SkyPesFrameController?",
|
||||
"private AcDream.App.Rendering.RetailPViewRenderer?",
|
||||
"private AcDream.App.Rendering.RetailPViewPassExecutor?",
|
||||
"private AcDream.App.Rendering.RetailPViewCellSource?",
|
||||
"private AcDream.App.Rendering.TerrainDrawDiagnosticsController?",
|
||||
"OnFrameRendered",
|
||||
];
|
||||
foreach (string identifier in removed)
|
||||
Assert.DoesNotContain(identifier, source, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
typeof(GameWindow).GetMethods(Declared),
|
||||
method => removedMethods.Contains(method.Name));
|
||||
|
||||
Assert.Contains("new PaperdollFramePresenter(", LivePresentationSource());
|
||||
string framePhase = FrameRootSource();
|
||||
Assert.Contains("new RenderFrameResourceController(", framePhase);
|
||||
Assert.Contains("new RenderWeatherFrameController(", framePhase);
|
||||
Assert.Contains("new PrivatePresentationRenderer(", framePhase);
|
||||
Assert.Contains("new RenderFrameOrchestrator(", framePhase);
|
||||
Assert.Contains("new DisplayFramePacingController(", source);
|
||||
string pointerSource = File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Input",
|
||||
"CameraPointerInputController.cs"));
|
||||
Assert.Contains("_capture.WantCaptureMouse", pointerSource);
|
||||
Assert.Contains(
|
||||
CompiledCallGraph.ReadDeclared(typeof(LivePresentationCompositionPhase)),
|
||||
call => call.Target.DeclaringType == typeof(PaperdollFramePresenter)
|
||||
&& call.Target.IsConstructor);
|
||||
IReadOnlyList<CompiledCall> frameCalls =
|
||||
CompiledCallGraph.ReadDeclared(typeof(FrameRootCompositionPhase));
|
||||
Assert.All(
|
||||
new[]
|
||||
{
|
||||
typeof(RenderFrameResourceController),
|
||||
typeof(RenderWeatherFrameController),
|
||||
typeof(PrivatePresentationRenderer),
|
||||
typeof(RenderFrameOrchestrator),
|
||||
},
|
||||
type => Assert.Contains(
|
||||
frameCalls,
|
||||
call => call.Target.DeclaringType == type
|
||||
&& call.Target.IsConstructor));
|
||||
Assert.Contains(
|
||||
CompiledCallGraph.ReadDeclared(typeof(GameWindow)),
|
||||
call => call.Target.DeclaringType == typeof(DisplayFramePacingController)
|
||||
&& call.Target.IsConstructor);
|
||||
Assert.Contains(
|
||||
CompiledCallGraph.ReadDeclared(typeof(CameraPointerInputController)),
|
||||
call => call.Target.DeclaringType == typeof(IInputCaptureSource)
|
||||
&& call.Target.Name == "get_WantCaptureMouse");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Shutdown_DrainsGpuBeforeFrontendsAndPreservesFrameBorrowerOrder()
|
||||
{
|
||||
// Campaign V slice V11 removed the ImGui developer-tools frontend and
|
||||
// its "developer tools" shutdown stage entry along with it; this test
|
||||
// used to pin that entry's position and is now renamed to pin what
|
||||
// survives it.
|
||||
string source = GameWindowLifetimeSource();
|
||||
MethodInfo create = RequiredMethod(
|
||||
typeof(GameWindowShutdownManifest),
|
||||
nameof(GameWindowShutdownManifest.Create));
|
||||
IReadOnlyList<string> labels = CompiledCallGraph.ReadStringLiterals(create);
|
||||
|
||||
AssertAppearsInOrder(
|
||||
source,
|
||||
"new ResourceShutdownStage(\"submitted GPU work\"",
|
||||
"new ResourceShutdownStage(\"render frontends\"",
|
||||
"Hard(\"portal tunnel\"",
|
||||
"Hard(\"paperdoll viewport\"",
|
||||
"new ResourceShutdownStage(\"graphics API context\"");
|
||||
AssertAppearsInOrder(
|
||||
source,
|
||||
"new ResourceShutdownStage(\"frame borrowers\"",
|
||||
"frame.FrameGraphPublication?.Dispose()",
|
||||
"frame.FrameBindings?.Dispose()",
|
||||
"new ResourceShutdownStage(\"session dependents\"");
|
||||
AssertAppearsInOrder(
|
||||
source,
|
||||
"Hard(\"frame pacing\", render.FramePacing.Dispose)",
|
||||
"Hard(\"frame profiler\", render.FrameProfiler.Dispose)");
|
||||
AssertAppearsInOrder(
|
||||
source,
|
||||
"frame.FrameGraphPublication?.Dispose()",
|
||||
"new ResourceShutdownStage(\"render frontends\"",
|
||||
"new ResourceShutdownStage(\"input context\"",
|
||||
"platform.Input?.Dispose()",
|
||||
"new ResourceShutdownStage(\"graphics API context\"");
|
||||
AssertLabelOrder(
|
||||
labels,
|
||||
"submitted GPU work",
|
||||
"render frontends",
|
||||
"portal tunnel",
|
||||
"paperdoll viewport",
|
||||
"graphics API context");
|
||||
AssertLabelOrder(
|
||||
labels,
|
||||
"frame borrowers",
|
||||
"world frame composition",
|
||||
"frame-root bindings",
|
||||
"session dependents");
|
||||
AssertLabelOrder(labels, "frame pacing", "frame profiler");
|
||||
AssertLabelOrder(
|
||||
labels,
|
||||
"world frame composition",
|
||||
"render frontends",
|
||||
"input context",
|
||||
"graphics API context");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProductionOutcome_UsesObservedWorldAndScreenshotFacts()
|
||||
{
|
||||
string presentation = Source("PrivatePresentationRenderer.cs");
|
||||
string orchestrator = Source("RenderFrameOrchestrator.cs");
|
||||
|
||||
AssertAppearsInOrder(
|
||||
MethodInfo presentation = RequiredMethod(
|
||||
typeof(PrivatePresentationRenderer),
|
||||
nameof(PrivatePresentationRenderer.Render));
|
||||
AssertCallOrder(
|
||||
presentation,
|
||||
"_foundation.Foundation.PortalViewportVisible;",
|
||||
"_portal.Draw(",
|
||||
"bool screenshotCaptured = _screenshots?.CapturePending(",
|
||||
"portalViewportVisible,",
|
||||
"screenshotCaptured);");
|
||||
AssertAppearsInOrder(
|
||||
(typeof(RenderFrameFoundation), "get_PortalViewportVisible"),
|
||||
(typeof(IPrivatePortalViewport), nameof(IPrivatePortalViewport.Draw)),
|
||||
(typeof(IPrivateFrameScreenshot), nameof(IPrivateFrameScreenshot.CapturePending)),
|
||||
(typeof(PrivatePresentationFrameOutcome), ".ctor"));
|
||||
|
||||
MethodInfo orchestrator = RequiredMethod(
|
||||
typeof(RenderFrameOrchestrator),
|
||||
nameof(RenderFrameOrchestrator.Render));
|
||||
AssertCallOrder(
|
||||
orchestrator,
|
||||
"_gpuMeasurement.BeginFrame();",
|
||||
"world = _world.Render(input);",
|
||||
"_presentation.Render(input, world);",
|
||||
"_gpuMeasurement.EndFrame();",
|
||||
"new RenderFrameOutcome(world, presentation);",
|
||||
"_diagnostics.Publish(input, outcome);");
|
||||
(typeof(IRenderFrameGpuMeasurement), nameof(IRenderFrameGpuMeasurement.BeginFrame)),
|
||||
(typeof(IWorldSceneFramePhase), nameof(IWorldSceneFramePhase.Render)),
|
||||
(typeof(IPrivatePresentationFramePhase), nameof(IPrivatePresentationFramePhase.Render)),
|
||||
(typeof(IRenderFrameGpuMeasurement), nameof(IRenderFrameGpuMeasurement.EndFrame)),
|
||||
(typeof(RenderFrameOutcome), ".ctor"),
|
||||
(typeof(IRenderFrameDiagnosticsPhase), nameof(IRenderFrameDiagnosticsPhase.Publish)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GameWindow_OnRenderIsOneImmutableOrchestratorHandoff()
|
||||
{
|
||||
string source = GameWindowSource();
|
||||
int start = source.IndexOf(
|
||||
"private void OnRender(double deltaSeconds)",
|
||||
StringComparison.Ordinal);
|
||||
int end = source.IndexOf(
|
||||
"private void OnFramebufferResize(",
|
||||
start,
|
||||
StringComparison.Ordinal);
|
||||
Assert.True(start >= 0 && end > start);
|
||||
string body = source[start..end];
|
||||
MethodInfo onRender = RequiredMethod(typeof(GameWindow), "OnRender");
|
||||
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(onRender);
|
||||
Assert.Single(
|
||||
calls,
|
||||
call => call.Target.DeclaringType == typeof(GameFrameGraphSlot)
|
||||
&& call.Target.Name == nameof(GameFrameGraphSlot.Render));
|
||||
|
||||
Assert.Equal(1, CountOccurrences(body, "_frameGraphs.Render("));
|
||||
Assert.DoesNotContain("_gpuFrameFlights", body);
|
||||
Assert.DoesNotContain("_worldScene", body);
|
||||
Assert.DoesNotContain("_devToolsFramePresenter", body);
|
||||
Assert.DoesNotContain("_retailUiRuntime", body);
|
||||
Assert.DoesNotContain("_frameScreenshots", body);
|
||||
HashSet<string> forbiddenFields =
|
||||
[
|
||||
"_gpuFrameFlights",
|
||||
"_worldScene",
|
||||
"_devToolsFramePresenter",
|
||||
"_retailUiRuntime",
|
||||
"_frameScreenshots",
|
||||
];
|
||||
Assert.DoesNotContain(
|
||||
CompiledCallGraph.ReadFieldReferences(onRender),
|
||||
reference => forbiddenFields.Contains(reference.Field.Name));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PaperdollComposition_SkipsEitherMissingOptionalUiSurface()
|
||||
{
|
||||
string source = LivePresentationSource();
|
||||
MethodBase compose = MethodConstructing(
|
||||
typeof(LivePresentationCompositionPhase),
|
||||
typeof(PaperdollFramePresenter));
|
||||
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(compose);
|
||||
CompiledCall viewport = Assert.Single(
|
||||
calls,
|
||||
call => call.Target.DeclaringType == typeof(RetailUiRuntime)
|
||||
&& call.Target.Name == "get_PaperdollViewportWidget");
|
||||
CompiledCall inventory = Assert.Single(
|
||||
calls,
|
||||
call => call.Target.DeclaringType == typeof(RetailUiRuntime)
|
||||
&& call.Target.Name == "get_InventoryFrame");
|
||||
CompiledCall presenter = Assert.Single(
|
||||
calls,
|
||||
call => call.Target.DeclaringType == typeof(PaperdollFramePresenter)
|
||||
&& call.Target.IsConstructor);
|
||||
Assert.True(viewport.Offset < inventory.Offset);
|
||||
Assert.True(inventory.Offset < presenter.Offset);
|
||||
|
||||
AssertAppearsInOrder(
|
||||
source,
|
||||
"PaperdollViewportWidget is { } viewport",
|
||||
"InventoryFrame is { } inventoryFrame",
|
||||
"new PaperdollFramePresenter(");
|
||||
Assert.DoesNotContain("Paperdoll inventory frame is required.", source);
|
||||
IReadOnlyList<CompiledBranch> branches = CompiledCallGraph.ReadBranches(compose);
|
||||
Assert.Contains(
|
||||
branches,
|
||||
branch => branch.OpCode.FlowControl == System.Reflection.Emit.FlowControl.Cond_Branch
|
||||
&& branch.Offset > viewport.Offset
|
||||
&& branch.Offset < inventory.Offset
|
||||
&& branch.TargetOffset > presenter.Offset);
|
||||
Assert.Contains(
|
||||
branches,
|
||||
branch => branch.OpCode.FlowControl == System.Reflection.Emit.FlowControl.Cond_Branch
|
||||
&& branch.Offset > inventory.Offset
|
||||
&& branch.Offset < presenter.Offset
|
||||
&& branch.TargetOffset > presenter.Offset);
|
||||
Assert.DoesNotContain(
|
||||
CompiledCallGraph.ReadStringLiterals(compose),
|
||||
value => value == "Paperdoll inventory frame is required.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TerrainAndFrameDiagnostics_AreComposedAsOneFocusedOwner()
|
||||
{
|
||||
string source = FrameRootSource();
|
||||
|
||||
Assert.Contains(
|
||||
"new TerrainDrawDiagnosticsController(",
|
||||
source);
|
||||
Assert.Contains("new WorldScenePassExecutor(", source);
|
||||
Assert.DoesNotContain("_terrainDrawDiagnostics!.Begin();", source);
|
||||
Assert.DoesNotContain("_terrainDrawDiagnostics.Complete();", source);
|
||||
MethodInfo compose = RequiredMethod(
|
||||
typeof(FrameRootCompositionPhase),
|
||||
"ComposeCore");
|
||||
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(compose);
|
||||
AssertCallOrder(
|
||||
compose,
|
||||
(typeof(TerrainDrawDiagnosticsController), ".ctor"),
|
||||
(typeof(WorldScenePassExecutor), ".ctor"));
|
||||
Assert.DoesNotContain(
|
||||
calls,
|
||||
call => call.Target.DeclaringType == typeof(TerrainDrawDiagnosticsController)
|
||||
&& (call.Target.Name == nameof(TerrainDrawDiagnosticsController.Begin)
|
||||
|| call.Target.Name == nameof(TerrainDrawDiagnosticsController.Complete)));
|
||||
}
|
||||
|
||||
private static string GameWindowSource() => File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Rendering",
|
||||
"GameWindow.cs"));
|
||||
private static MethodInfo RequiredMethod(Type type, string name) =>
|
||||
type.GetMethod(
|
||||
name,
|
||||
BindingFlags.Instance | BindingFlags.Static
|
||||
| BindingFlags.Public | BindingFlags.NonPublic)
|
||||
?? throw new MissingMethodException(type.FullName, name);
|
||||
|
||||
private static string GameWindowLifetimeSource() => File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Rendering",
|
||||
"GameWindowLifetime.cs"));
|
||||
private static MethodBase MethodConstructing(Type owner, Type constructed) =>
|
||||
Assert.Single(
|
||||
owner.GetMethods(Declared)
|
||||
.Cast<MethodBase>()
|
||||
.Concat(owner.GetConstructors(Declared))
|
||||
.Where(method => method.GetMethodBody() is not null),
|
||||
method => Constructs(method, constructed));
|
||||
|
||||
private static string LivePresentationSource() => File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Composition",
|
||||
"LivePresentationComposition.cs"));
|
||||
private static MethodBase ReferencedMethodConstructing(
|
||||
MethodBase owner,
|
||||
Type constructed) =>
|
||||
Assert.Single(
|
||||
CompiledCallGraph.ReadMethodReferences(owner)
|
||||
.Select(reference => reference.Target)
|
||||
.Where(method => method.GetMethodBody() is not null)
|
||||
.Distinct(),
|
||||
method => Constructs(method, constructed));
|
||||
|
||||
private static string FrameRootSource() => File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Composition",
|
||||
"FrameRootComposition.cs"));
|
||||
private static bool Constructs(MethodBase method, Type type) =>
|
||||
method.GetMethodBody() is not null
|
||||
&& CompiledCallGraph.Read(method).Any(call =>
|
||||
call.Target.DeclaringType == type && call.Target.IsConstructor);
|
||||
|
||||
private static string Source(string fileName) => File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Rendering",
|
||||
fileName));
|
||||
|
||||
private static int CountOccurrences(string source, string value)
|
||||
private static void AssertCallOrder(
|
||||
MethodBase method,
|
||||
params (Type Type, string Method)[] expected)
|
||||
{
|
||||
int count = 0;
|
||||
int cursor = 0;
|
||||
while ((cursor = source.IndexOf(value, cursor, StringComparison.Ordinal)) >= 0)
|
||||
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(method);
|
||||
int cursor = -1;
|
||||
foreach ((Type type, string name) in expected)
|
||||
{
|
||||
count++;
|
||||
cursor += value.Length;
|
||||
int found = calls
|
||||
.Select((call, index) => (call, index))
|
||||
.Where(pair => pair.index > cursor)
|
||||
.Select(pair => pair.call)
|
||||
.Select((call, index) => (call, index: index + cursor + 1))
|
||||
.FirstOrDefault(
|
||||
pair => MatchesType(pair.call.Target.DeclaringType, type)
|
||||
&& pair.call.Target.Name == name,
|
||||
defaultValue: (default, -1))
|
||||
.index;
|
||||
Assert.True(
|
||||
found > cursor,
|
||||
$"Missing compiled edge after index {cursor}: {type.FullName}.{name}.");
|
||||
cursor = found;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private static void AssertAppearsInOrder(string source, params string[] needles)
|
||||
private static bool MatchesType(Type? actual, Type expected) =>
|
||||
actual == expected
|
||||
|| expected.IsGenericTypeDefinition
|
||||
&& actual?.IsGenericType == true
|
||||
&& actual.GetGenericTypeDefinition() == expected;
|
||||
|
||||
private static void AssertLabelOrder(
|
||||
IReadOnlyList<string> labels,
|
||||
params string[] expected)
|
||||
{
|
||||
int cursor = -1;
|
||||
foreach (string needle in needles)
|
||||
foreach (string label in expected)
|
||||
{
|
||||
int next = source.IndexOf(needle, cursor + 1, StringComparison.Ordinal);
|
||||
Assert.True(next >= 0, $"Missing expected source fragment: {needle}");
|
||||
Assert.True(next > cursor, $"Out-of-order source fragment: {needle}");
|
||||
cursor = next;
|
||||
int found = Enumerable.Range(cursor + 1, labels.Count - cursor - 1)
|
||||
.FirstOrDefault(index => labels[index] == label, -1);
|
||||
Assert.True(found > cursor, $"Missing shutdown label after {cursor}: {label}.");
|
||||
cursor = found;
|
||||
}
|
||||
}
|
||||
|
||||
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