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
|
attributed methods, and reduces direct/total source readers from 44/70 to
|
||||||
41/64. The remaining 64 reconcile to the 22 approved retained
|
41/64. The remaining 64 reconcile to the 22 approved retained
|
||||||
policies/contracts and 42 staged replacements.
|
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,
|
int Offset,
|
||||||
OpCode OpCode,
|
OpCode OpCode,
|
||||||
FieldInfo Field);
|
FieldInfo Field);
|
||||||
|
internal readonly record struct CompiledBranch(
|
||||||
|
int Offset,
|
||||||
|
OpCode OpCode,
|
||||||
|
int TargetOffset);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads compiled call/new-object edges from a method body. Architecture tests
|
/// Reads compiled call/new-object edges from a method body. Architecture tests
|
||||||
|
|
@ -146,6 +150,40 @@ internal static class CompiledCallGraph
|
||||||
return references;
|
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>
|
/// <summary>
|
||||||
/// Reads types named by compiled type operands such as casts, boxing, and
|
/// Reads types named by compiled type operands such as casts, boxing, and
|
||||||
/// <c>isinst</c>. This lets tests retain an exact type boundary without
|
/// <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.Rendering;
|
||||||
|
using AcDream.App.Streaming;
|
||||||
|
using AcDream.App.Tests.Architecture;
|
||||||
|
using AcDream.App.UI;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Rendering;
|
namespace AcDream.App.Tests.Rendering;
|
||||||
|
|
||||||
public sealed class GameWindowRenderLeafCompositionTests
|
public sealed class GameWindowRenderLeafCompositionTests
|
||||||
{
|
{
|
||||||
|
private const BindingFlags Declared = BindingFlags.Instance
|
||||||
|
| BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic
|
||||||
|
| BindingFlags.DeclaredOnly;
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ProductionRender_PreservesPrivatePresentationAndCaptureOrder()
|
public void ProductionRender_PreservesPrivatePresentationAndCaptureOrder()
|
||||||
{
|
{
|
||||||
string presentation = Source("PrivatePresentationRenderer.cs");
|
MethodInfo presentation = RequiredMethod(
|
||||||
string orchestrator = Source("RenderFrameOrchestrator.cs");
|
typeof(PrivatePresentationRenderer),
|
||||||
|
nameof(PrivatePresentationRenderer.Render));
|
||||||
AssertAppearsInOrder(
|
AssertCallOrder(
|
||||||
presentation,
|
presentation,
|
||||||
"_portal.Draw(",
|
(typeof(IPrivatePortalViewport), nameof(IPrivatePortalViewport.Draw)),
|
||||||
"_entityViewports?.Render();",
|
(typeof(IPrivateEntityViewportFrame), nameof(IPrivateEntityViewportFrame.Render)),
|
||||||
"_gameplayUi?.Render(",
|
(typeof(IRetainedGameplayUiFrame), nameof(IRetainedGameplayUiFrame.Render)),
|
||||||
"_devTools?.Render(",
|
(typeof(IDevToolsFrameLifecycle), nameof(IDevToolsFrameLifecycle.Render)),
|
||||||
"_screenshots?.CapturePending(");
|
(typeof(IPrivateFrameScreenshot), nameof(IPrivateFrameScreenshot.CapturePending)));
|
||||||
AssertAppearsInOrder(
|
|
||||||
|
MethodInfo orchestrator = RequiredMethod(
|
||||||
|
typeof(RenderFrameOrchestrator),
|
||||||
|
nameof(RenderFrameOrchestrator.Render));
|
||||||
|
AssertCallOrder(
|
||||||
orchestrator,
|
orchestrator,
|
||||||
"_world.Render(input);",
|
(typeof(IWorldSceneFramePhase), nameof(IWorldSceneFramePhase.Render)),
|
||||||
"_presentation.Render(input, world);",
|
(typeof(IPrivatePresentationFramePhase), nameof(IPrivatePresentationFramePhase.Render)),
|
||||||
"_diagnostics.Publish(input, outcome);");
|
(typeof(IRenderFrameDiagnosticsPhase), nameof(IRenderFrameDiagnosticsPhase.Publish)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ProductionRender_Prepares_resources_then_devtools_weather_and_world()
|
public void ProductionRender_Prepares_resources_then_devtools_weather_and_world()
|
||||||
{
|
{
|
||||||
string source = Source("RenderFramePreparationController.cs");
|
MethodInfo prepare = RequiredMethod(
|
||||||
|
typeof(RenderFramePreparationController),
|
||||||
AssertAppearsInOrder(
|
nameof(RenderFramePreparationController.Prepare));
|
||||||
source,
|
AssertCallOrder(
|
||||||
"_resources.Prepare(input);",
|
prepare,
|
||||||
"_devTools?.BeginFrame((float)input.DeltaSeconds);",
|
(typeof(IRenderFrameResourcePhase), nameof(IRenderFrameResourcePhase.Prepare)),
|
||||||
"_weather.Tick(input.DeltaSeconds);");
|
(typeof(IDevToolsFrameLifecycle), nameof(IDevToolsFrameLifecycle.BeginFrame)),
|
||||||
|
(typeof(IRenderWeatherFramePhase), nameof(IRenderWeatherFramePhase.Tick)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Composition_transfers_portal_tunnel_before_constructing_frame_borrowers()
|
public void Composition_transfers_portal_tunnel_before_constructing_frame_borrowers()
|
||||||
{
|
{
|
||||||
string phase = File.ReadAllText(Path.Combine(
|
MethodInfo complete = RequiredMethod(
|
||||||
FindRepoRoot(),
|
typeof(SessionPlayerCompositionPhase),
|
||||||
"src",
|
"CompleteSessionPlayer");
|
||||||
"AcDream.App",
|
AssertCallOrder(
|
||||||
"Composition",
|
complete,
|
||||||
"SessionPlayerComposition.cs"));
|
(typeof(TransferableResourceSlot<>), "Transfer"),
|
||||||
string source = GameWindowSource();
|
(typeof(DeferredLocalPlayerTeleportNetworkSink), "BindOwned"),
|
||||||
string framePhase = FrameRootSource();
|
(typeof(IGameWindowSessionPlayerPublication), "PublishSessionPlayer"));
|
||||||
|
|
||||||
AssertAppearsInOrder(
|
MethodBase tunnelFactory = ReferencedMethodConstructing(
|
||||||
phase,
|
complete,
|
||||||
"d.PortalTunnelFallback.Transfer(",
|
typeof(LocalPlayerTeleportPresentation));
|
||||||
"new LocalPlayerTeleportController(",
|
Assert.NotNull(ReferencedMethodConstructing(
|
||||||
"new LocalPlayerTeleportPresentation(portalTunnel)",
|
tunnelFactory,
|
||||||
"d.TeleportSink.BindOwned(localTeleport)",
|
typeof(LocalPlayerTeleportController)));
|
||||||
"_publication.PublishSessionPlayer(result);");
|
|
||||||
AssertAppearsInOrder(
|
MethodInfo onLoad = RequiredMethod(typeof(GameWindow), "OnLoad");
|
||||||
source,
|
IReadOnlyList<CompiledCall> loadReferences =
|
||||||
"new SessionPlayerCompositionPhase(",
|
CompiledCallGraph.ReadMethodReferences(onLoad);
|
||||||
"new FrameRootCompositionPhase(");
|
CompiledCall sessionPhase = Assert.Single(
|
||||||
AssertAppearsInOrder(
|
loadReferences,
|
||||||
framePhase,
|
reference => Constructs(
|
||||||
"new LocalPlayerTeleportRenderStateSource(",
|
reference.Target,
|
||||||
"new RenderFrameResourceController(",
|
typeof(SessionPlayerCompositionPhase)));
|
||||||
"new PrivatePresentationRenderer(",
|
CompiledCall framePhase = Assert.Single(
|
||||||
"new RenderFrameOrchestrator(");
|
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]
|
[Fact]
|
||||||
public void ProductionComposition_RemovesLegacyLeafOwnership()
|
public void ProductionComposition_RemovesLegacyLeafOwnership()
|
||||||
{
|
{
|
||||||
string source = GameWindowSource();
|
FieldInfo[] windowFields = typeof(GameWindow).GetFields(Declared);
|
||||||
|
HashSet<string> removedFieldNames =
|
||||||
string[] removed =
|
|
||||||
[
|
[
|
||||||
"_imguiBootstrap",
|
"_imguiBootstrap",
|
||||||
"_panelHost",
|
"_panelHost",
|
||||||
"_paperdollDollDirty",
|
"_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",
|
"RefreshPaperdollDoll",
|
||||||
"ApplyPaperdollPose",
|
"ApplyPaperdollPose",
|
||||||
"ResolvePaperdollPoseDid",
|
"ResolvePaperdollPoseDid",
|
||||||
"EnumerateDebugPanel",
|
"EnumerateDebugPanel",
|
||||||
"ResetPanelLayout",
|
"ResetPanelLayout",
|
||||||
"SetPanelLayout",
|
"SetPanelLayout",
|
||||||
"_lastRenderSignature",
|
"EmitRenderSignatureIfChanged",
|
||||||
"private void EmitRenderSignatureIfChanged(",
|
"EmitRetailPViewDiagnostics",
|
||||||
"private void EmitRetailPViewDiagnostics(",
|
"EmitGlStateTripwireIfChanged",
|
||||||
"EmitGlStateTripwireIfChanged();",
|
"EmitClipRouteScissorProbe",
|
||||||
"EmitClipRouteScissorProbe(scissor",
|
|
||||||
"_lastVisibleLandblocks",
|
|
||||||
"_perfAccum",
|
|
||||||
"_entityUploadTiming",
|
|
||||||
"_weatherAccum",
|
|
||||||
"TryGetLoginWorldCell",
|
"TryGetLoginWorldCell",
|
||||||
"ApplyFramePacingPreference",
|
"ApplyFramePacingPreference",
|
||||||
"RefreshActiveMonitorFramePacing",
|
"RefreshActiveMonitorFramePacing",
|
||||||
"private void OnFrameRendered(",
|
"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?",
|
|
||||||
];
|
];
|
||||||
foreach (string identifier in removed)
|
Assert.DoesNotContain(
|
||||||
Assert.DoesNotContain(identifier, source, StringComparison.Ordinal);
|
typeof(GameWindow).GetMethods(Declared),
|
||||||
|
method => removedMethods.Contains(method.Name));
|
||||||
|
|
||||||
Assert.Contains("new PaperdollFramePresenter(", LivePresentationSource());
|
Assert.Contains(
|
||||||
string framePhase = FrameRootSource();
|
CompiledCallGraph.ReadDeclared(typeof(LivePresentationCompositionPhase)),
|
||||||
Assert.Contains("new RenderFrameResourceController(", framePhase);
|
call => call.Target.DeclaringType == typeof(PaperdollFramePresenter)
|
||||||
Assert.Contains("new RenderWeatherFrameController(", framePhase);
|
&& call.Target.IsConstructor);
|
||||||
Assert.Contains("new PrivatePresentationRenderer(", framePhase);
|
IReadOnlyList<CompiledCall> frameCalls =
|
||||||
Assert.Contains("new RenderFrameOrchestrator(", framePhase);
|
CompiledCallGraph.ReadDeclared(typeof(FrameRootCompositionPhase));
|
||||||
Assert.Contains("new DisplayFramePacingController(", source);
|
Assert.All(
|
||||||
string pointerSource = File.ReadAllText(Path.Combine(
|
new[]
|
||||||
FindRepoRoot(),
|
{
|
||||||
"src",
|
typeof(RenderFrameResourceController),
|
||||||
"AcDream.App",
|
typeof(RenderWeatherFrameController),
|
||||||
"Input",
|
typeof(PrivatePresentationRenderer),
|
||||||
"CameraPointerInputController.cs"));
|
typeof(RenderFrameOrchestrator),
|
||||||
Assert.Contains("_capture.WantCaptureMouse", pointerSource);
|
},
|
||||||
|
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]
|
[Fact]
|
||||||
public void Shutdown_DrainsGpuBeforeFrontendsAndPreservesFrameBorrowerOrder()
|
public void Shutdown_DrainsGpuBeforeFrontendsAndPreservesFrameBorrowerOrder()
|
||||||
{
|
{
|
||||||
// Campaign V slice V11 removed the ImGui developer-tools frontend and
|
MethodInfo create = RequiredMethod(
|
||||||
// its "developer tools" shutdown stage entry along with it; this test
|
typeof(GameWindowShutdownManifest),
|
||||||
// used to pin that entry's position and is now renamed to pin what
|
nameof(GameWindowShutdownManifest.Create));
|
||||||
// survives it.
|
IReadOnlyList<string> labels = CompiledCallGraph.ReadStringLiterals(create);
|
||||||
string source = GameWindowLifetimeSource();
|
|
||||||
|
|
||||||
AssertAppearsInOrder(
|
AssertLabelOrder(
|
||||||
source,
|
labels,
|
||||||
"new ResourceShutdownStage(\"submitted GPU work\"",
|
"submitted GPU work",
|
||||||
"new ResourceShutdownStage(\"render frontends\"",
|
"render frontends",
|
||||||
"Hard(\"portal tunnel\"",
|
"portal tunnel",
|
||||||
"Hard(\"paperdoll viewport\"",
|
"paperdoll viewport",
|
||||||
"new ResourceShutdownStage(\"graphics API context\"");
|
"graphics API context");
|
||||||
AssertAppearsInOrder(
|
AssertLabelOrder(
|
||||||
source,
|
labels,
|
||||||
"new ResourceShutdownStage(\"frame borrowers\"",
|
"frame borrowers",
|
||||||
"frame.FrameGraphPublication?.Dispose()",
|
"world frame composition",
|
||||||
"frame.FrameBindings?.Dispose()",
|
"frame-root bindings",
|
||||||
"new ResourceShutdownStage(\"session dependents\"");
|
"session dependents");
|
||||||
AssertAppearsInOrder(
|
AssertLabelOrder(labels, "frame pacing", "frame profiler");
|
||||||
source,
|
AssertLabelOrder(
|
||||||
"Hard(\"frame pacing\", render.FramePacing.Dispose)",
|
labels,
|
||||||
"Hard(\"frame profiler\", render.FrameProfiler.Dispose)");
|
"world frame composition",
|
||||||
AssertAppearsInOrder(
|
"render frontends",
|
||||||
source,
|
"input context",
|
||||||
"frame.FrameGraphPublication?.Dispose()",
|
"graphics API context");
|
||||||
"new ResourceShutdownStage(\"render frontends\"",
|
|
||||||
"new ResourceShutdownStage(\"input context\"",
|
|
||||||
"platform.Input?.Dispose()",
|
|
||||||
"new ResourceShutdownStage(\"graphics API context\"");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ProductionOutcome_UsesObservedWorldAndScreenshotFacts()
|
public void ProductionOutcome_UsesObservedWorldAndScreenshotFacts()
|
||||||
{
|
{
|
||||||
string presentation = Source("PrivatePresentationRenderer.cs");
|
MethodInfo presentation = RequiredMethod(
|
||||||
string orchestrator = Source("RenderFrameOrchestrator.cs");
|
typeof(PrivatePresentationRenderer),
|
||||||
|
nameof(PrivatePresentationRenderer.Render));
|
||||||
AssertAppearsInOrder(
|
AssertCallOrder(
|
||||||
presentation,
|
presentation,
|
||||||
"_foundation.Foundation.PortalViewportVisible;",
|
(typeof(RenderFrameFoundation), "get_PortalViewportVisible"),
|
||||||
"_portal.Draw(",
|
(typeof(IPrivatePortalViewport), nameof(IPrivatePortalViewport.Draw)),
|
||||||
"bool screenshotCaptured = _screenshots?.CapturePending(",
|
(typeof(IPrivateFrameScreenshot), nameof(IPrivateFrameScreenshot.CapturePending)),
|
||||||
"portalViewportVisible,",
|
(typeof(PrivatePresentationFrameOutcome), ".ctor"));
|
||||||
"screenshotCaptured);");
|
|
||||||
AssertAppearsInOrder(
|
MethodInfo orchestrator = RequiredMethod(
|
||||||
|
typeof(RenderFrameOrchestrator),
|
||||||
|
nameof(RenderFrameOrchestrator.Render));
|
||||||
|
AssertCallOrder(
|
||||||
orchestrator,
|
orchestrator,
|
||||||
"_gpuMeasurement.BeginFrame();",
|
(typeof(IRenderFrameGpuMeasurement), nameof(IRenderFrameGpuMeasurement.BeginFrame)),
|
||||||
"world = _world.Render(input);",
|
(typeof(IWorldSceneFramePhase), nameof(IWorldSceneFramePhase.Render)),
|
||||||
"_presentation.Render(input, world);",
|
(typeof(IPrivatePresentationFramePhase), nameof(IPrivatePresentationFramePhase.Render)),
|
||||||
"_gpuMeasurement.EndFrame();",
|
(typeof(IRenderFrameGpuMeasurement), nameof(IRenderFrameGpuMeasurement.EndFrame)),
|
||||||
"new RenderFrameOutcome(world, presentation);",
|
(typeof(RenderFrameOutcome), ".ctor"),
|
||||||
"_diagnostics.Publish(input, outcome);");
|
(typeof(IRenderFrameDiagnosticsPhase), nameof(IRenderFrameDiagnosticsPhase.Publish)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GameWindow_OnRenderIsOneImmutableOrchestratorHandoff()
|
public void GameWindow_OnRenderIsOneImmutableOrchestratorHandoff()
|
||||||
{
|
{
|
||||||
string source = GameWindowSource();
|
MethodInfo onRender = RequiredMethod(typeof(GameWindow), "OnRender");
|
||||||
int start = source.IndexOf(
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(onRender);
|
||||||
"private void OnRender(double deltaSeconds)",
|
Assert.Single(
|
||||||
StringComparison.Ordinal);
|
calls,
|
||||||
int end = source.IndexOf(
|
call => call.Target.DeclaringType == typeof(GameFrameGraphSlot)
|
||||||
"private void OnFramebufferResize(",
|
&& call.Target.Name == nameof(GameFrameGraphSlot.Render));
|
||||||
start,
|
|
||||||
StringComparison.Ordinal);
|
|
||||||
Assert.True(start >= 0 && end > start);
|
|
||||||
string body = source[start..end];
|
|
||||||
|
|
||||||
Assert.Equal(1, CountOccurrences(body, "_frameGraphs.Render("));
|
HashSet<string> forbiddenFields =
|
||||||
Assert.DoesNotContain("_gpuFrameFlights", body);
|
[
|
||||||
Assert.DoesNotContain("_worldScene", body);
|
"_gpuFrameFlights",
|
||||||
Assert.DoesNotContain("_devToolsFramePresenter", body);
|
"_worldScene",
|
||||||
Assert.DoesNotContain("_retailUiRuntime", body);
|
"_devToolsFramePresenter",
|
||||||
Assert.DoesNotContain("_frameScreenshots", body);
|
"_retailUiRuntime",
|
||||||
|
"_frameScreenshots",
|
||||||
|
];
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
CompiledCallGraph.ReadFieldReferences(onRender),
|
||||||
|
reference => forbiddenFields.Contains(reference.Field.Name));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PaperdollComposition_SkipsEitherMissingOptionalUiSurface()
|
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(
|
IReadOnlyList<CompiledBranch> branches = CompiledCallGraph.ReadBranches(compose);
|
||||||
source,
|
Assert.Contains(
|
||||||
"PaperdollViewportWidget is { } viewport",
|
branches,
|
||||||
"InventoryFrame is { } inventoryFrame",
|
branch => branch.OpCode.FlowControl == System.Reflection.Emit.FlowControl.Cond_Branch
|
||||||
"new PaperdollFramePresenter(");
|
&& branch.Offset > viewport.Offset
|
||||||
Assert.DoesNotContain("Paperdoll inventory frame is required.", source);
|
&& 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]
|
[Fact]
|
||||||
public void TerrainAndFrameDiagnostics_AreComposedAsOneFocusedOwner()
|
public void TerrainAndFrameDiagnostics_AreComposedAsOneFocusedOwner()
|
||||||
{
|
{
|
||||||
string source = FrameRootSource();
|
MethodInfo compose = RequiredMethod(
|
||||||
|
typeof(FrameRootCompositionPhase),
|
||||||
Assert.Contains(
|
"ComposeCore");
|
||||||
"new TerrainDrawDiagnosticsController(",
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(compose);
|
||||||
source);
|
AssertCallOrder(
|
||||||
Assert.Contains("new WorldScenePassExecutor(", source);
|
compose,
|
||||||
Assert.DoesNotContain("_terrainDrawDiagnostics!.Begin();", source);
|
(typeof(TerrainDrawDiagnosticsController), ".ctor"),
|
||||||
Assert.DoesNotContain("_terrainDrawDiagnostics.Complete();", source);
|
(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(
|
private static MethodInfo RequiredMethod(Type type, string name) =>
|
||||||
FindRepoRoot(),
|
type.GetMethod(
|
||||||
"src",
|
name,
|
||||||
"AcDream.App",
|
BindingFlags.Instance | BindingFlags.Static
|
||||||
"Rendering",
|
| BindingFlags.Public | BindingFlags.NonPublic)
|
||||||
"GameWindow.cs"));
|
?? throw new MissingMethodException(type.FullName, name);
|
||||||
|
|
||||||
private static string GameWindowLifetimeSource() => File.ReadAllText(Path.Combine(
|
private static MethodBase MethodConstructing(Type owner, Type constructed) =>
|
||||||
FindRepoRoot(),
|
Assert.Single(
|
||||||
"src",
|
owner.GetMethods(Declared)
|
||||||
"AcDream.App",
|
.Cast<MethodBase>()
|
||||||
"Rendering",
|
.Concat(owner.GetConstructors(Declared))
|
||||||
"GameWindowLifetime.cs"));
|
.Where(method => method.GetMethodBody() is not null),
|
||||||
|
method => Constructs(method, constructed));
|
||||||
|
|
||||||
private static string LivePresentationSource() => File.ReadAllText(Path.Combine(
|
private static MethodBase ReferencedMethodConstructing(
|
||||||
FindRepoRoot(),
|
MethodBase owner,
|
||||||
"src",
|
Type constructed) =>
|
||||||
"AcDream.App",
|
Assert.Single(
|
||||||
"Composition",
|
CompiledCallGraph.ReadMethodReferences(owner)
|
||||||
"LivePresentationComposition.cs"));
|
.Select(reference => reference.Target)
|
||||||
|
.Where(method => method.GetMethodBody() is not null)
|
||||||
|
.Distinct(),
|
||||||
|
method => Constructs(method, constructed));
|
||||||
|
|
||||||
private static string FrameRootSource() => File.ReadAllText(Path.Combine(
|
private static bool Constructs(MethodBase method, Type type) =>
|
||||||
FindRepoRoot(),
|
method.GetMethodBody() is not null
|
||||||
"src",
|
&& CompiledCallGraph.Read(method).Any(call =>
|
||||||
"AcDream.App",
|
call.Target.DeclaringType == type && call.Target.IsConstructor);
|
||||||
"Composition",
|
|
||||||
"FrameRootComposition.cs"));
|
|
||||||
|
|
||||||
private static string Source(string fileName) => File.ReadAllText(Path.Combine(
|
private static void AssertCallOrder(
|
||||||
FindRepoRoot(),
|
MethodBase method,
|
||||||
"src",
|
params (Type Type, string Method)[] expected)
|
||||||
"AcDream.App",
|
|
||||||
"Rendering",
|
|
||||||
fileName));
|
|
||||||
|
|
||||||
private static int CountOccurrences(string source, string value)
|
|
||||||
{
|
{
|
||||||
int count = 0;
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(method);
|
||||||
int cursor = 0;
|
int cursor = -1;
|
||||||
while ((cursor = source.IndexOf(value, cursor, StringComparison.Ordinal)) >= 0)
|
foreach ((Type type, string name) in expected)
|
||||||
{
|
{
|
||||||
count++;
|
int found = calls
|
||||||
cursor += value.Length;
|
.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;
|
int cursor = -1;
|
||||||
foreach (string needle in needles)
|
foreach (string label in expected)
|
||||||
{
|
{
|
||||||
int next = source.IndexOf(needle, cursor + 1, StringComparison.Ordinal);
|
int found = Enumerable.Range(cursor + 1, labels.Count - cursor - 1)
|
||||||
Assert.True(next >= 0, $"Missing expected source fragment: {needle}");
|
.FirstOrDefault(index => labels[index] == label, -1);
|
||||||
Assert.True(next > cursor, $"Out-of-order source fragment: {needle}");
|
Assert.True(found > cursor, $"Missing shutdown label after {cursor}: {label}.");
|
||||||
cursor = next;
|
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