Carry one immutable prepared collision closure with each accepted near-tier generation and install graph plus flat views through the same retained publication receipt. Apply the same strict package-only rule to live entities, add exact sampled graph-authoritative comparison artifacts and lifecycle counters, and prove cancellation, demotion, rehydrate, revisit, teardown, reconnect, and the nine-stop route with 14,064 zero-mismatch samples. Co-authored-by: OpenAI Codex <codex@openai.com>
579 lines
22 KiB
C#
579 lines
22 KiB
C#
using AcDream.App.Diagnostics;
|
|
using AcDream.App.Input;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Rendering.Scene;
|
|
using AcDream.App.Rendering.Vfx;
|
|
using AcDream.App.Settings;
|
|
using AcDream.App.Update;
|
|
using AcDream.App.World;
|
|
using AcDream.Core.Combat;
|
|
using AcDream.Core.Lighting;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Selection;
|
|
using AcDream.Core.World;
|
|
using Silk.NET.Input;
|
|
using Silk.NET.OpenGL;
|
|
using Silk.NET.Windowing;
|
|
|
|
namespace AcDream.App.Composition;
|
|
|
|
internal sealed record FrameRootDependencies(
|
|
RuntimeOptions Options,
|
|
GL Gl,
|
|
IWindow Window,
|
|
IInputContext Input,
|
|
WorldTimeService WorldTime,
|
|
WeatherSystem Weather,
|
|
LightManager Lighting,
|
|
WorldEnvironmentController WorldEnvironment,
|
|
PhysicsEngine PhysicsEngine,
|
|
CellVisibility CellVisibility,
|
|
LocalPlayerModeState PlayerMode,
|
|
LocalPlayerIdentityState PlayerIdentity,
|
|
ChaseCameraInputState ChaseCameraInput,
|
|
LocalPlayerControllerSlot PlayerController,
|
|
LiveWorldOriginState WorldOrigin,
|
|
ParticleVisibilityController ParticleVisibility,
|
|
EntityEffectPoseRegistry EffectPoses,
|
|
WorldRenderRangeState RenderRange,
|
|
RuntimeSettingsController Settings,
|
|
DisplayFramePacingController DisplayFramePacing,
|
|
WorldSceneDebugState WorldSceneDebugState,
|
|
RetailAlphaQueue RetailAlphaQueue,
|
|
FrameProfiler FrameProfiler,
|
|
bool FrameDiagnosticsEnabled,
|
|
IRenderFrameDiagnosticLog RenderDiagnosticLog,
|
|
DebugVmRenderFactsPublisher DebugVmRenderFacts,
|
|
IInputCaptureSource InputCapture,
|
|
DispatcherCameraInputSource CameraInput,
|
|
SelectionState Selection,
|
|
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> Animations,
|
|
UpdateFrameClock UpdateClock,
|
|
CombatState Combat,
|
|
GameFrameGraphSlot FrameGraphs,
|
|
Action<string> Log);
|
|
|
|
internal sealed record FrameRootResult(
|
|
UpdateFrameOrchestrator Update,
|
|
RenderFrameOrchestrator Render,
|
|
FrameRootRuntimeBindings RuntimeBindings,
|
|
IDisposable FrameGraphPublication,
|
|
AcDream.App.Net.LiveSessionHost SessionHost);
|
|
|
|
internal interface IGameWindowFrameRootPublication
|
|
{
|
|
void PublishFrameRoots(FrameRootResult result);
|
|
}
|
|
|
|
internal enum FrameRootCompositionPoint
|
|
{
|
|
RenderResourcesCreated,
|
|
WorldRendererCreated,
|
|
LifecycleAutomationBound,
|
|
RenderRootCreated,
|
|
UpdateRootCreated,
|
|
FrameGraphPublished,
|
|
ResultPublished,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Exact late edges created with the frame roots. Completed detach operations
|
|
/// are removed immediately so shutdown retry never replays them.
|
|
/// </summary>
|
|
internal sealed class FrameRootRuntimeBindings : IDisposable
|
|
{
|
|
private readonly List<(string Name, IDisposable Binding)> _bindings = [];
|
|
private bool _deactivationStarted;
|
|
|
|
public void Adopt(string name, IDisposable binding)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
|
ArgumentNullException.ThrowIfNull(binding);
|
|
ObjectDisposedException.ThrowIf(_deactivationStarted, this);
|
|
_bindings.Add((name, binding));
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_deactivationStarted && _bindings.Count == 0)
|
|
return;
|
|
_deactivationStarted = true;
|
|
|
|
List<Exception>? failures = null;
|
|
for (int i = _bindings.Count - 1; i >= 0; i--)
|
|
{
|
|
(string name, IDisposable binding) = _bindings[i];
|
|
try
|
|
{
|
|
binding.Dispose();
|
|
_bindings.RemoveAt(i);
|
|
}
|
|
catch (Exception failure)
|
|
{
|
|
(failures ??= []).Add(new InvalidOperationException(
|
|
$"Frame-root binding '{name}' did not detach.",
|
|
failure));
|
|
}
|
|
}
|
|
|
|
if (failures is not null)
|
|
{
|
|
throw new AggregateException(
|
|
"Frame-root binding cleanup remains incomplete.",
|
|
failures);
|
|
}
|
|
}
|
|
}
|
|
|
|
internal sealed class FrameRootCompositionPhase
|
|
: IFrameRootCompositionPhase<
|
|
GameWindowPlatformResult<GL, IInputContext>,
|
|
HostInputCameraResult,
|
|
ContentEffectsAudioResult,
|
|
SettingsDevToolsResult,
|
|
WorldRenderResult,
|
|
InteractionRetainedUiResult,
|
|
LivePresentationResult,
|
|
SessionPlayerResult,
|
|
FrameRootResult>
|
|
{
|
|
private readonly FrameRootDependencies _dependencies;
|
|
private readonly IGameWindowFrameRootPublication _publication;
|
|
private readonly Action<FrameRootCompositionPoint>? _faultInjection;
|
|
|
|
public FrameRootCompositionPhase(
|
|
FrameRootDependencies dependencies,
|
|
IGameWindowFrameRootPublication publication,
|
|
Action<FrameRootCompositionPoint>? faultInjection = null)
|
|
{
|
|
_dependencies = dependencies
|
|
?? throw new ArgumentNullException(nameof(dependencies));
|
|
_publication = publication
|
|
?? throw new ArgumentNullException(nameof(publication));
|
|
_faultInjection = faultInjection;
|
|
}
|
|
|
|
public FrameRootResult Compose(
|
|
GameWindowPlatformResult<GL, IInputContext> platform,
|
|
HostInputCameraResult host,
|
|
ContentEffectsAudioResult content,
|
|
SettingsDevToolsResult settings,
|
|
WorldRenderResult world,
|
|
InteractionRetainedUiResult interaction,
|
|
LivePresentationResult live,
|
|
SessionPlayerResult session)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(platform);
|
|
ArgumentNullException.ThrowIfNull(host);
|
|
ArgumentNullException.ThrowIfNull(content);
|
|
ArgumentNullException.ThrowIfNull(settings);
|
|
ArgumentNullException.ThrowIfNull(world);
|
|
ArgumentNullException.ThrowIfNull(interaction);
|
|
ArgumentNullException.ThrowIfNull(live);
|
|
ArgumentNullException.ThrowIfNull(session);
|
|
if (!ReferenceEquals(_dependencies.Gl, platform.Graphics)
|
|
|| !ReferenceEquals(_dependencies.Input, platform.Input))
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Frame-root dependencies do not match the ordered platform result.");
|
|
}
|
|
|
|
var scope = new CompositionAcquisitionScope();
|
|
FrameRootRuntimeBindings? bindings = null;
|
|
bool bindingsOwnedByScope = false;
|
|
try
|
|
{
|
|
FrameRootResult result = ComposeCore(
|
|
host,
|
|
content,
|
|
settings,
|
|
world,
|
|
interaction,
|
|
live,
|
|
session,
|
|
scope,
|
|
ref bindings,
|
|
ref bindingsOwnedByScope);
|
|
scope.Complete();
|
|
return result;
|
|
}
|
|
catch (Exception failure)
|
|
{
|
|
if (bindings is not null && !bindingsOwnedByScope)
|
|
{
|
|
scope.Own(
|
|
"frame-root runtime bindings",
|
|
bindings,
|
|
static value => value.Dispose());
|
|
}
|
|
|
|
scope.RollbackAndThrow(failure);
|
|
throw new System.Diagnostics.UnreachableException();
|
|
}
|
|
}
|
|
|
|
private FrameRootResult ComposeCore(
|
|
HostInputCameraResult host,
|
|
ContentEffectsAudioResult content,
|
|
SettingsDevToolsResult settings,
|
|
WorldRenderResult world,
|
|
InteractionRetainedUiResult interaction,
|
|
LivePresentationResult live,
|
|
SessionPlayerResult session,
|
|
CompositionAcquisitionScope scope,
|
|
ref FrameRootRuntimeBindings? bindings,
|
|
ref bool bindingsOwnedByScope)
|
|
{
|
|
FrameRootDependencies d = _dependencies;
|
|
WorldRenderFoundation foundation = world.Foundation;
|
|
var teleportRenderState =
|
|
new LocalPlayerTeleportRenderStateSource(session.LocalTeleport);
|
|
var renderLoginState = new RenderLoginStateSource(
|
|
d.Options.LiveMode,
|
|
d.PlayerMode);
|
|
var renderFrameGlState = new RenderFrameGlStateController(
|
|
new SilkRenderFrameGlStateApi(d.Gl));
|
|
var renderFrameLivePreparation =
|
|
new RuntimeRenderFrameLivePreparation(
|
|
foundation.TextureCache,
|
|
foundation.MeshAdapter,
|
|
session.WorldReveal,
|
|
teleportRenderState,
|
|
renderLoginState,
|
|
new LiveLoginRevealCellSource(
|
|
live.LiveEntities,
|
|
d.PlayerIdentity),
|
|
live.ParticleRenderer,
|
|
d.FrameProfiler,
|
|
d.FrameDiagnosticsEnabled);
|
|
var renderFrameResources = new RenderFrameResourceController(
|
|
host.GpuFrameFlights,
|
|
new RuntimeRenderFrameBeginResources(
|
|
foundation.TextureCache,
|
|
live.DrawDispatcher,
|
|
live.EnvCellRenderer,
|
|
live.PortalDepthMask,
|
|
foundation.TextRenderer,
|
|
interaction.RetainedUi?.Host.TextRenderer,
|
|
live.ClipFrame,
|
|
foundation.Terrain,
|
|
foundation.SceneLighting),
|
|
new RuntimeRenderFrameClearPhase(
|
|
d.Gl,
|
|
d.WorldTime,
|
|
d.Weather,
|
|
teleportRenderState,
|
|
d.ParticleVisibility,
|
|
host.WorldRenderDiagnostics,
|
|
renderFrameGlState),
|
|
renderFrameLivePreparation);
|
|
Fault(FrameRootCompositionPoint.RenderResourcesCreated);
|
|
|
|
var renderWeatherFrame = new RenderWeatherFrameController(
|
|
d.WorldTime,
|
|
d.Weather);
|
|
var skyPesFrame = new SkyPesFrameController(
|
|
content.ScriptRunner,
|
|
content.ParticleSink,
|
|
d.EffectPoses,
|
|
live.EntityEffects);
|
|
var worldFrameEnvironment =
|
|
new RuntimeWorldFrameEnvironmentPreparation(
|
|
d.Options,
|
|
d.WorldTime,
|
|
d.Lighting,
|
|
live.DrawDispatcher,
|
|
live.EnvCellRenderer,
|
|
foundation.SceneLighting,
|
|
d.RenderRange,
|
|
skyPesFrame);
|
|
var worldRenderFrameBuilder = new WorldRenderFrameBuilder(
|
|
new RuntimeWorldFrameCameraSource(
|
|
host.CameraController,
|
|
session.LocalTeleport),
|
|
new RuntimeWorldFrameVisibilityPreparation(
|
|
live.SelectionScene,
|
|
d.ParticleVisibility,
|
|
foundation.Terrain,
|
|
session.WorldReveal,
|
|
live.EnvCellFrustum),
|
|
new RuntimeWorldFrameSettingsPreview(
|
|
d.Settings,
|
|
content.Audio?.Engine,
|
|
host.CameraController,
|
|
d.DisplayFramePacing),
|
|
new RuntimeWorldFrameRootSource(
|
|
d.PhysicsEngine,
|
|
d.CellVisibility,
|
|
d.PlayerMode,
|
|
d.ChaseCameraInput,
|
|
d.PlayerController,
|
|
d.WorldOrigin),
|
|
worldFrameEnvironment,
|
|
new RuntimeWorldFrameAnimatedEntitySource(
|
|
d.Animations,
|
|
live.StaticAnimationScheduler,
|
|
live.EquippedChildren),
|
|
new RuntimeWorldFrameBuildingSource(
|
|
live.LandblockPipeline,
|
|
d.CellVisibility));
|
|
var terrainDrawDiagnostics = new TerrainDrawDiagnosticsController(
|
|
d.FrameDiagnosticsEnabled,
|
|
host.WorldRenderDiagnostics,
|
|
new RuntimeFramePipelineDiagnosticFactsSource(
|
|
foundation.Terrain,
|
|
live.LandblockPipeline,
|
|
renderFrameLivePreparation,
|
|
live.DrawDispatcher,
|
|
session.Streaming,
|
|
live.LiveEntities,
|
|
live.WorldState),
|
|
d.RenderDiagnosticLog);
|
|
var retailPViewCells = new RetailPViewCellSource(d.CellVisibility);
|
|
var retailPViewPassExecutor = new RetailPViewPassExecutor(
|
|
d.Gl,
|
|
renderFrameGlState,
|
|
new SilkRetailPViewFramebufferSource(d.Window),
|
|
live.ClipFrame,
|
|
foundation.Terrain,
|
|
live.EnvCellRenderer,
|
|
live.DrawDispatcher,
|
|
live.SkyRenderer,
|
|
content.ParticleSystem,
|
|
live.ParticleRenderer,
|
|
live.PortalDepthMask,
|
|
d.RetailAlphaQueue,
|
|
host.WorldRenderDiagnostics,
|
|
terrainDrawDiagnostics);
|
|
var worldSceneDiagnostics = new WorldSceneDiagnosticsController(
|
|
host.WorldRenderDiagnostics,
|
|
new RuntimeWorldScenePViewDiagnosticSource(
|
|
d.PlayerController,
|
|
d.PhysicsEngine,
|
|
d.CellVisibility),
|
|
d.WorldSceneDebugState,
|
|
foundation.DebugLines,
|
|
d.PhysicsEngine,
|
|
d.PlayerMode,
|
|
d.PlayerController,
|
|
d.DebugVmRenderFacts,
|
|
settings.DevTools is not null);
|
|
var worldScenePasses = new WorldScenePassExecutor(
|
|
d.Gl,
|
|
renderFrameGlState,
|
|
live.ClipFrame,
|
|
live.DrawDispatcher,
|
|
live.EnvCellRenderer,
|
|
foundation.Terrain,
|
|
terrainDrawDiagnostics,
|
|
live.SkyRenderer,
|
|
content.ParticleSystem,
|
|
live.ParticleRenderer);
|
|
CurrentRenderSceneOracle? currentRenderSceneOracle =
|
|
interaction.RetainedUi?.Screenshots is not null
|
|
&& d.Options.AutomationArtifactDirectory is not null
|
|
? new CurrentRenderSceneOracle()
|
|
: null;
|
|
RenderSceneShadowComparisonController? renderSceneShadowComparison =
|
|
currentRenderSceneOracle is not null
|
|
&& live.RenderSceneShadow is not null
|
|
? new RenderSceneShadowComparisonController(
|
|
live.RenderSceneShadow,
|
|
currentRenderSceneOracle,
|
|
message => d.Log("[UI-PROBE] " + message),
|
|
acknowledgeDirty: false)
|
|
: null;
|
|
RenderScenePViewFrameProductController? renderFrameProduct =
|
|
live.RenderSceneShadow is not null
|
|
? new RenderScenePViewFrameProductController(
|
|
live.RenderSceneShadow,
|
|
currentRenderSceneOracle,
|
|
message => d.Log("[UI-PROBE] " + message),
|
|
live.DrawDispatcher)
|
|
: null;
|
|
// After G4 the retained product is the production object source.
|
|
// The old dispatcher/selection observer is intentionally detached;
|
|
// automation still compares the independently built PView route list.
|
|
live.DrawDispatcher.SetCurrentRenderSceneObserver(null);
|
|
live.SelectionScene.SetCurrentRenderSceneObserver(null);
|
|
var worldSceneRenderer = new WorldSceneRenderer(
|
|
renderFrameResources,
|
|
renderLoginState,
|
|
d.WorldEnvironment,
|
|
worldRenderFrameBuilder,
|
|
new RuntimeWorldSceneEntitySource(live.WorldState),
|
|
live.SelectionScene,
|
|
d.RetailAlphaQueue,
|
|
d.ParticleVisibility,
|
|
new WorldScenePViewRenderer(
|
|
new RetailPViewRenderer(
|
|
currentRenderSceneOracle,
|
|
renderFrameProduct),
|
|
retailPViewPassExecutor,
|
|
retailPViewPassExecutor),
|
|
retailPViewCells,
|
|
worldScenePasses,
|
|
d.RenderRange,
|
|
worldSceneDiagnostics,
|
|
live.WorldAvailability);
|
|
Fault(FrameRootCompositionPoint.WorldRendererCreated);
|
|
|
|
bindings = new FrameRootRuntimeBindings();
|
|
WorldLifecycleAutomationController? lifecycleAutomation = null;
|
|
if (interaction.RetainedUi?.Screenshots is { } screenshots
|
|
&& d.Options.AutomationArtifactDirectory is { } artifactDirectory)
|
|
{
|
|
var resourceSnapshots =
|
|
new WorldLifecycleResourceSnapshotSource(
|
|
live.WorldState,
|
|
d.Animations,
|
|
live.FrameDiagnostics,
|
|
live.LiveEntities,
|
|
session.Streaming,
|
|
content.ParticleSystem,
|
|
content.ParticleSink,
|
|
live.EntityEffects,
|
|
live.Lights,
|
|
content.ScriptRunner,
|
|
foundation.MeshAdapter,
|
|
foundation.TextureCache,
|
|
live.DrawDispatcher,
|
|
d.FrameProfiler,
|
|
content.Dats,
|
|
foundation.Residency,
|
|
d.PhysicsEngine.DataCache
|
|
?? throw new InvalidOperationException(
|
|
"Lifecycle automation requires the canonical physics cache."),
|
|
currentRenderSceneOracle,
|
|
renderSceneShadowComparison,
|
|
renderFrameProduct);
|
|
lifecycleAutomation =
|
|
new WorldLifecycleAutomationController(
|
|
() => session.WorldReveal.Snapshot,
|
|
() => session.WorldReveal.PortalMaterializationCount,
|
|
resourceSnapshots.Capture,
|
|
screenshots,
|
|
artifactDirectory,
|
|
message => d.Log("[UI-PROBE] " + message));
|
|
bindings.Adopt(
|
|
"world lifecycle automation owner",
|
|
lifecycleAutomation);
|
|
bindings.Adopt(
|
|
"world lifecycle automation binding",
|
|
interaction.LateBindings.Automation.Bind(
|
|
lifecycleAutomation));
|
|
}
|
|
Fault(FrameRootCompositionPoint.LifecycleAutomationBound);
|
|
|
|
IRetainedGameplayUiFrame? retainedGameplayUi =
|
|
d.Options.RetailUi && interaction.RetainedUi is { } retained
|
|
? new RetainedGameplayUiFrame(retained.Runtime, d.Input)
|
|
: null;
|
|
IPrivateFrameScreenshot? privateScreenshot =
|
|
interaction.RetainedUi?.Screenshots is { } frameScreenshots
|
|
? new PrivateFrameScreenshot(frameScreenshots)
|
|
: null;
|
|
var privatePresentation = new PrivatePresentationRenderer(
|
|
new LocalPlayerPortalViewport(
|
|
session.LocalTeleport,
|
|
host.CameraController),
|
|
renderFrameResources,
|
|
new PrivateEntityViewportFrameGroup(
|
|
live.PaperdollPresenter,
|
|
live.CreatureAppraisalPresenter),
|
|
retainedGameplayUi,
|
|
settings.DevTools?.Presenter,
|
|
privateScreenshot);
|
|
var framePreparation = new RenderFramePreparationController(
|
|
renderFrameResources,
|
|
settings.DevTools?.Presenter,
|
|
renderWeatherFrame);
|
|
IRenderFramePostDiagnosticsPhase postDiagnostics =
|
|
renderSceneShadowComparison is not null
|
|
&& lifecycleAutomation is not null
|
|
? new SerialRenderFramePostDiagnosticsPhase(
|
|
renderSceneShadowComparison,
|
|
lifecycleAutomation)
|
|
: (IRenderFramePostDiagnosticsPhase?)lifecycleAutomation
|
|
?? NullRenderFramePostDiagnosticsPhase.Instance;
|
|
var renderFrame = new RenderFrameOrchestrator(
|
|
host.GpuFrameFlights,
|
|
new FrameProfilerGpuMeasurement(d.FrameProfiler, d.Gl),
|
|
framePreparation,
|
|
worldSceneRenderer,
|
|
privatePresentation,
|
|
live.FrameDiagnostics,
|
|
postDiagnostics,
|
|
(IRenderFrameFailureRecovery?)settings.DevTools?.Presenter
|
|
?? NullRenderFrameFailureRecovery.Instance);
|
|
Fault(FrameRootCompositionPoint.RenderRootCreated);
|
|
|
|
var liveFrameCoordinator = new RetailLiveFrameCoordinator(
|
|
session.LiveObjectFrame,
|
|
live.WorldState,
|
|
session.LiveSession,
|
|
session.LocalPlayerFrame,
|
|
session.LiveSpatialReconciler,
|
|
live.WorldAvailability,
|
|
live.RenderSceneShadow?.LiveProjections);
|
|
var cameraFrame = new CameraFrameController(
|
|
host.CameraController,
|
|
d.InputCapture,
|
|
d.CameraInput,
|
|
session.LocalPlayerFrameRuntime,
|
|
d.ChaseCameraInput,
|
|
session.LocalPlayerFrame,
|
|
session.LiveSpatialReconciler,
|
|
new AcDream.App.Combat.CombatCameraTargetSource(
|
|
d.Settings,
|
|
d.Combat,
|
|
d.Selection,
|
|
live.SelectionQuery));
|
|
var updateFrame = new UpdateFrameOrchestrator(
|
|
new LiveEntityTeardownFramePhase(live.LiveEntities),
|
|
new ConsoleUpdateFrameFailureSink(),
|
|
d.UpdateClock,
|
|
new PhysicsScriptClockPublisher(content.ScriptRunner),
|
|
session.StreamingFrame,
|
|
session.GameplayInput,
|
|
liveFrameCoordinator,
|
|
new LiveEntityLivenessFramePhase(
|
|
session.Liveness,
|
|
new StopwatchClientMonotonicTimeSource()),
|
|
session.LocalTeleport,
|
|
new PlayerModeAutoEntryFramePhase(session.PlayerModeAutoEntry),
|
|
cameraFrame,
|
|
new RenderSceneUpdateCommitPhase(live.RenderSceneShadow),
|
|
live.WorldAvailability);
|
|
Fault(FrameRootCompositionPoint.UpdateRootCreated);
|
|
|
|
var bindingsLease = scope.Own(
|
|
"frame-root runtime bindings",
|
|
bindings,
|
|
static value => value.Dispose());
|
|
bindingsOwnedByScope = true;
|
|
IDisposable frameGraphPublication = d.FrameGraphs.PublishOwned(
|
|
updateFrame,
|
|
renderFrame);
|
|
var graphLease = scope.Own(
|
|
"game frame graph publication",
|
|
frameGraphPublication,
|
|
static value => value.Dispose());
|
|
Fault(FrameRootCompositionPoint.FrameGraphPublished);
|
|
|
|
var result = new FrameRootResult(
|
|
updateFrame,
|
|
renderFrame,
|
|
bindings,
|
|
frameGraphPublication,
|
|
session.SessionHost);
|
|
_publication.PublishFrameRoots(result);
|
|
graphLease.Transfer();
|
|
bindingsLease.Transfer();
|
|
Fault(FrameRootCompositionPoint.ResultPublished);
|
|
return result;
|
|
}
|
|
|
|
private void Fault(FrameRootCompositionPoint point) =>
|
|
_faultInjection?.Invoke(point);
|
|
}
|