Persist shared profile on clean shutdown

This commit is contained in:
gamer147
2026-07-24 23:55:06 -04:00
parent 0a4e200876
commit 1828701872
9 changed files with 370 additions and 13 deletions

View File

@@ -348,9 +348,11 @@ public sealed class SharedProfile
private uint[] _extendedSelectorCounts = Array.Empty<uint>();
private uint[] _extendedValues = Array.Empty<uint>();
private uint[] _reservedTail = new uint[SharedProfilePayloadCodec.ReservedTailDwordCount];
private uint _accumulatedPlaySeconds;
public IReadOnlyDictionary<int, uint> IntegerCells => _integerCells;
public IReadOnlyDictionary<int, string> StringCells => _stringCells;
public uint AccumulatedPlaySeconds => _accumulatedPlaySeconds;
public ReadTextDatabase ReadText { get; } = new();
/// <summary>The engine setting manipulated by opcodes 0x1ca/0x1cb.</summary>
public bool ReadMessageSkipEnabled { get; set; }
@@ -388,10 +390,12 @@ public sealed class SharedProfile
if (document is null)
{
ClearSharedPayload();
_accumulatedPlaySeconds = 0;
}
else
{
Replace(SharedProfilePayloadCodec.Decode(document.Payload, document.Metadata));
_accumulatedPlaySeconds = document.Metadata.AccumulatedPlaySeconds;
}
bool readTextLoaded = ReadText.Load(store);
return document is not null || readTextLoaded;
@@ -406,6 +410,7 @@ public sealed class SharedProfile
NativeSaveMetadata metadata = store.Identity.CreateMetadata(timestamp, accumulatedPlaySeconds);
byte[] payload = SharedProfilePayloadCodec.Encode(Snapshot(), metadata);
store.SaveShared(payload, timestamp, accumulatedPlaySeconds);
_accumulatedPlaySeconds = accumulatedPlaySeconds;
ReadText.Save(store);
}
@@ -436,6 +441,7 @@ public sealed class SharedProfile
ClearSharedPayload();
ReadText.Clear();
ReadMessageSkipEnabled = false;
_accumulatedPlaySeconds = 0;
}
private void ClearSharedPayload()

View File

@@ -8,6 +8,19 @@ namespace Age.Engine.Vm;
/// <summary>A stable identity/snapshot of the exact script frame currently executing.</summary>
public sealed record DebugFrameSnapshot(long FrameId, string CurrentScript, IReadOnlyList<string> CallStack);
public enum SharedProfileShutdownFlushOutcome
{
Saved,
AlreadyHandled,
Suppressed,
StoreUnavailable,
Failed,
}
public readonly record struct SharedProfileShutdownFlushResult(
SharedProfileShutdownFlushOutcome Outcome,
string? Error = null);
public sealed class VirtualMachine
{
private const long NoJump = 0xFFFFFFFF;
@@ -34,6 +47,7 @@ public sealed class VirtualMachine
private readonly ITraceSink _sink;
private readonly object _interactiveLock = new();
private readonly object _debugControlLock = new();
private readonly object _sharedProfileShutdownLock = new();
private readonly List<string> _activeFrameNames = new();
private readonly List<ExecFrame> _activeExecutionFrames = new();
private ExecFrame? _saveResumeFrame;
@@ -61,6 +75,8 @@ public sealed class VirtualMachine
private long _autoMessageTime1Ms = 2000;
private bool _autoVoicePending;
private bool _initialRootRun = true;
private volatile bool _stopRequested;
private bool _sharedProfileShutdownHandled;
private volatile bool _messageSkipEnabled;
private volatile bool _messageSkipServiceActive;
private volatile bool _advSkipServiceEnabled;
@@ -135,9 +151,48 @@ public sealed class VirtualMachine
_sink = sink ?? NullTraceSink.Instance; TextHistory = textHistory ?? new AdvTextHistory();
_sharedProfile = sharedProfile ?? new SharedProfile();
_nativeDatStore = nativeDatStore;
_accumulatedPlaySeconds = _sharedProfile.AccumulatedPlaySeconds;
_sessionStartTimestamp = System.Diagnostics.Stopwatch.GetTimestamp();
}
/// <summary>Request a clean stop at the next opcode boundary.</summary>
public void RequestStop() => _stopRequested = true;
/// <summary>
/// Match AGE's accepted-WM_CLOSE shared-profile lifecycle. The native NoSaveDat setting gates only
/// this shutdown write; numbered-save opcode 0x19e continues to flush shared state independently.
/// Repeated frontend teardown notifications are handled without rotating backups more than once.
/// </summary>
public SharedProfileShutdownFlushResult FlushSharedProfileOnShutdown()
{
lock (_sharedProfileShutdownLock)
{
if (_sharedProfileShutdownHandled)
return new(SharedProfileShutdownFlushOutcome.AlreadyHandled);
_sharedProfileShutdownHandled = true;
if (_o.NoSaveDat)
return new(SharedProfileShutdownFlushOutcome.Suppressed);
if (_nativeDatStore == null)
return new(SharedProfileShutdownFlushOutcome.StoreUnavailable);
try
{
_sharedProfile.Save(
_nativeDatStore,
NativeSystemTime.FromLocalDateTime(DateTime.Now),
AccumulatedPlaySeconds());
return new(SharedProfileShutdownFlushOutcome.Saved);
}
catch (Exception error) when (
error is IOException or UnauthorizedAccessException or InvalidDataException
or ArgumentOutOfRangeException or OverflowException)
{
return new(SharedProfileShutdownFlushOutcome.Failed, error.Message);
}
}
}
/// <summary>Queue global writes and return only the identified active frame at its next opcode boundary.
/// Writes are copied here and applied by the VM thread before another opcode executes.</summary>
public bool TryRequestDebugFrameReturn(long frameId, IReadOnlyDictionary<int, long> globalWrites)
@@ -721,6 +776,7 @@ public sealed class VirtualMachine
{
while (pc >= 0 && pc < frame.Script.Instructions.Count)
{
if (_stopRequested) { outcome = FrameOutcome.ExitRequested; break; }
if (Steps >= _o.MaxSteps) { HaltReason ??= "STEP-LIMIT"; outcome = FrameOutcome.Halted; break; }
Steps++;
frame.Pc = pc;
@@ -1040,6 +1096,7 @@ public sealed class VirtualMachine
_cur.CallStack.Add(HOTSPOT_RETURN);
while (pc >= 0 && pc < _cur.Script.Instructions.Count)
{
if (_stopRequested) break;
if (Steps >= _o.MaxSteps) { HaltReason ??= "STEP-LIMIT"; break; }
Steps++;
_cur.Pc = pc;

View File

@@ -14,7 +14,9 @@ namespace Age.Engine.Vm;
/// <paramref name="AutoFreeTextures"/>, controls the optional all-surface release before numbered load.</param>
/// <param name="AutoFreeTextures">Native set:AutoFreeTex profile setting. Himegari defaults this off,
/// so initialized system textures survive a numbered load unless an explicit reload record replaces them.</param>
/// <param name="NoSaveDat">Native set:NoSaveDat profile setting. This suppresses the clean-shutdown
/// shared SAVE.DAT/RT.DAT flush only; successful numbered saves still flush both shared files.</param>
public sealed record VmOptions(int EmitCap = 2, long MaxSteps = 2_000_000, int CallDepthCap = 64,
bool HaltAtWaitForInput = false, bool IgnoreExitRequests = false,
int NativeStringCodePage = 932, bool CreateObject = true,
bool AutoFreeTextures = false);
bool AutoFreeTextures = false, bool NoSaveDat = false);