test(app): add canonical connected soak snapshots
Make scripted lifecycle checkpoints acknowledged post-diagnostics render barriers, capture the exact frame outcome beside canonical resource ownership, and harden the nine-stop route with ordered same-location cache and lifetime gates without weakening process residency thresholds. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
2862622ba2
commit
bca4148739
17 changed files with 995 additions and 52 deletions
|
|
@ -55,7 +55,6 @@ internal sealed record FrameRootDependencies(
|
|||
internal sealed record FrameRootResult(
|
||||
UpdateFrameOrchestrator Update,
|
||||
RenderFrameOrchestrator Render,
|
||||
WorldLifecycleAutomationController? LifecycleAutomation,
|
||||
FrameRootRuntimeBindings RuntimeBindings,
|
||||
IDisposable FrameGraphPublication,
|
||||
AcDream.App.Net.LiveSessionHost SessionHost);
|
||||
|
|
@ -420,7 +419,10 @@ internal sealed class FrameRootCompositionPhase
|
|||
artifactDirectory,
|
||||
message => d.Log("[UI-PROBE] " + message));
|
||||
bindings.Adopt(
|
||||
"world lifecycle automation",
|
||||
"world lifecycle automation owner",
|
||||
lifecycleAutomation);
|
||||
bindings.Adopt(
|
||||
"world lifecycle automation binding",
|
||||
interaction.LateBindings.Automation.Bind(
|
||||
lifecycleAutomation));
|
||||
}
|
||||
|
|
@ -453,6 +455,8 @@ internal sealed class FrameRootCompositionPhase
|
|||
worldSceneRenderer,
|
||||
privatePresentation,
|
||||
live.FrameDiagnostics,
|
||||
(IRenderFramePostDiagnosticsPhase?)lifecycleAutomation
|
||||
?? NullRenderFramePostDiagnosticsPhase.Instance,
|
||||
(IRenderFrameFailureRecovery?)settings.DevTools?.Presenter
|
||||
?? NullRenderFrameFailureRecovery.Instance);
|
||||
Fault(FrameRootCompositionPoint.RenderRootCreated);
|
||||
|
|
@ -509,7 +513,6 @@ internal sealed class FrameRootCompositionPhase
|
|||
var result = new FrameRootResult(
|
||||
updateFrame,
|
||||
renderFrame,
|
||||
lifecycleAutomation,
|
||||
bindings,
|
||||
frameGraphPublication,
|
||||
session.SessionHost);
|
||||
|
|
|
|||
|
|
@ -446,14 +446,25 @@ internal sealed class DeferredWorldLifecycleAutomationRuntime
|
|||
return new ExpectedOwnerBinding<IRetailUiAutomationRuntime>(target, Release);
|
||||
}
|
||||
|
||||
public bool TryWriteCheckpoint(string name, out string error)
|
||||
public bool TryRequestCheckpoint(
|
||||
string name,
|
||||
out IRetailUiAutomationCheckpoint? checkpoint,
|
||||
out string error)
|
||||
{
|
||||
if (!_deactivated && _target is { } target)
|
||||
return target.TryWriteCheckpoint(name, out error);
|
||||
return target.TryRequestCheckpoint(name, out checkpoint, out error);
|
||||
checkpoint = null;
|
||||
error = "world lifecycle automation is not bound";
|
||||
return false;
|
||||
}
|
||||
|
||||
public void CancelCheckpoint(IRetailUiAutomationCheckpoint checkpoint)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(checkpoint);
|
||||
if (!_deactivated && _target is { } target)
|
||||
target.CancelCheckpoint(checkpoint);
|
||||
}
|
||||
|
||||
public bool TryRequestScreenshot(string name, out string error)
|
||||
{
|
||||
if (!_deactivated && _target is { } target)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Text.Json;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.UI.Testing;
|
||||
|
||||
|
|
@ -71,14 +72,73 @@ internal sealed record WorldLifecycleCheckpoint(
|
|||
DateTime TimestampUtc,
|
||||
int ProcessId,
|
||||
WorldRevealLifecycleSnapshot Reveal,
|
||||
RenderFrameOutcome Render,
|
||||
WorldLifecycleResourceSnapshot Resources);
|
||||
|
||||
internal sealed class WorldLifecycleCheckpointRequest :
|
||||
IRetailUiAutomationCheckpoint
|
||||
{
|
||||
private readonly object _sync = new();
|
||||
private RetailUiAutomationCheckpointStatus _status =
|
||||
RetailUiAutomationCheckpointStatus.Pending;
|
||||
private string? _error;
|
||||
|
||||
public WorldLifecycleCheckpointRequest(object owner, int sequence, string name)
|
||||
{
|
||||
Owner = owner ?? throw new ArgumentNullException(nameof(owner));
|
||||
Sequence = sequence;
|
||||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
internal object Owner { get; }
|
||||
public int Sequence { get; }
|
||||
public string Name { get; }
|
||||
|
||||
public RetailUiAutomationCheckpointStatus Status
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync)
|
||||
return _status;
|
||||
}
|
||||
}
|
||||
|
||||
public string? Error
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync)
|
||||
return _error;
|
||||
}
|
||||
}
|
||||
|
||||
internal bool TrySetTerminal(
|
||||
RetailUiAutomationCheckpointStatus status,
|
||||
string? error)
|
||||
{
|
||||
if (status == RetailUiAutomationCheckpointStatus.Pending)
|
||||
throw new ArgumentOutOfRangeException(nameof(status));
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (_status != RetailUiAutomationCheckpointStatus.Pending)
|
||||
return false;
|
||||
_status = status;
|
||||
_error = error;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Diagnostic-only runtime seam used by production retained-UI scripts. It
|
||||
/// writes one structured checkpoint at explicit script edges and delegates GL
|
||||
/// capture to the render-thread screenshot owner.
|
||||
/// </summary>
|
||||
internal sealed class WorldLifecycleAutomationController : IRetailUiAutomationRuntime
|
||||
internal sealed class WorldLifecycleAutomationController :
|
||||
IRetailUiAutomationRuntime,
|
||||
IRenderFramePostDiagnosticsPhase,
|
||||
IDisposable
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
|
|
@ -87,16 +147,21 @@ internal sealed class WorldLifecycleAutomationController : IRetailUiAutomationRu
|
|||
|
||||
private readonly Func<WorldRevealLifecycleSnapshot> _getReveal;
|
||||
private readonly Func<int> _getPortalMaterializationCount;
|
||||
private readonly Func<WorldLifecycleResourceSnapshot> _captureResources;
|
||||
private readonly Func<RenderFrameOutcome, WorldLifecycleResourceSnapshot>
|
||||
_captureResources;
|
||||
private readonly FrameScreenshotController _screenshots;
|
||||
private readonly string _artifactDirectory;
|
||||
private readonly Action<string> _log;
|
||||
private readonly object _requestOwner = new();
|
||||
private readonly object _sync = new();
|
||||
private readonly Queue<WorldLifecycleCheckpointRequest> _requests = [];
|
||||
private int _sequence;
|
||||
private bool _disposed;
|
||||
|
||||
public WorldLifecycleAutomationController(
|
||||
Func<WorldRevealLifecycleSnapshot> getReveal,
|
||||
Func<int> getPortalMaterializationCount,
|
||||
Func<WorldLifecycleResourceSnapshot> captureResources,
|
||||
Func<RenderFrameOutcome, WorldLifecycleResourceSnapshot> captureResources,
|
||||
FrameScreenshotController screenshots,
|
||||
string artifactDirectory,
|
||||
Action<string>? log = null)
|
||||
|
|
@ -116,35 +181,127 @@ internal sealed class WorldLifecycleAutomationController : IRetailUiAutomationRu
|
|||
public bool IsWorldViewportVisible => _getReveal().WorldViewportObserved;
|
||||
public int PortalMaterializationCount => _getPortalMaterializationCount();
|
||||
|
||||
public bool TryWriteCheckpoint(string name, out string error)
|
||||
public bool TryRequestCheckpoint(
|
||||
string name,
|
||||
out IRetailUiAutomationCheckpoint? checkpoint,
|
||||
out string error)
|
||||
{
|
||||
checkpoint = null;
|
||||
if (!AutomationArtifactName.TryValidate(name, out error))
|
||||
return false;
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
error = $"checkpoint '{name}' was rejected because world lifecycle automation is shutting down";
|
||||
return false;
|
||||
}
|
||||
|
||||
var request = new WorldLifecycleCheckpointRequest(
|
||||
_requestOwner,
|
||||
checked(++_sequence),
|
||||
name);
|
||||
_requests.Enqueue(request);
|
||||
checkpoint = request;
|
||||
error = string.Empty;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public void CancelCheckpoint(IRetailUiAutomationCheckpoint checkpoint)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(checkpoint);
|
||||
if (checkpoint is not WorldLifecycleCheckpointRequest request
|
||||
|| !ReferenceEquals(request.Owner, _requestOwner))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"The checkpoint acknowledgement is not owned by this automation runtime.",
|
||||
nameof(checkpoint));
|
||||
}
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
request.TrySetTerminal(
|
||||
RetailUiAutomationCheckpointStatus.Cancelled,
|
||||
$"checkpoint '{request.Name}' was cancelled before render-frame capture");
|
||||
}
|
||||
}
|
||||
|
||||
public void Process(RenderFrameInput input, RenderFrameOutcome outcome)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
while (_requests.Count != 0)
|
||||
{
|
||||
WorldLifecycleCheckpointRequest request = _requests.Dequeue();
|
||||
if (request.Status != RetailUiAutomationCheckpointStatus.Pending)
|
||||
continue;
|
||||
WriteCheckpoint(request, outcome);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteCheckpoint(
|
||||
WorldLifecycleCheckpointRequest request,
|
||||
RenderFrameOutcome outcome)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(_artifactDirectory);
|
||||
var checkpoint = new WorldLifecycleCheckpoint(
|
||||
Sequence: checked(++_sequence),
|
||||
Name: name,
|
||||
Sequence: request.Sequence,
|
||||
Name: request.Name,
|
||||
TimestampUtc: DateTime.UtcNow,
|
||||
ProcessId: Environment.ProcessId,
|
||||
Reveal: _getReveal(),
|
||||
Resources: _captureResources());
|
||||
Render: outcome,
|
||||
Resources: _captureResources(outcome));
|
||||
string json = JsonSerializer.Serialize(checkpoint, JsonOptions);
|
||||
string timelinePath = Path.Combine(_artifactDirectory, "world-lifecycle.checkpoints.jsonl");
|
||||
File.AppendAllText(timelinePath, json + Environment.NewLine);
|
||||
string timelinePath = Path.Combine(
|
||||
_artifactDirectory,
|
||||
"world-lifecycle.checkpoints.jsonl");
|
||||
File.WriteAllText(
|
||||
Path.Combine(_artifactDirectory, $"checkpoint-{name}.json"),
|
||||
Path.Combine(
|
||||
_artifactDirectory,
|
||||
$"checkpoint-{request.Name}.json"),
|
||||
json + Environment.NewLine);
|
||||
_log($"[world-gate] checkpoint name={name} sequence={checkpoint.Sequence} path={timelinePath}");
|
||||
error = string.Empty;
|
||||
return true;
|
||||
// Timeline publication is the commit edge: a visible row always
|
||||
// has its complete named checkpoint artifact beside it.
|
||||
File.AppendAllText(timelinePath, json + Environment.NewLine);
|
||||
request.TrySetTerminal(
|
||||
RetailUiAutomationCheckpointStatus.Succeeded,
|
||||
error: null);
|
||||
_log(
|
||||
$"[world-gate] checkpoint name={request.Name} "
|
||||
+ $"sequence={request.Sequence} path={timelinePath}");
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
error = $"checkpoint '{name}' failed: {exception.Message}";
|
||||
return false;
|
||||
request.TrySetTerminal(
|
||||
RetailUiAutomationCheckpointStatus.Failed,
|
||||
$"checkpoint '{request.Name}' sequence {request.Sequence} failed: "
|
||||
+ exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
while (_requests.Count != 0)
|
||||
{
|
||||
WorldLifecycleCheckpointRequest request = _requests.Dequeue();
|
||||
request.TrySetTerminal(
|
||||
RetailUiAutomationCheckpointStatus.Cancelled,
|
||||
$"checkpoint '{request.Name}' was cancelled during world lifecycle automation shutdown");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
|||
?? throw new ArgumentNullException(nameof(frameProfiler));
|
||||
}
|
||||
|
||||
public WorldLifecycleResourceSnapshot Capture()
|
||||
public WorldLifecycleResourceSnapshot Capture(RenderFrameOutcome outcome)
|
||||
{
|
||||
ObjectMeshManager meshManager = _meshes.MeshManager
|
||||
?? throw new InvalidOperationException(
|
||||
|
|
@ -82,8 +82,8 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
|||
LoadedLandblocks: _world.LoadedLandblockIds.Count,
|
||||
WorldEntities: _world.Entities.Count,
|
||||
AnimatedEntities: _animations.Count,
|
||||
VisibleLandblocks: render.VisibleLandblocks,
|
||||
TotalLandblocks: render.TotalLandblocks,
|
||||
VisibleLandblocks: outcome.World.VisibleLandblocks,
|
||||
TotalLandblocks: outcome.World.TotalLandblocks,
|
||||
LiveEntities: _liveEntities.Count,
|
||||
MaterializedLiveEntities: _liveEntities.MaterializedCount,
|
||||
PendingLiveTeardowns: _liveEntities.PendingTeardownCount,
|
||||
|
|
|
|||
|
|
@ -103,7 +103,6 @@ public sealed class GameWindow :
|
|||
private LivePresentationRuntimeBindings? _livePresentationBindings;
|
||||
private SessionPlayerRuntimeBindings? _sessionPlayerBindings;
|
||||
private AcDream.App.Diagnostics.FrameScreenshotController? _frameScreenshots;
|
||||
private AcDream.App.Diagnostics.WorldLifecycleAutomationController? _worldLifecycleAutomation;
|
||||
private FrameRootRuntimeBindings? _frameRootBindings;
|
||||
private IDisposable? _frameGraphPublication;
|
||||
private AcDream.App.Rendering.GpuFrameFlightController? _gpuFrameFlights;
|
||||
|
|
@ -1048,15 +1047,13 @@ public sealed class GameWindow :
|
|||
FrameRootResult result)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(result);
|
||||
if (_worldLifecycleAutomation is not null
|
||||
|| _frameRootBindings is not null
|
||||
if (_frameRootBindings is not null
|
||||
|| _frameGraphPublication is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The GameWindow composition shell already owns frame roots.");
|
||||
}
|
||||
|
||||
_worldLifecycleAutomation = result.LifecycleAutomation;
|
||||
_frameRootBindings = result.RuntimeBindings;
|
||||
_frameGraphPublication = result.FrameGraphPublication;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,25 @@ internal interface IRenderFrameDiagnosticsPhase
|
|||
void Publish(RenderFrameInput input, RenderFrameOutcome outcome);
|
||||
}
|
||||
|
||||
internal interface IRenderFramePostDiagnosticsPhase
|
||||
{
|
||||
void Process(RenderFrameInput input, RenderFrameOutcome outcome);
|
||||
}
|
||||
|
||||
internal sealed class NullRenderFramePostDiagnosticsPhase :
|
||||
IRenderFramePostDiagnosticsPhase
|
||||
{
|
||||
public static NullRenderFramePostDiagnosticsPhase Instance { get; } = new();
|
||||
|
||||
private NullRenderFramePostDiagnosticsPhase()
|
||||
{
|
||||
}
|
||||
|
||||
public void Process(RenderFrameInput input, RenderFrameOutcome outcome)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes presentation transactions which may have opened before a later
|
||||
/// render phase failed. Recovery is idempotent so failures before the
|
||||
|
|
@ -102,6 +121,7 @@ internal sealed class RenderFrameOrchestrator : IGameRenderFrameRoot
|
|||
private readonly IWorldSceneFramePhase _world;
|
||||
private readonly IPrivatePresentationFramePhase _presentation;
|
||||
private readonly IRenderFrameDiagnosticsPhase _diagnostics;
|
||||
private readonly IRenderFramePostDiagnosticsPhase _postDiagnostics;
|
||||
private readonly IRenderFrameFailureRecovery _recovery;
|
||||
|
||||
public RenderFrameOrchestrator(
|
||||
|
|
@ -110,6 +130,7 @@ internal sealed class RenderFrameOrchestrator : IGameRenderFrameRoot
|
|||
IWorldSceneFramePhase world,
|
||||
IPrivatePresentationFramePhase presentation,
|
||||
IRenderFrameDiagnosticsPhase diagnostics,
|
||||
IRenderFramePostDiagnosticsPhase postDiagnostics,
|
||||
IRenderFrameFailureRecovery recovery)
|
||||
{
|
||||
_lifetime = lifetime ?? throw new ArgumentNullException(nameof(lifetime));
|
||||
|
|
@ -117,6 +138,8 @@ internal sealed class RenderFrameOrchestrator : IGameRenderFrameRoot
|
|||
_world = world ?? throw new ArgumentNullException(nameof(world));
|
||||
_presentation = presentation ?? throw new ArgumentNullException(nameof(presentation));
|
||||
_diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
|
||||
_postDiagnostics = postDiagnostics
|
||||
?? throw new ArgumentNullException(nameof(postDiagnostics));
|
||||
_recovery = recovery ?? throw new ArgumentNullException(nameof(recovery));
|
||||
}
|
||||
|
||||
|
|
@ -132,6 +155,7 @@ internal sealed class RenderFrameOrchestrator : IGameRenderFrameRoot
|
|||
_presentation.Render(input, world);
|
||||
var outcome = new RenderFrameOutcome(world, presentation);
|
||||
_diagnostics.Publish(input, outcome);
|
||||
_postDiagnostics.Process(input, outcome);
|
||||
return outcome;
|
||||
}
|
||||
catch (Exception error)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,22 @@ using AcDream.UI.Abstractions.Input;
|
|||
|
||||
namespace AcDream.App.UI.Testing;
|
||||
|
||||
public enum RetailUiAutomationCheckpointStatus
|
||||
{
|
||||
Pending,
|
||||
Succeeded,
|
||||
Failed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
public interface IRetailUiAutomationCheckpoint
|
||||
{
|
||||
int Sequence { get; }
|
||||
string Name { get; }
|
||||
RetailUiAutomationCheckpointStatus Status { get; }
|
||||
string? Error { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Narrow bridge from retained-UI scripts to render/world lifecycle
|
||||
/// diagnostics. Implementations run on the same update/render thread as the
|
||||
|
|
@ -17,7 +33,11 @@ public interface IRetailUiAutomationRuntime
|
|||
bool IsWorldReady { get; }
|
||||
bool IsWorldViewportVisible { get; }
|
||||
int PortalMaterializationCount { get; }
|
||||
bool TryWriteCheckpoint(string name, out string error);
|
||||
bool TryRequestCheckpoint(
|
||||
string name,
|
||||
out IRetailUiAutomationCheckpoint? checkpoint,
|
||||
out string error);
|
||||
void CancelCheckpoint(IRetailUiAutomationCheckpoint checkpoint);
|
||||
bool TryRequestScreenshot(string name, out string error);
|
||||
bool IsScreenshotComplete(string name);
|
||||
}
|
||||
|
|
@ -40,6 +60,8 @@ public sealed class RetailUiAutomationScriptRunner : IDisposable
|
|||
private readonly HashSet<InputAction> _heldInputs = new();
|
||||
private readonly bool _dumpOnStart;
|
||||
private readonly string? _loadError;
|
||||
private IRetailUiAutomationCheckpoint? _checkpoint;
|
||||
private int _checkpointCommandIndex = -1;
|
||||
private int _index;
|
||||
private int _activeIndex = -1;
|
||||
private double _commandElapsedMs;
|
||||
|
|
@ -405,8 +427,47 @@ public sealed class RetailUiAutomationScriptRunner : IDisposable
|
|||
return Stop(command, "usage: checkpoint <name>");
|
||||
if (_runtime is null)
|
||||
return Stop(command, "world lifecycle automation is unavailable");
|
||||
return _runtime.TryWriteCheckpoint(command.Parts[1], out string error)
|
||||
|| Stop(command, error);
|
||||
|
||||
if (_checkpoint is null)
|
||||
{
|
||||
if (!_runtime.TryRequestCheckpoint(
|
||||
command.Parts[1],
|
||||
out IRetailUiAutomationCheckpoint? checkpoint,
|
||||
out string error))
|
||||
{
|
||||
return Stop(command, error);
|
||||
}
|
||||
|
||||
if (checkpoint is null)
|
||||
{
|
||||
return Stop(
|
||||
command,
|
||||
"world lifecycle automation accepted a checkpoint without returning its acknowledgement");
|
||||
}
|
||||
|
||||
_checkpoint = checkpoint;
|
||||
_checkpointCommandIndex = _index;
|
||||
}
|
||||
|
||||
if (_checkpointCommandIndex != _index)
|
||||
{
|
||||
return Stop(
|
||||
command,
|
||||
"checkpoint acknowledgement belongs to a different script command");
|
||||
}
|
||||
|
||||
RetailUiAutomationCheckpointStatus status = _checkpoint.Status;
|
||||
if (status == RetailUiAutomationCheckpointStatus.Pending)
|
||||
return false;
|
||||
|
||||
string? terminalError = _checkpoint.Error;
|
||||
_checkpoint = null;
|
||||
_checkpointCommandIndex = -1;
|
||||
return status == RetailUiAutomationCheckpointStatus.Succeeded
|
||||
|| Stop(
|
||||
command,
|
||||
terminalError
|
||||
?? $"checkpoint '{command.Parts[1]}' ended with {status}");
|
||||
}
|
||||
|
||||
private bool DoScreenshot(ScriptCommand command)
|
||||
|
|
@ -476,6 +537,12 @@ public sealed class RetailUiAutomationScriptRunner : IDisposable
|
|||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
if (_checkpoint is not null)
|
||||
{
|
||||
_runtime?.CancelCheckpoint(_checkpoint);
|
||||
_checkpoint = null;
|
||||
_checkpointCommandIndex = -1;
|
||||
}
|
||||
ReleaseHeldInputs();
|
||||
_disposed = true;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue