refactor(render): establish frame orchestration contract
This commit is contained in:
parent
f316bbd817
commit
7e4cfb37c3
4 changed files with 489 additions and 5 deletions
|
|
@ -14,7 +14,7 @@ port, or resource-lifetime redesign.
|
||||||
|
|
||||||
## Progress ledger
|
## Progress ledger
|
||||||
|
|
||||||
- [ ] A — freeze the complete render graph, correct the architecture SSOT, and
|
- [x] A — freeze the complete render graph, correct the architecture SSOT, and
|
||||||
introduce data-only frame contracts plus deterministic order/failure tests.
|
introduce data-only frame contracts plus deterministic order/failure tests.
|
||||||
- [ ] B — extract paperdoll, panel-layout/devtools, and render-diagnostics leaf
|
- [ ] B — extract paperdoll, panel-layout/devtools, and render-diagnostics leaf
|
||||||
owners while `GameWindow` still orders the frame.
|
owners while `GameWindow` still orders the frame.
|
||||||
|
|
@ -319,14 +319,24 @@ never choose a draw branch or mutate canonical world ownership.
|
||||||
|
|
||||||
- correct the stale render-pipeline and per-frame architecture sections;
|
- correct the stale render-pipeline and per-frame architecture sections;
|
||||||
- add `RenderFrameInput`, phase interfaces, and a recording orchestrator;
|
- add `RenderFrameInput`, phase interfaces, and a recording orchestrator;
|
||||||
- pin normal order, portal replacement, login wait, indoor/outdoor/null-root
|
- pin the outer resource/world/private/diagnostic order across normal,
|
||||||
branches, and screenshot placement; camera is an initialized runtime
|
portal-replacement, login-wait, and screenshot outcome shapes; defer the
|
||||||
invariant, so the slice does not invent a new graceful missing-camera path;
|
concrete indoor/outdoor/null-root branch tests to the world builder and scene
|
||||||
|
owner in D/F; camera is an initialized runtime invariant, so the slice does
|
||||||
|
not invent a new graceful missing-camera path;
|
||||||
- pin the exact GPU begin/end exception matrix;
|
- pin the exact GPU begin/end exception matrix;
|
||||||
- add source guards against `GameWindow` back-references, mega contexts, and a
|
- add source guards against `GameWindow` back-references, mega contexts, and a
|
||||||
new PView delegate bag;
|
new PView delegate bag;
|
||||||
- capture the current connected framebuffer/resource baseline.
|
- capture the current connected framebuffer/resource baseline.
|
||||||
|
|
||||||
|
Completed 2026-07-22. `RenderFrameOrchestrator` now owns the inert five-owner
|
||||||
|
outer transaction contract; `GpuFrameFlightController` implements its narrow
|
||||||
|
lifetime seam. Twenty-six focused render/GPU tests pin value-only contracts,
|
||||||
|
required-owner construction, exact phase prefixes, per-phase input propagation,
|
||||||
|
and every begin/render/close failure combination. Release build and the full
|
||||||
|
suite pass (7,199 passed / 5 skipped after adding 17 tests). All three
|
||||||
|
corrected-diff reviews are clean; production `GameWindow.OnRender` is unchanged.
|
||||||
|
|
||||||
### B — leaf presentation and diagnostics
|
### B — leaf presentation and diagnostics
|
||||||
|
|
||||||
- extract paperdoll refresh/pose/dirty/FBO ownership;
|
- extract paperdoll refresh/pose/dirty/FBO ownership;
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,10 @@ internal sealed class ImmediateGpuResourceRetirementQueue : IGpuResourceRetireme
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class GpuFrameFlightController : IGpuResourceRetirementQueue, IDisposable
|
internal sealed class GpuFrameFlightController :
|
||||||
|
IGpuResourceRetirementQueue,
|
||||||
|
IRenderFrameLifetime,
|
||||||
|
IDisposable
|
||||||
{
|
{
|
||||||
internal const int DefaultMaximumFramesInFlight = 3;
|
internal const int DefaultMaximumFramesInFlight = 3;
|
||||||
private const ulong WaitSliceNanoseconds = 1_000_000;
|
private const ulong WaitSliceNanoseconds = 1_000_000;
|
||||||
|
|
|
||||||
131
src/AcDream.App/Rendering/RenderFrameOrchestrator.cs
Normal file
131
src/AcDream.App/Rendering/RenderFrameOrchestrator.cs
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Host-owned values for one render callback. Render phases borrow this value
|
||||||
|
/// for the duration of <see cref="RenderFrameOrchestrator.Render"/> only.
|
||||||
|
/// </summary>
|
||||||
|
internal readonly record struct RenderFrameInput(
|
||||||
|
double DeltaSeconds,
|
||||||
|
int ViewportWidth,
|
||||||
|
int ViewportHeight);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// World facts published after the normal-world phase has either drawn or
|
||||||
|
/// intentionally skipped the viewport.
|
||||||
|
/// </summary>
|
||||||
|
internal readonly record struct WorldRenderFrameOutcome(
|
||||||
|
int VisibleLandblocks,
|
||||||
|
int TotalLandblocks,
|
||||||
|
bool NormalWorldDrawn);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Private-presentation facts published after portal, paperdoll, retained UI,
|
||||||
|
/// developer UI, and screenshot work has completed.
|
||||||
|
/// </summary>
|
||||||
|
internal readonly record struct PrivatePresentationFrameOutcome(
|
||||||
|
bool PortalViewportDrawn,
|
||||||
|
bool ScreenshotCaptured);
|
||||||
|
|
||||||
|
/// <summary>The immutable result observed by post-screenshot diagnostics.</summary>
|
||||||
|
internal readonly record struct RenderFrameOutcome(
|
||||||
|
WorldRenderFrameOutcome World,
|
||||||
|
PrivatePresentationFrameOutcome Presentation);
|
||||||
|
|
||||||
|
internal interface IRenderFrameLifetime
|
||||||
|
{
|
||||||
|
void BeginFrame();
|
||||||
|
|
||||||
|
void EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface IRenderFrameResourcePhase
|
||||||
|
{
|
||||||
|
void Prepare(RenderFrameInput input);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface IWorldSceneFramePhase
|
||||||
|
{
|
||||||
|
WorldRenderFrameOutcome Render(RenderFrameInput input);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface IPrivatePresentationFramePhase
|
||||||
|
{
|
||||||
|
PrivatePresentationFrameOutcome Render(
|
||||||
|
RenderFrameInput input,
|
||||||
|
WorldRenderFrameOutcome world);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface IRenderFrameDiagnosticsPhase
|
||||||
|
{
|
||||||
|
void Publish(RenderFrameInput input, RenderFrameOutcome outcome);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Owns the accepted outer render transaction. Focused phase owners retain the
|
||||||
|
/// detailed PView, private-viewport, UI, and diagnostic order inside their
|
||||||
|
/// typed boundaries.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <see cref="IRenderFrameLifetime.BeginFrame"/> intentionally runs outside
|
||||||
|
/// the protected render body. Once begin succeeds, close is attempted exactly
|
||||||
|
/// once. This preserves the production GPU-flight contract: a render failure
|
||||||
|
/// and close failure are reported together, while either failure alone
|
||||||
|
/// propagates directly.
|
||||||
|
/// </remarks>
|
||||||
|
internal sealed class RenderFrameOrchestrator
|
||||||
|
{
|
||||||
|
private readonly IRenderFrameLifetime _lifetime;
|
||||||
|
private readonly IRenderFrameResourcePhase _resources;
|
||||||
|
private readonly IWorldSceneFramePhase _world;
|
||||||
|
private readonly IPrivatePresentationFramePhase _presentation;
|
||||||
|
private readonly IRenderFrameDiagnosticsPhase _diagnostics;
|
||||||
|
|
||||||
|
public RenderFrameOrchestrator(
|
||||||
|
IRenderFrameLifetime lifetime,
|
||||||
|
IRenderFrameResourcePhase resources,
|
||||||
|
IWorldSceneFramePhase world,
|
||||||
|
IPrivatePresentationFramePhase presentation,
|
||||||
|
IRenderFrameDiagnosticsPhase diagnostics)
|
||||||
|
{
|
||||||
|
_lifetime = lifetime ?? throw new ArgumentNullException(nameof(lifetime));
|
||||||
|
_resources = resources ?? throw new ArgumentNullException(nameof(resources));
|
||||||
|
_world = world ?? throw new ArgumentNullException(nameof(world));
|
||||||
|
_presentation = presentation ?? throw new ArgumentNullException(nameof(presentation));
|
||||||
|
_diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
|
||||||
|
}
|
||||||
|
|
||||||
|
public RenderFrameOutcome Render(RenderFrameInput input)
|
||||||
|
{
|
||||||
|
_lifetime.BeginFrame();
|
||||||
|
Exception? renderFailure = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_resources.Prepare(input);
|
||||||
|
WorldRenderFrameOutcome world = _world.Render(input);
|
||||||
|
PrivatePresentationFrameOutcome presentation =
|
||||||
|
_presentation.Render(input, world);
|
||||||
|
var outcome = new RenderFrameOutcome(world, presentation);
|
||||||
|
_diagnostics.Publish(input, outcome);
|
||||||
|
return outcome;
|
||||||
|
}
|
||||||
|
catch (Exception error)
|
||||||
|
{
|
||||||
|
renderFailure = error;
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_lifetime.EndFrame();
|
||||||
|
}
|
||||||
|
catch (Exception closeFailure) when (renderFailure is not null)
|
||||||
|
{
|
||||||
|
throw new AggregateException(
|
||||||
|
"Rendering failed and the in-flight GPU frame could not be closed.",
|
||||||
|
renderFailure,
|
||||||
|
closeFailure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,340 @@
|
||||||
|
using System.Reflection;
|
||||||
|
using AcDream.App.Rendering;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.Rendering;
|
||||||
|
|
||||||
|
public sealed class RenderFrameOrchestratorTests
|
||||||
|
{
|
||||||
|
private static readonly RenderFrameInput Input = new(
|
||||||
|
DeltaSeconds: 1.0 / 60.0,
|
||||||
|
ViewportWidth: 1920,
|
||||||
|
ViewportHeight: 1080);
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(12, 40, true, false, false)]
|
||||||
|
[InlineData(0, 0, false, false, false)]
|
||||||
|
[InlineData(0, 0, false, true, false)]
|
||||||
|
[InlineData(4, 17, true, false, true)]
|
||||||
|
public void Render_PreservesOuterOrderAcrossWorldAndPresentationOutcomes(
|
||||||
|
int visibleLandblocks,
|
||||||
|
int totalLandblocks,
|
||||||
|
bool normalWorldDrawn,
|
||||||
|
bool portalViewportDrawn,
|
||||||
|
bool screenshotCaptured)
|
||||||
|
{
|
||||||
|
var calls = new List<string>();
|
||||||
|
var phases = new RecordingPhases(calls)
|
||||||
|
{
|
||||||
|
World = new WorldRenderFrameOutcome(
|
||||||
|
visibleLandblocks,
|
||||||
|
totalLandblocks,
|
||||||
|
normalWorldDrawn),
|
||||||
|
Presentation = new PrivatePresentationFrameOutcome(
|
||||||
|
portalViewportDrawn,
|
||||||
|
screenshotCaptured),
|
||||||
|
};
|
||||||
|
var orchestrator = Create(phases);
|
||||||
|
|
||||||
|
RenderFrameOutcome outcome = orchestrator.Render(Input);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
["gpu-begin", "resources", "world", "presentation", "diagnostics", "gpu-end"],
|
||||||
|
calls);
|
||||||
|
Assert.Equal(phases.World, outcome.World);
|
||||||
|
Assert.Equal(phases.Presentation, outcome.Presentation);
|
||||||
|
Assert.Equal(
|
||||||
|
[
|
||||||
|
("resources", Input),
|
||||||
|
("world", Input),
|
||||||
|
("presentation", Input),
|
||||||
|
("diagnostics", Input),
|
||||||
|
],
|
||||||
|
phases.ObservedInputs);
|
||||||
|
Assert.Equal(phases.World, phases.ObservedWorld);
|
||||||
|
Assert.Equal(outcome, phases.ObservedOutcome);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BeginFailure_DoesNotAttemptAnyPhaseOrClose()
|
||||||
|
{
|
||||||
|
var calls = new List<string>();
|
||||||
|
var expected = new InvalidOperationException("begin");
|
||||||
|
var phases = new RecordingPhases(calls)
|
||||||
|
{
|
||||||
|
FailurePoint = "gpu-begin",
|
||||||
|
Failure = expected,
|
||||||
|
};
|
||||||
|
|
||||||
|
InvalidOperationException actual = Assert.Throws<InvalidOperationException>(
|
||||||
|
() => Create(phases).Render(Input));
|
||||||
|
|
||||||
|
Assert.Same(expected, actual);
|
||||||
|
Assert.Equal(["gpu-begin"], calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("resources")]
|
||||||
|
[InlineData("world")]
|
||||||
|
[InlineData("presentation")]
|
||||||
|
[InlineData("diagnostics")]
|
||||||
|
public void RenderFailure_ClosesExactlyOnceAndPropagates(string failurePoint)
|
||||||
|
{
|
||||||
|
var calls = new List<string>();
|
||||||
|
var expected = new InvalidOperationException(failurePoint);
|
||||||
|
var phases = new RecordingPhases(calls)
|
||||||
|
{
|
||||||
|
FailurePoint = failurePoint,
|
||||||
|
Failure = expected,
|
||||||
|
};
|
||||||
|
|
||||||
|
InvalidOperationException actual = Assert.Throws<InvalidOperationException>(
|
||||||
|
() => Create(phases).Render(Input));
|
||||||
|
|
||||||
|
Assert.Same(expected, actual);
|
||||||
|
Assert.Equal(ExpectedFailureCalls(failurePoint), calls);
|
||||||
|
Assert.Equal(
|
||||||
|
ExpectedPhaseInputs(failurePoint),
|
||||||
|
phases.ObservedInputs);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CloseOnlyFailure_PropagatesDirectly()
|
||||||
|
{
|
||||||
|
var calls = new List<string>();
|
||||||
|
var expected = new InvalidOperationException("close");
|
||||||
|
var phases = new RecordingPhases(calls)
|
||||||
|
{
|
||||||
|
CloseFailure = expected,
|
||||||
|
};
|
||||||
|
|
||||||
|
InvalidOperationException actual = Assert.Throws<InvalidOperationException>(
|
||||||
|
() => Create(phases).Render(Input));
|
||||||
|
|
||||||
|
Assert.Same(expected, actual);
|
||||||
|
Assert.Equal(
|
||||||
|
["gpu-begin", "resources", "world", "presentation", "diagnostics", "gpu-end"],
|
||||||
|
calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("resources")]
|
||||||
|
[InlineData("world")]
|
||||||
|
[InlineData("presentation")]
|
||||||
|
[InlineData("diagnostics")]
|
||||||
|
public void RenderAndCloseFailure_AreAggregatedInCausalOrder(string failurePoint)
|
||||||
|
{
|
||||||
|
var calls = new List<string>();
|
||||||
|
var renderFailure = new InvalidOperationException("render");
|
||||||
|
var closeFailure = new InvalidOperationException("close");
|
||||||
|
var phases = new RecordingPhases(calls)
|
||||||
|
{
|
||||||
|
FailurePoint = failurePoint,
|
||||||
|
Failure = renderFailure,
|
||||||
|
CloseFailure = closeFailure,
|
||||||
|
};
|
||||||
|
|
||||||
|
AggregateException actual = Assert.Throws<AggregateException>(
|
||||||
|
() => Create(phases).Render(Input));
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
"Rendering failed and the in-flight GPU frame could not be closed.",
|
||||||
|
actual.Message.Split(" (")[0]);
|
||||||
|
Assert.Equal(2, actual.InnerExceptions.Count);
|
||||||
|
Assert.Same(renderFailure, actual.InnerExceptions[0]);
|
||||||
|
Assert.Same(closeFailure, actual.InnerExceptions[1]);
|
||||||
|
Assert.Equal(ExpectedFailureCalls(failurePoint), calls);
|
||||||
|
Assert.Equal(
|
||||||
|
ExpectedPhaseInputs(failurePoint),
|
||||||
|
phases.ObservedInputs);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_RejectsEveryMissingRequiredOwner()
|
||||||
|
{
|
||||||
|
var phases = new RecordingPhases([]);
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentNullException>(() => new RenderFrameOrchestrator(
|
||||||
|
null!, phases, phases, phases, phases));
|
||||||
|
Assert.Throws<ArgumentNullException>(() => new RenderFrameOrchestrator(
|
||||||
|
phases, null!, phases, phases, phases));
|
||||||
|
Assert.Throws<ArgumentNullException>(() => new RenderFrameOrchestrator(
|
||||||
|
phases, phases, null!, phases, phases));
|
||||||
|
Assert.Throws<ArgumentNullException>(() => new RenderFrameOrchestrator(
|
||||||
|
phases, phases, phases, null!, phases));
|
||||||
|
Assert.Throws<ArgumentNullException>(() => new RenderFrameOrchestrator(
|
||||||
|
phases, phases, phases, phases, null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Orchestrator_UsesOnlyTheExplicitTypedOwnerGraph()
|
||||||
|
{
|
||||||
|
Type[] expectedFieldTypes =
|
||||||
|
[
|
||||||
|
typeof(IRenderFrameLifetime),
|
||||||
|
typeof(IRenderFrameResourcePhase),
|
||||||
|
typeof(IWorldSceneFramePhase),
|
||||||
|
typeof(IPrivatePresentationFramePhase),
|
||||||
|
typeof(IRenderFrameDiagnosticsPhase),
|
||||||
|
];
|
||||||
|
FieldInfo[] fields = typeof(RenderFrameOrchestrator).GetFields(
|
||||||
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
expectedFieldTypes.OrderBy(type => type.FullName),
|
||||||
|
fields.Select(field => field.FieldType).OrderBy(type => type.FullName));
|
||||||
|
Assert.All(fields, field =>
|
||||||
|
{
|
||||||
|
Assert.NotEqual(typeof(GameWindow), field.FieldType);
|
||||||
|
Assert.False(typeof(Delegate).IsAssignableFrom(field.FieldType));
|
||||||
|
});
|
||||||
|
Assert.All(
|
||||||
|
expectedFieldTypes,
|
||||||
|
contract => Assert.False(contract.IsAssignableFrom(typeof(GameWindow))));
|
||||||
|
foreach (Type contract in expectedFieldTypes)
|
||||||
|
{
|
||||||
|
foreach (MethodInfo method in contract.GetMethods())
|
||||||
|
{
|
||||||
|
Assert.True(
|
||||||
|
method.ReturnType == typeof(void) || method.ReturnType.IsValueType,
|
||||||
|
$"{contract.Name}.{method.Name} returned owner-like type "
|
||||||
|
+ method.ReturnType.FullName);
|
||||||
|
Assert.All(
|
||||||
|
method.GetParameters(),
|
||||||
|
parameter => Assert.True(
|
||||||
|
parameter.ParameterType.IsValueType,
|
||||||
|
$"{contract.Name}.{method.Name} accepted owner-like type "
|
||||||
|
+ parameter.ParameterType.FullName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Assert.True(typeof(IRenderFrameLifetime).IsAssignableFrom(
|
||||||
|
typeof(GpuFrameFlightController)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FrameContracts_AreDataOnlyValuesWithoutOwnerOrDelegateReferences()
|
||||||
|
{
|
||||||
|
Type[] contracts =
|
||||||
|
[
|
||||||
|
typeof(RenderFrameInput),
|
||||||
|
typeof(WorldRenderFrameOutcome),
|
||||||
|
typeof(PrivatePresentationFrameOutcome),
|
||||||
|
typeof(RenderFrameOutcome),
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach (Type contract in contracts)
|
||||||
|
{
|
||||||
|
Assert.True(contract.IsValueType);
|
||||||
|
Assert.All(
|
||||||
|
contract.GetFields(
|
||||||
|
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic),
|
||||||
|
field =>
|
||||||
|
{
|
||||||
|
Assert.True(
|
||||||
|
field.FieldType.IsValueType,
|
||||||
|
$"{contract.Name}.{field.Name} retained owner-like type "
|
||||||
|
+ field.FieldType.FullName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string[] ExpectedFailureCalls(string failurePoint) => failurePoint switch
|
||||||
|
{
|
||||||
|
"resources" => ["gpu-begin", "resources", "gpu-end"],
|
||||||
|
"world" => ["gpu-begin", "resources", "world", "gpu-end"],
|
||||||
|
"presentation" =>
|
||||||
|
["gpu-begin", "resources", "world", "presentation", "gpu-end"],
|
||||||
|
"diagnostics" =>
|
||||||
|
[
|
||||||
|
"gpu-begin", "resources", "world", "presentation", "diagnostics",
|
||||||
|
"gpu-end",
|
||||||
|
],
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(failurePoint)),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static (string Phase, RenderFrameInput Input)[] ExpectedPhaseInputs(
|
||||||
|
string failurePoint)
|
||||||
|
{
|
||||||
|
string[] phases = failurePoint switch
|
||||||
|
{
|
||||||
|
"resources" => ["resources"],
|
||||||
|
"world" => ["resources", "world"],
|
||||||
|
"presentation" => ["resources", "world", "presentation"],
|
||||||
|
"diagnostics" => ["resources", "world", "presentation", "diagnostics"],
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(failurePoint)),
|
||||||
|
};
|
||||||
|
return phases.Select(phase => (phase, Input)).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RenderFrameOrchestrator Create(RecordingPhases phases) =>
|
||||||
|
new(phases, phases, phases, phases, phases);
|
||||||
|
|
||||||
|
private sealed class RecordingPhases :
|
||||||
|
IRenderFrameLifetime,
|
||||||
|
IRenderFrameResourcePhase,
|
||||||
|
IWorldSceneFramePhase,
|
||||||
|
IPrivatePresentationFramePhase,
|
||||||
|
IRenderFrameDiagnosticsPhase
|
||||||
|
{
|
||||||
|
private readonly List<string> _calls;
|
||||||
|
|
||||||
|
public RecordingPhases(List<string> calls)
|
||||||
|
{
|
||||||
|
_calls = calls;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? FailurePoint { get; init; }
|
||||||
|
public Exception? Failure { get; init; }
|
||||||
|
public Exception? CloseFailure { get; init; }
|
||||||
|
public WorldRenderFrameOutcome World { get; init; } = new(7, 19, true);
|
||||||
|
public PrivatePresentationFrameOutcome Presentation { get; init; } = new(false, false);
|
||||||
|
public List<(string Phase, RenderFrameInput Input)> ObservedInputs { get; } = [];
|
||||||
|
public WorldRenderFrameOutcome ObservedWorld { get; private set; }
|
||||||
|
public RenderFrameOutcome ObservedOutcome { get; private set; }
|
||||||
|
|
||||||
|
public void BeginFrame() => Record("gpu-begin");
|
||||||
|
|
||||||
|
public void EndFrame()
|
||||||
|
{
|
||||||
|
_calls.Add("gpu-end");
|
||||||
|
if (CloseFailure is not null)
|
||||||
|
throw CloseFailure;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Prepare(RenderFrameInput input)
|
||||||
|
{
|
||||||
|
ObservedInputs.Add(("resources", input));
|
||||||
|
Record("resources");
|
||||||
|
}
|
||||||
|
|
||||||
|
public WorldRenderFrameOutcome Render(RenderFrameInput input)
|
||||||
|
{
|
||||||
|
ObservedInputs.Add(("world", input));
|
||||||
|
Record("world");
|
||||||
|
return World;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PrivatePresentationFrameOutcome Render(
|
||||||
|
RenderFrameInput input,
|
||||||
|
WorldRenderFrameOutcome world)
|
||||||
|
{
|
||||||
|
ObservedInputs.Add(("presentation", input));
|
||||||
|
ObservedWorld = world;
|
||||||
|
Record("presentation");
|
||||||
|
return Presentation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Publish(RenderFrameInput input, RenderFrameOutcome outcome)
|
||||||
|
{
|
||||||
|
ObservedInputs.Add(("diagnostics", input));
|
||||||
|
ObservedOutcome = outcome;
|
||||||
|
Record("diagnostics");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Record(string call)
|
||||||
|
{
|
||||||
|
_calls.Add(call);
|
||||||
|
if (FailurePoint == call && Failure is not null)
|
||||||
|
throw Failure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue