Complete native numbered-save round trips
This commit is contained in:
@@ -28,6 +28,8 @@ public class NativeNumberedSaveCodecTests
|
||||
new NativeSavedGfxObject(0xcf08,
|
||||
Enumerable.Range(0, NativeNumberedSaveState.GfxRecordSize)
|
||||
.Select(i => unchecked((byte)i)).ToArray()),
|
||||
new NativeSavedGfxObject(0xcf09,
|
||||
Enumerable.Repeat((byte)0xa5, NativeNumberedSaveState.GfxRecordSize).ToArray()),
|
||||
],
|
||||
RangeTransformFirst = 100,
|
||||
RangeTransformCount = 4,
|
||||
@@ -53,7 +55,16 @@ public class NativeNumberedSaveCodecTests
|
||||
Assert.Equal(state.PointerGlobals, decoded.PointerGlobals);
|
||||
Assert.Equal(state.GfxObjects[0].Handle, decoded.GfxObjects[0].Handle);
|
||||
Assert.Equal(state.GfxObjects[0].Record, decoded.GfxObjects[0].Record);
|
||||
Assert.Equal(state.GfxObjects[1].Handle, decoded.GfxObjects[1].Handle);
|
||||
Assert.Equal(state.GfxObjects[1].Record, decoded.GfxObjects[1].Record);
|
||||
Assert.False(decoded.LegacyTightGfxLayout);
|
||||
Assert.Equal(state.RangeTransformRecord, decoded.RangeTransformRecord);
|
||||
|
||||
const int nativeEntryStride = (1 + NativeNumberedSaveState.GfxRecordSize) * 4;
|
||||
int objectsAt = FindGfxObjects(encoded, 2, 0xcf08);
|
||||
Assert.Equal(0xcf08, BinaryPrimitives.ReadInt32LittleEndian(encoded.AsSpan(objectsAt)));
|
||||
Assert.Equal(0xcf09, BinaryPrimitives.ReadInt32LittleEndian(
|
||||
encoded.AsSpan(objectsAt + nativeEntryStride)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -153,4 +164,15 @@ public class NativeNumberedSaveCodecTests
|
||||
BinaryPrimitives.ReadInt32LittleEndian(
|
||||
state.SurfaceRecords.AsSpan(slot * 20 + 8))));
|
||||
}
|
||||
|
||||
private static int FindGfxObjects(byte[] payload, int count, int firstHandle)
|
||||
{
|
||||
for (int offset = 0; offset <= payload.Length - 12; offset += 4)
|
||||
if (BinaryPrimitives.ReadInt32LittleEndian(payload.AsSpan(offset)) ==
|
||||
NativeNumberedSaveState.GfxRecordSize
|
||||
&& BinaryPrimitives.ReadInt32LittleEndian(payload.AsSpan(offset + 4)) == count
|
||||
&& BinaryPrimitives.ReadInt32LittleEndian(payload.AsSpan(offset + 8)) == firstHandle)
|
||||
return offset + 8;
|
||||
throw new Xunit.Sdk.XunitException("Encoded gfx table was not found.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,8 @@ public class NumberedSaveVmTests
|
||||
vm.Gfx.SetSurfaceReloadOnRestore(7, true);
|
||||
vm.Gfx.ReleaseSurfaceRange(7, 1);
|
||||
vm.Gfx.BindDraw(100, 3, 1, 2, 30, 40, 50, 60);
|
||||
vm.Gfx.SetCurrentTranslation(100, (7, 8, 9));
|
||||
vm.Gfx.SetRotationChannel(100, 0, 500, (0, 0, 1), 90);
|
||||
|
||||
vm.Run();
|
||||
|
||||
@@ -58,7 +60,25 @@ public class NumberedSaveVmTests
|
||||
Assert.Equal(0x3321, state.SoundEffectResourceIds[1]);
|
||||
Assert.Equal("姫狩り", state.StringGlobals[4]);
|
||||
Assert.Equal(0x77u, state.Frames.Single().ScriptId);
|
||||
Assert.Contains(state.GfxObjects, item => item.Handle == 100);
|
||||
NativeSavedGfxObject savedObject =
|
||||
Assert.Single(state.GfxObjects, item => item.Handle == 100);
|
||||
Assert.Equal(0, ReadGfxField(savedObject.Record, 0x34));
|
||||
Assert.Equal(-1, ReadGfxField(savedObject.Record, 0x60));
|
||||
Assert.Equal(0, ReadGfxField(savedObject.Record, 0x238));
|
||||
Assert.Equal(-1, ReadGfxField(savedObject.Record, 0x240));
|
||||
foreach (int matrixOffset in new[] { 0x6c, 0xac, 0xec, 0x16c, 0x1ac })
|
||||
{
|
||||
Assert.Equal(BitConverter.SingleToInt32Bits(1), ReadGfxField(savedObject.Record, matrixOffset));
|
||||
Assert.Equal(BitConverter.SingleToInt32Bits(1), ReadGfxField(savedObject.Record, matrixOffset + 0x14));
|
||||
Assert.Equal(BitConverter.SingleToInt32Bits(1), ReadGfxField(savedObject.Record, matrixOffset + 0x28));
|
||||
Assert.Equal(BitConverter.SingleToInt32Bits(1), ReadGfxField(savedObject.Record, matrixOffset + 0x3c));
|
||||
}
|
||||
Assert.Equal(BitConverter.SingleToInt32Bits(1), ReadGfxField(savedObject.Record, 0x168));
|
||||
Assert.Equal(BitConverter.SingleToInt32Bits(7), ReadGfxField(savedObject.Record, 0x19c));
|
||||
Assert.Equal(BitConverter.SingleToInt32Bits(8), ReadGfxField(savedObject.Record, 0x1a0));
|
||||
Assert.Equal(BitConverter.SingleToInt32Bits(9), ReadGfxField(savedObject.Record, 0x1a4));
|
||||
Assert.Equal(BitConverter.SingleToInt32Bits(1), ReadGfxField(savedObject.Record, 0x130));
|
||||
Assert.Equal(BitConverter.SingleToInt32Bits(-1), ReadGfxField(savedObject.Record, 0x13c));
|
||||
Assert.Equal(0x1234, ReadSurfaceField(state, 3, 0));
|
||||
Assert.Equal(0, ReadSurfaceField(state, 3, 8));
|
||||
Assert.Equal(-1, ReadSurfaceField(state, 4, 0));
|
||||
@@ -88,14 +108,18 @@ public class NumberedSaveVmTests
|
||||
var store = new DirectoryNativeDatStore(root, Identity);
|
||||
Script resumed = WithTables(WithPackedId(ScriptAssembler.Assemble(Table, "RESUMED.BIN",
|
||||
[
|
||||
(Table.ByLabel("mov")!.Value,
|
||||
[new Operand(GlobalInt, 0x510), new Operand(Immediate, 11)]),
|
||||
(0xae, Array.Empty<Operand>()),
|
||||
(0x3, [new Operand(Immediate, 0x89)]),
|
||||
(Table.ByLabel("mov")!.Value,
|
||||
[new Operand(GlobalInt, 0x500), new Operand(GlobalInt, 0x123)]),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
], []), 0x88), scriptCallOffsets: [1]);
|
||||
], []), 0x88), scriptCallOffsets: [6]);
|
||||
Script child = WithPackedId(ScriptAssembler.Assemble(Table, "CHILD.BIN",
|
||||
[
|
||||
(Table.ByLabel("mov")!.Value,
|
||||
[new Operand(GlobalInt, 0x511), new Operand(Immediate, 22)]),
|
||||
(0xae, Array.Empty<Operand>()),
|
||||
(Table.ByLabel("mov")!.Value,
|
||||
[new Operand(GlobalInt, 0x501), new Operand(GlobalInt, 0x123)]),
|
||||
@@ -168,6 +192,8 @@ public class NumberedSaveVmTests
|
||||
Assert.Equal(777, vm.Globals[0x124]);
|
||||
Assert.Equal(456, vm.Globals[0x500]);
|
||||
Assert.Equal(456, vm.Globals[0x501]);
|
||||
Assert.Equal(11, vm.Globals[0x510]);
|
||||
Assert.Equal(22, vm.Globals[0x511]);
|
||||
Assert.Equal(1, vm.Globals[0x502]);
|
||||
Assert.Equal("復帰", vm.GlobalStrings[0]);
|
||||
Assert.Equal(0x123, vm.GlobalPointers[0]);
|
||||
@@ -204,6 +230,163 @@ public class NumberedSaveVmTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FullLoadReestablishesSaveBoundaryBeforeNestedSaveUiWritesAnotherSlot()
|
||||
{
|
||||
string root = NewTemporaryDirectory();
|
||||
try
|
||||
{
|
||||
var store = new DirectoryNativeDatStore(root, Identity);
|
||||
int callScript = Table.ByLabel("call-script")!.Value;
|
||||
Script resumed = WithTables(WithPackedId(ScriptAssembler.Assemble(Table, "RESUMED.BIN",
|
||||
[
|
||||
(0xae, Array.Empty<Operand>()),
|
||||
(callScript, [new Operand(Immediate, 0x89)]),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
], []), 0x88), scriptCallOffsets: [1]);
|
||||
Script child = WithPackedId(ScriptAssembler.Assemble(Table, "CHILD.BIN",
|
||||
[
|
||||
(0xae, Array.Empty<Operand>()),
|
||||
(callScript, [new Operand(Immediate, 0x91)]),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
], []), 0x89);
|
||||
Script saveUi = WithPackedId(ScriptAssembler.Assemble(Table, "SAVE_UI.BIN",
|
||||
[
|
||||
(0x19e, [new Operand(LocalInt, 0), new Operand(Immediate, 2)]),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
], []), 0x91);
|
||||
Script loader = WithPackedId(ScriptAssembler.Assemble(Table, "LOADER.BIN",
|
||||
[
|
||||
(0x1a1, [new Operand(LocalInt, 0), new Operand(Immediate, 1)]),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
], []), 0x99);
|
||||
byte[] opaqueRecord = NativeGfxRecord(3);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(opaqueRecord.AsSpan(0x68), 0x12345678);
|
||||
NativeNumberedSaveState source = NativeNumberedSaveCodec.Empty(
|
||||
[
|
||||
new NativeSavedScriptFrame(-1, 0x88, [], 8, 0),
|
||||
new NativeSavedScriptFrame(0, 0x89, [], -1, -1),
|
||||
]) with
|
||||
{
|
||||
GfxObjects = [new NativeSavedGfxObject(100, opaqueRecord)],
|
||||
};
|
||||
store.SaveNumberedFile(
|
||||
1, NativeNumberedSaveCodec.Encode(source), [],
|
||||
NativeSystemTime.FromLocalDateTime(DateTime.Now), 0);
|
||||
var vm = new VirtualMachine(
|
||||
loader, Table, new RecordingHost(),
|
||||
provider: new MapProvider(new()
|
||||
{
|
||||
[0x88] = resumed,
|
||||
[0x89] = child,
|
||||
[0x91] = saveUi,
|
||||
}),
|
||||
nativeDatStore: store);
|
||||
|
||||
vm.Run();
|
||||
|
||||
NativeNumberedSaveFile? resaved = store.LoadNumberedFile(2);
|
||||
Assert.NotNull(resaved);
|
||||
NativeNumberedSaveState state =
|
||||
NativeNumberedSaveCodec.Decode(resaved!.Document.Payload);
|
||||
Assert.Equal([0x88u, 0x89u], state.Frames.Select(frame => frame.ScriptId));
|
||||
Assert.Equal(8, state.Frames[0].ResumeIndex);
|
||||
Assert.Equal(0, state.Frames[0].CallTargetIndex);
|
||||
Assert.Equal(
|
||||
0x12345678,
|
||||
ReadGfxField(Assert.Single(state.GfxObjects).Record, 0x68));
|
||||
|
||||
Script reloader = WithPackedId(ScriptAssembler.Assemble(Table, "RELOADER.BIN",
|
||||
[
|
||||
(0x1a1, [new Operand(LocalInt, 0), new Operand(Immediate, 2)]),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
], []), 0x9a);
|
||||
var trace = new RecordingTraceSink();
|
||||
var roundTrippedVm = new VirtualMachine(
|
||||
reloader, Table, new RecordingHost(),
|
||||
provider: new MapProvider(new()
|
||||
{
|
||||
[0x88] = resumed,
|
||||
[0x89] = child,
|
||||
[0x91] = saveUi,
|
||||
}),
|
||||
sink: trace,
|
||||
nativeDatStore: store);
|
||||
|
||||
roundTrippedVm.Run();
|
||||
|
||||
Assert.DoesNotContain(trace.Events, item =>
|
||||
item.Kind == Age.Engine.Diagnostics.TraceEventKind.FrameEnter
|
||||
&& item.Name == "CHILD.BIN"
|
||||
&& item.Cause == Age.Engine.Diagnostics.FrameCause.CallScript);
|
||||
Assert.Equal("exit", roundTrippedVm.HaltReason);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(root, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RestoredChildRootReloadDiscardsSyntheticAncestorChain()
|
||||
{
|
||||
string root = NewTemporaryDirectory();
|
||||
try
|
||||
{
|
||||
var store = new DirectoryNativeDatStore(root, Identity);
|
||||
int callScript = Table.ByLabel("call-script")!.Value;
|
||||
int move = Table.ByLabel("mov")!.Value;
|
||||
Script resumed = WithTables(WithPackedId(ScriptAssembler.Assemble(Table, "RESUMED.BIN",
|
||||
[
|
||||
(0xae, Array.Empty<Operand>()),
|
||||
(callScript, [new Operand(Immediate, 0x89)]),
|
||||
(move, [new Operand(GlobalInt, 0x600), new Operand(Immediate, 1)]),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
], []), 0x88), scriptCallOffsets: [1]);
|
||||
Script child = WithPackedId(ScriptAssembler.Assemble(Table, "CHILD_RELOAD.BIN",
|
||||
[
|
||||
(0xae, Array.Empty<Operand>()),
|
||||
(0x9, Array.Empty<Operand>()),
|
||||
], []), 0x89);
|
||||
Script reloaded = WithPackedId(ScriptAssembler.Assemble(Table, "RELOADED.BIN",
|
||||
[
|
||||
(move, [new Operand(GlobalInt, 0x601), new Operand(Immediate, 1)]),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
], []), 0);
|
||||
Script loader = WithPackedId(ScriptAssembler.Assemble(Table, "LOADER.BIN",
|
||||
[
|
||||
(0x1a1, [new Operand(LocalInt, 0), new Operand(Immediate, 1)]),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
], []), 0x99);
|
||||
NativeNumberedSaveState source = NativeNumberedSaveCodec.Empty(
|
||||
[
|
||||
new NativeSavedScriptFrame(-1, 0x88, [], -1, 0),
|
||||
new NativeSavedScriptFrame(0, 0x89, [], -1, -1),
|
||||
]);
|
||||
store.SaveNumberedFile(
|
||||
1, NativeNumberedSaveCodec.Encode(source), [],
|
||||
NativeSystemTime.FromLocalDateTime(DateTime.Now), 0);
|
||||
var vm = new VirtualMachine(
|
||||
loader, Table, new RecordingHost(),
|
||||
provider: new MapProvider(new()
|
||||
{
|
||||
[0] = reloaded,
|
||||
[0x88] = resumed,
|
||||
[0x89] = child,
|
||||
}),
|
||||
nativeDatStore: store);
|
||||
|
||||
vm.Run();
|
||||
|
||||
Assert.False(vm.Globals.ContainsKey(0x600));
|
||||
Assert.Equal(1, vm.Globals[0x601]);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(root, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DataOnlyLoadReleasesAllSurfacesWhenBothNativeSettingsEnableIt()
|
||||
{
|
||||
@@ -272,6 +455,28 @@ public class NumberedSaveVmTests
|
||||
Assert.False(surface.ReloadOnRestore);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SurfacePersistenceDeclarationSetsAndClearsReloadBit()
|
||||
{
|
||||
Script script = ScriptAssembler.Assemble(Table, "SURFACE_POLICY.BIN",
|
||||
[
|
||||
(0x258, [new Operand(Immediate, 15), new Operand(Immediate, 1)]),
|
||||
(0x258, [new Operand(Immediate, 16), new Operand(Immediate, 3)]),
|
||||
(0x258, [new Operand(Immediate, 15), new Operand(Immediate, 0)]),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
], []);
|
||||
var vm = new VirtualMachine(script, Table, new RecordingHost());
|
||||
vm.Gfx.SetSurface(15, 0x3383, 0);
|
||||
vm.Gfx.SetSurface(16, 0x3384, 0);
|
||||
|
||||
vm.Run();
|
||||
|
||||
GfxSurfacePersistenceState[] surfaces =
|
||||
vm.Gfx.CapturePersistenceSnapshot().Surfaces.ToArray();
|
||||
Assert.False(Assert.Single(surfaces, item => item.Slot == 15).ReloadOnRestore);
|
||||
Assert.True(Assert.Single(surfaces, item => item.Slot == 16).ReloadOnRestore);
|
||||
}
|
||||
|
||||
private static Script WithPackedId(Script source, uint packedId)
|
||||
=> new()
|
||||
{
|
||||
@@ -342,11 +547,21 @@ public class NumberedSaveVmTests
|
||||
Write(0x24, 50);
|
||||
Write(0x28, 60);
|
||||
Write(0x60, -1);
|
||||
Write(0x238, 1);
|
||||
Write(0x23c, 1);
|
||||
Write(0x64, -1);
|
||||
foreach (int matrixOffset in new[] { 0x6c, 0xac, 0xec, 0x12c, 0x16c, 0x1ac })
|
||||
{
|
||||
Write(matrixOffset, BitConverter.SingleToInt32Bits(1));
|
||||
Write(matrixOffset + 0x14, BitConverter.SingleToInt32Bits(1));
|
||||
Write(matrixOffset + 0x28, BitConverter.SingleToInt32Bits(1));
|
||||
Write(matrixOffset + 0x3c, BitConverter.SingleToInt32Bits(1));
|
||||
}
|
||||
Write(0x240, -1);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int ReadGfxField(byte[] record, int offset)
|
||||
=> BinaryPrimitives.ReadInt32LittleEndian(record.AsSpan(offset));
|
||||
|
||||
private static string NewTemporaryDirectory()
|
||||
{
|
||||
string path = Path.Combine(Path.GetTempPath(), "age-save-vm-" + Guid.NewGuid().ToString("N"));
|
||||
|
||||
41
engine/Age.Engine.Tests/RetainedSurfaceRasterizerTests.cs
Normal file
41
engine/Age.Engine.Tests/RetainedSurfaceRasterizerTests.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using Age.Engine.Model;
|
||||
using Age.Engine.Sys4;
|
||||
|
||||
public class RetainedSurfaceRasterizerTests
|
||||
{
|
||||
[Fact]
|
||||
public void CompositeRangePublishesScaledRetainedTextureIntoOffscreenSurface()
|
||||
{
|
||||
var gfx = new GfxState();
|
||||
gfx.SetSurface(2, 0x1234, -1);
|
||||
gfx.BindDraw(100, 2, 0, 0, 4, 4, 0, 0);
|
||||
gfx.SetCurrentScale(100, (50, 50, 100));
|
||||
gfx.SetSurface(3, 0x5678, -1);
|
||||
gfx.BindDraw(200, 3, 0, 0, 2, 2, 0, 0);
|
||||
|
||||
byte[] sourcePixels = new byte[4 * 4 * 4];
|
||||
for (int index = 0; index < sourcePixels.Length; index += 4)
|
||||
{
|
||||
sourcePixels[index] = 0x44;
|
||||
sourcePixels[index + 1] = 0x88;
|
||||
sourcePixels[index + 2] = 0xcc;
|
||||
sourcePixels[index + 3] = 0xff;
|
||||
}
|
||||
var source = new RgbaImage(4, 4, sourcePixels);
|
||||
var excluded = new RgbaImage(2, 2, Enumerable.Repeat((byte)0xff, 16).ToArray());
|
||||
var destination = new RgbaImage(2, 2, new byte[16]);
|
||||
|
||||
int rendered = RetainedSurfaceRasterizer.CompositeRange(
|
||||
destination, gfx.SnapshotVisibleObjects(), 100, 1,
|
||||
item => item.Handle == 100 ? source : excluded);
|
||||
|
||||
Assert.Equal(1, rendered);
|
||||
for (int index = 0; index < destination.Pixels.Length; index += 4)
|
||||
{
|
||||
Assert.Equal(0x44, destination.Pixels[index]);
|
||||
Assert.Equal(0x88, destination.Pixels[index + 1]);
|
||||
Assert.Equal(0xcc, destination.Pixels[index + 2]);
|
||||
Assert.Equal(0xff, destination.Pixels[index + 3]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,6 +101,10 @@ public sealed class GfxState
|
||||
}
|
||||
public sealed class GfxObject
|
||||
{
|
||||
// The native numbered-save record contains several full transform matrices and reserved fields
|
||||
// beyond the port's semantic model. Preserve the decoded bytes so a native load/save round trip
|
||||
// does not erase state that the port has not modeled yet; known fields are patched by the codec.
|
||||
public byte[]? NativePersistenceRecord;
|
||||
public (long X, long Y, long Z) V18, V24, V16c;
|
||||
public long Field64, Field68, Field6c;
|
||||
public long Color = 0xffffffff; // native gfx_object_init_default obj+0x60: identity packed ARGB
|
||||
@@ -464,6 +468,7 @@ public sealed class GfxState
|
||||
private static GfxObject CloneState(GfxObject s)
|
||||
=> new()
|
||||
{
|
||||
NativePersistenceRecord = s.NativePersistenceRecord?.ToArray(),
|
||||
V18 = s.V18, V24 = s.V24, V16c = s.V16c,
|
||||
Field64 = s.Field64, Field68 = s.Field68, Field6c = s.Field6c,
|
||||
Color = s.Color, HasColor = s.HasColor, StaticColorMode = s.StaticColorMode,
|
||||
|
||||
@@ -55,18 +55,27 @@ internal static class NativeGfxPersistenceCodec
|
||||
reload));
|
||||
}
|
||||
var objects = state.GfxObjects
|
||||
.Select(item => (item.Handle, DecodeObject(item.Record)))
|
||||
.Select(item => (item.Handle, DecodeObject(item.Record, state.LegacyTightGfxLayout)))
|
||||
.ToArray();
|
||||
return new GfxPersistenceSnapshot(
|
||||
surfaces, objects, state.RangeTransformFirst, state.RangeTransformCount,
|
||||
DecodeObject(state.RangeTransformRecord));
|
||||
DecodeObject(state.RangeTransformRecord, state.LegacyTightGfxLayout));
|
||||
}
|
||||
|
||||
private static byte[] EncodeObject(GfxState.GfxObject value)
|
||||
{
|
||||
byte[] raw = new byte[NativeNumberedSaveState.GfxRecordSize];
|
||||
int flags = value.Visible ? 1 : 0;
|
||||
if (value.RotationEnabled || value.SrcAnim || value.ColorAnim) flags |= 4;
|
||||
byte[] raw = value.NativePersistenceRecord is { Length: NativeNumberedSaveState.GfxRecordSize }
|
||||
? value.NativePersistenceRecord.ToArray()
|
||||
: CreateDefaultObjectRecord();
|
||||
int flags = ReadInt(raw, 0);
|
||||
flags = value.Visible ? flags | 1 : flags & ~1;
|
||||
flags = value.OneShotColorEnabled || value.ScaleEnabled
|
||||
|| value.RotationChannelEnabled || value.TranslationEnabled
|
||||
? flags | 2
|
||||
: flags & ~2;
|
||||
flags = value.RotationEnabled || value.SrcAnim || value.ColorAnim
|
||||
? flags | 4
|
||||
: flags & ~4;
|
||||
WriteInt(raw, 0, flags);
|
||||
WriteInt(raw, 4, value.SourceSlot);
|
||||
WriteInt(raw, 8, value.SrcRect.X);
|
||||
@@ -76,7 +85,7 @@ internal static class NativeGfxPersistenceCodec
|
||||
WriteVector(raw, 0x18, value.V18);
|
||||
WriteVector(raw, 0x24, value.V24);
|
||||
WriteInt(raw, 0x30, unchecked((int)value.StaticColorMode));
|
||||
WriteInt(raw, 0x34, unchecked((int)value.OneShotStartMs));
|
||||
WriteInt(raw, 0x34, EncodeNativeStart(value.OneShotStartMs));
|
||||
WriteInt(raw, 0x38, unchecked((int)value.ColorDelayMs));
|
||||
WriteInt(raw, 0x3c, unchecked((int)value.ScaleDelayMs));
|
||||
WriteInt(raw, 0x40, unchecked((int)value.RotationDelayMs));
|
||||
@@ -89,27 +98,34 @@ internal static class NativeGfxPersistenceCodec
|
||||
WriteInt(raw, 0x64, unchecked((int)value.OneShotColorTarget));
|
||||
WriteScaleMatrix(raw, 0x6c, value.ScaleCurrent);
|
||||
WriteScaleMatrix(raw, 0xac, value.ScaleTarget);
|
||||
WriteFloat(raw, 0x16c, value.TranslationCurrent.X);
|
||||
WriteFloat(raw, 0x170, value.TranslationCurrent.Y);
|
||||
WriteFloat(raw, 0x174, value.TranslationCurrent.Z);
|
||||
WriteFloat(raw, 0x1ac, value.TranslationTarget.X);
|
||||
WriteFloat(raw, 0x1b0, value.TranslationTarget.Y);
|
||||
WriteFloat(raw, 0x1b4, value.TranslationTarget.Z);
|
||||
WriteInt(raw, 0x20c, unchecked((int)value.ColorStart));
|
||||
WriteInt(raw, 0x214, unchecked((int)value.RotationStartMs));
|
||||
WriteRotationMatrix(raw, 0xec, value.RotationCurrent);
|
||||
WriteRotationMatrix(raw, 0x12c, value.RotationTarget);
|
||||
WriteTranslationMatrix(raw, 0x16c, value.TranslationCurrent);
|
||||
WriteTranslationMatrix(raw, 0x1ac, value.TranslationTarget);
|
||||
WriteFloat(raw, 0x1ec, value.RotationCurrent.X);
|
||||
WriteFloat(raw, 0x1f0, value.RotationCurrent.Y);
|
||||
WriteFloat(raw, 0x1f4, value.RotationCurrent.Z);
|
||||
WriteFloat(raw, 0x1f8, value.RotationTarget.X);
|
||||
WriteFloat(raw, 0x1fc, value.RotationTarget.Y);
|
||||
WriteFloat(raw, 0x200, value.RotationTarget.Z);
|
||||
WriteFloat(raw, 0x204, value.RotationCurrent.Angle);
|
||||
WriteFloat(raw, 0x208, value.RotationTarget.Angle);
|
||||
WriteInt(raw, 0x20c, EncodeNativeStart(value.ColorStart));
|
||||
WriteInt(raw, 0x214, EncodeNativeStart(value.RotationStartMs));
|
||||
WriteInt(raw, 0x220, unchecked((int)value.ColorPeriod));
|
||||
WriteInt(raw, 0x228, unchecked((int)value.RotationPeriodMs));
|
||||
WriteInt(raw, 0x230, unchecked((int)value.SrcPeriod));
|
||||
WriteInt(raw, 0x234, unchecked((int)value.SrcCell));
|
||||
WriteInt(raw, 0x238, unchecked((int)value.SrcFrameCount));
|
||||
WriteInt(raw, 0x23c, unchecked((int)value.SrcColumns));
|
||||
WriteInt(raw, 0x240, unchecked((int)value.ColorTarget));
|
||||
WriteInt(raw, 0x238, value.SrcAnim ? unchecked((int)value.SrcFrameCount) : 0);
|
||||
WriteInt(raw, 0x23c, value.SrcAnim ? unchecked((int)value.SrcColumns) : 0);
|
||||
if (value.ColorAnim)
|
||||
WriteInt(raw, 0x240, unchecked((int)value.ColorTarget));
|
||||
WriteVector(raw, 0x244, value.RotationAxis);
|
||||
WriteInt(raw, 0x2d0, unchecked((int)value.OneShotAnimationControlFlags));
|
||||
return raw;
|
||||
}
|
||||
|
||||
private static GfxState.GfxObject DecodeObject(ReadOnlySpan<byte> raw)
|
||||
private static GfxState.GfxObject DecodeObject(ReadOnlySpan<byte> raw, bool legacyTightLayout)
|
||||
{
|
||||
if (raw.Length != NativeNumberedSaveState.GfxRecordSize)
|
||||
throw new InvalidDataException("Native retained-gfx record has the wrong size.");
|
||||
@@ -118,13 +134,16 @@ internal static class NativeGfxPersistenceCodec
|
||||
int flags = ReadInt(raw, 0);
|
||||
return new GfxState.GfxObject
|
||||
{
|
||||
NativePersistenceRecord = legacyTightLayout
|
||||
? CreateDefaultObjectRecord()
|
||||
: raw.ToArray(),
|
||||
Visible = (flags & 1) != 0,
|
||||
SourceSlot = ReadInt(raw, 4),
|
||||
SrcRect = (left, top, right - left, bottom - top),
|
||||
V18 = ReadLongVector(raw, 0x18),
|
||||
V24 = ReadLongVector(raw, 0x24),
|
||||
StaticColorMode = ReadInt(raw, 0x30),
|
||||
OneShotStartMs = ReadInt(raw, 0x34),
|
||||
OneShotStartMs = DecodeNativeStart(ReadInt(raw, 0x34)),
|
||||
ColorDelayMs = ReadInt(raw, 0x38),
|
||||
ScaleDelayMs = ReadInt(raw, 0x3c),
|
||||
RotationDelayMs = ReadInt(raw, 0x40),
|
||||
@@ -138,11 +157,17 @@ internal static class NativeGfxPersistenceCodec
|
||||
OneShotColorTarget = unchecked((uint)ReadInt(raw, 0x64)),
|
||||
ScaleCurrent = ReadScale(raw, 0x6c),
|
||||
ScaleTarget = ReadScale(raw, 0xac),
|
||||
TranslationCurrent = ReadDoubleVector(raw, 0x16c),
|
||||
V16c = ReadLongFloatVector(raw, 0x16c),
|
||||
TranslationTarget = ReadDoubleVector(raw, 0x1ac),
|
||||
ColorStart = ReadInt(raw, 0x20c),
|
||||
RotationStartMs = ReadInt(raw, 0x214),
|
||||
TranslationCurrent = ReadMatrixTranslation(raw, 0x16c),
|
||||
V16c = ReadLongMatrixTranslation(raw, 0x16c),
|
||||
TranslationTarget = ReadMatrixTranslation(raw, 0x1ac),
|
||||
RotationCurrent = (
|
||||
ReadFloat(raw, 0x1ec), ReadFloat(raw, 0x1f0), ReadFloat(raw, 0x1f4),
|
||||
ReadFloat(raw, 0x204)),
|
||||
RotationTarget = (
|
||||
ReadFloat(raw, 0x1f8), ReadFloat(raw, 0x1fc), ReadFloat(raw, 0x200),
|
||||
ReadFloat(raw, 0x208)),
|
||||
ColorStart = DecodeNativeStart(ReadInt(raw, 0x20c)),
|
||||
RotationStartMs = DecodeNativeStart(ReadInt(raw, 0x214)),
|
||||
ColorPeriod = ReadInt(raw, 0x220),
|
||||
RotationPeriodMs = ReadInt(raw, 0x228),
|
||||
SrcPeriod = ReadInt(raw, 0x230),
|
||||
@@ -170,6 +195,7 @@ internal static class NativeGfxPersistenceCodec
|
||||
|
||||
private static void WriteScaleMatrix(Span<byte> raw, int offset, (double X, double Y, double Z) scale)
|
||||
{
|
||||
ClearMatrix(raw, offset);
|
||||
WriteFloat(raw, offset, scale.X);
|
||||
WriteFloat(raw, offset + 0x14, scale.Y);
|
||||
WriteFloat(raw, offset + 0x28, scale.Z);
|
||||
@@ -179,6 +205,40 @@ internal static class NativeGfxPersistenceCodec
|
||||
private static (double X, double Y, double Z) ReadScale(ReadOnlySpan<byte> raw, int offset)
|
||||
=> (ReadFloat(raw, offset), ReadFloat(raw, offset + 0x14), ReadFloat(raw, offset + 0x28));
|
||||
|
||||
private static void WriteRotationMatrix(
|
||||
Span<byte> raw, int offset, (double X, double Y, double Z, double Angle) rotation)
|
||||
{
|
||||
ClearMatrix(raw, offset);
|
||||
double length = Math.Sqrt(
|
||||
rotation.X * rotation.X + rotation.Y * rotation.Y + rotation.Z * rotation.Z);
|
||||
if (length <= double.Epsilon || rotation.Angle == 0)
|
||||
{
|
||||
WriteFloat(raw, offset, 1);
|
||||
WriteFloat(raw, offset + 0x14, 1);
|
||||
WriteFloat(raw, offset + 0x28, 1);
|
||||
WriteFloat(raw, offset + 0x3c, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
double x = rotation.X / length;
|
||||
double y = rotation.Y / length;
|
||||
double z = rotation.Z / length;
|
||||
double radians = rotation.Angle * Math.PI / 180.0;
|
||||
double cosine = Math.Cos(radians);
|
||||
double sine = Math.Sin(radians);
|
||||
double complement = 1.0 - cosine;
|
||||
WriteFloat(raw, offset, x * x * complement + cosine);
|
||||
WriteFloat(raw, offset + 0x04, x * y * complement + z * sine);
|
||||
WriteFloat(raw, offset + 0x08, x * z * complement - y * sine);
|
||||
WriteFloat(raw, offset + 0x10, x * y * complement - z * sine);
|
||||
WriteFloat(raw, offset + 0x14, y * y * complement + cosine);
|
||||
WriteFloat(raw, offset + 0x18, x * sine + y * z * complement);
|
||||
WriteFloat(raw, offset + 0x20, y * sine + x * z * complement);
|
||||
WriteFloat(raw, offset + 0x24, y * z * complement - x * sine);
|
||||
WriteFloat(raw, offset + 0x28, z * z * complement + cosine);
|
||||
WriteFloat(raw, offset + 0x3c, 1);
|
||||
}
|
||||
|
||||
private static void WriteVector(Span<byte> raw, int offset, (long X, long Y, long Z) vector)
|
||||
{
|
||||
WriteInt(raw, offset, unchecked((int)vector.X));
|
||||
@@ -189,11 +249,55 @@ internal static class NativeGfxPersistenceCodec
|
||||
private static (long X, long Y, long Z) ReadLongVector(ReadOnlySpan<byte> raw, int offset)
|
||||
=> (ReadInt(raw, offset), ReadInt(raw, offset + 4), ReadInt(raw, offset + 8));
|
||||
|
||||
private static (long X, long Y, long Z) ReadLongFloatVector(ReadOnlySpan<byte> raw, int offset)
|
||||
=> ((long)ReadFloat(raw, offset), (long)ReadFloat(raw, offset + 4), (long)ReadFloat(raw, offset + 8));
|
||||
private static void WriteTranslationMatrix(
|
||||
Span<byte> raw, int offset, (double X, double Y, double Z) translation)
|
||||
{
|
||||
ClearMatrix(raw, offset);
|
||||
WriteFloat(raw, offset, 1);
|
||||
WriteFloat(raw, offset + 0x14, 1);
|
||||
WriteFloat(raw, offset + 0x28, 1);
|
||||
WriteFloat(raw, offset + 0x30, translation.X);
|
||||
WriteFloat(raw, offset + 0x34, translation.Y);
|
||||
WriteFloat(raw, offset + 0x38, translation.Z);
|
||||
WriteFloat(raw, offset + 0x3c, 1);
|
||||
}
|
||||
|
||||
private static (double X, double Y, double Z) ReadDoubleVector(ReadOnlySpan<byte> raw, int offset)
|
||||
=> (ReadFloat(raw, offset), ReadFloat(raw, offset + 4), ReadFloat(raw, offset + 8));
|
||||
private static (long X, long Y, long Z) ReadLongMatrixTranslation(
|
||||
ReadOnlySpan<byte> raw, int offset)
|
||||
=> ((long)ReadFloat(raw, offset + 0x30),
|
||||
(long)ReadFloat(raw, offset + 0x34),
|
||||
(long)ReadFloat(raw, offset + 0x38));
|
||||
|
||||
private static (double X, double Y, double Z) ReadMatrixTranslation(
|
||||
ReadOnlySpan<byte> raw, int offset)
|
||||
=> (ReadFloat(raw, offset + 0x30),
|
||||
ReadFloat(raw, offset + 0x34),
|
||||
ReadFloat(raw, offset + 0x38));
|
||||
|
||||
private static void ClearMatrix(Span<byte> raw, int offset)
|
||||
=> raw.Slice(offset, 0x40).Clear();
|
||||
|
||||
private static int EncodeNativeStart(long value)
|
||||
=> value < 0 ? 0 : unchecked((int)value);
|
||||
|
||||
private static long DecodeNativeStart(int value)
|
||||
=> value == 0 ? -1 : value;
|
||||
|
||||
private static byte[] CreateDefaultObjectRecord()
|
||||
{
|
||||
byte[] raw = new byte[NativeNumberedSaveState.GfxRecordSize];
|
||||
WriteInt(raw, 0x60, -1);
|
||||
WriteInt(raw, 0x64, -1);
|
||||
foreach (int matrixOffset in new[] { 0x6c, 0xac, 0xec, 0x12c, 0x16c, 0x1ac })
|
||||
{
|
||||
WriteFloat(raw, matrixOffset, 1);
|
||||
WriteFloat(raw, matrixOffset + 0x14, 1);
|
||||
WriteFloat(raw, matrixOffset + 0x28, 1);
|
||||
WriteFloat(raw, matrixOffset + 0x3c, 1);
|
||||
}
|
||||
WriteInt(raw, 0x240, -1);
|
||||
return raw;
|
||||
}
|
||||
|
||||
private static void WriteFloat(Span<byte> raw, int offset, double value)
|
||||
=> WriteInt(raw, offset, BitConverter.SingleToInt32Bits((float)value));
|
||||
|
||||
@@ -28,7 +28,8 @@ public sealed record NativeNumberedSaveState(
|
||||
IReadOnlyList<NativeSavedGfxObject> GfxObjects,
|
||||
long RangeTransformFirst,
|
||||
int RangeTransformCount,
|
||||
byte[] RangeTransformRecord)
|
||||
byte[] RangeTransformRecord,
|
||||
bool LegacyTightGfxLayout = false)
|
||||
{
|
||||
public const int SoundEffectChannelCount = 10;
|
||||
public const int ResourceRecordsSize = 300 * 4;
|
||||
@@ -45,6 +46,9 @@ public static class NativeNumberedSaveCodec
|
||||
private const int FrameReturnCapacity = 256;
|
||||
private const int GfxAllocationDwords = 0x2d8;
|
||||
private const int GfxConstantDwords = 0x2e1;
|
||||
// AGE stores a byte-sized 0x2d4 object record at the front of a 0x2d4-DWORD region.
|
||||
// The handle consumes one preceding DWORD, so consecutive entries start 0x2d5 DWORDs apart.
|
||||
private const int GfxEntryStrideBytes = (1 + NativeNumberedSaveState.GfxRecordSize) * 4;
|
||||
private static readonly Encoding NativeEncoding = CreateNativeEncoding();
|
||||
|
||||
public static byte[] Encode(NativeNumberedSaveState state)
|
||||
@@ -103,7 +107,7 @@ public static class NativeNumberedSaveCodec
|
||||
{
|
||||
WriteInt(payload, at, unchecked((int)gfx.Handle));
|
||||
gfx.Record.CopyTo(payload, at + 4);
|
||||
at += 4 + NativeNumberedSaveState.GfxRecordSize;
|
||||
at += GfxEntryStrideBytes;
|
||||
}
|
||||
WriteInt(payload, at, unchecked((int)state.RangeTransformFirst));
|
||||
WriteInt(payload, at + 4, state.RangeTransformCount);
|
||||
@@ -154,14 +158,19 @@ public static class NativeNumberedSaveCodec
|
||||
if (gfxRecordSize != NativeNumberedSaveState.GfxRecordSize)
|
||||
throw new InvalidDataException($"Unsupported native gfx record size 0x{gfxRecordSize:x}.");
|
||||
at += 8;
|
||||
var objects = new NativeSavedGfxObject[gfxCount];
|
||||
for (int i = 0; i < objects.Length; i++)
|
||||
int objectsAt = at;
|
||||
(NativeSavedGfxObject[] objects, int afterObjects) =
|
||||
ReadGfxObjects(payload, objectsAt, gfxCount, GfxEntryStrideBytes);
|
||||
bool legacyTightGfxLayout = false;
|
||||
// Compatibility for experimental files written by the port before the native DWORD stride was
|
||||
// understood. A native retained-object map cannot contain duplicate handles.
|
||||
if (objects.Select(item => item.Handle).Distinct().Count() != objects.Length)
|
||||
{
|
||||
Require(payload, at, 4 + gfxRecordSize, "numbered-save gfx object");
|
||||
long handle = ReadInt(payload, at);
|
||||
objects[i] = new NativeSavedGfxObject(handle, payload.Slice(at + 4, gfxRecordSize).ToArray());
|
||||
at += 4 + gfxRecordSize;
|
||||
(objects, afterObjects) =
|
||||
ReadGfxObjects(payload, objectsAt, gfxCount, 4 + gfxRecordSize);
|
||||
legacyTightGfxLayout = true;
|
||||
}
|
||||
at = afterObjects;
|
||||
Require(payload, at, 8 + gfxRecordSize, "numbered-save range transform");
|
||||
long rangeFirst = ReadInt(payload, at);
|
||||
int rangeCount = ReadInt(payload, at + 4);
|
||||
@@ -170,7 +179,25 @@ public static class NativeNumberedSaveCodec
|
||||
return new NativeNumberedSaveState(
|
||||
ReadInt(payload, 4), ReadInt(payload, 8), soundEffects, resources, surfaces, frames,
|
||||
integers, floats, strings, pointers, pointerStrings, localPointerScratch, objects,
|
||||
rangeFirst, rangeCount, rangeRecord);
|
||||
rangeFirst, rangeCount, rangeRecord, legacyTightGfxLayout);
|
||||
}
|
||||
|
||||
private static (NativeSavedGfxObject[] Objects, int After) ReadGfxObjects(
|
||||
ReadOnlySpan<byte> payload, int offset, int count, int strideBytes)
|
||||
{
|
||||
var objects = new NativeSavedGfxObject[count];
|
||||
int at = offset;
|
||||
for (int i = 0; i < objects.Length; i++)
|
||||
{
|
||||
Require(payload, at, 4 + NativeNumberedSaveState.GfxRecordSize,
|
||||
"numbered-save gfx object");
|
||||
long handle = ReadInt(payload, at);
|
||||
objects[i] = new NativeSavedGfxObject(
|
||||
handle,
|
||||
payload.Slice(at + 4, NativeNumberedSaveState.GfxRecordSize).ToArray());
|
||||
at = checked(at + strideBytes);
|
||||
}
|
||||
return (objects, at);
|
||||
}
|
||||
|
||||
public static NativeNumberedSaveState Empty(IReadOnlyList<NativeSavedScriptFrame> frames)
|
||||
|
||||
65
engine/Age.Engine/Sys4/RetainedSurfaceRasterizer.cs
Normal file
65
engine/Age.Engine/Sys4/RetainedSurfaceRasterizer.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using Age.Engine.Model;
|
||||
|
||||
namespace Age.Engine.Sys4;
|
||||
|
||||
/// <summary>Platform-neutral software publication of retained gfx objects into an AGE surface.
|
||||
/// Native scripts use the same object list for the backbuffer and selected offscreen render targets.</summary>
|
||||
public static class RetainedSurfaceRasterizer
|
||||
{
|
||||
public static int CompositeRange(
|
||||
RgbaImage destination,
|
||||
IReadOnlyList<RenderObject> visible,
|
||||
long firstHandle,
|
||||
long count,
|
||||
Func<RenderObject, RgbaImage?> resolveSource)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(destination);
|
||||
ArgumentNullException.ThrowIfNull(visible);
|
||||
ArgumentNullException.ThrowIfNull(resolveSource);
|
||||
if (destination.Width <= 0 || destination.Height <= 0
|
||||
|| destination.Pixels.Length != checked(destination.Width * destination.Height * 4)
|
||||
|| count <= 0)
|
||||
return 0;
|
||||
|
||||
int rendered = 0;
|
||||
foreach (RenderObject item in visible)
|
||||
{
|
||||
if (item.Handle < firstHandle || item.Handle - firstHandle >= count) continue;
|
||||
|
||||
TransformState transform = item.Transform;
|
||||
Affine2D localToDestination =
|
||||
Transform2DMath.Build(transform, item.Rotation).FromLocalOrigin(item.DstX, item.DstY);
|
||||
if (item.RangeTransform is { } rangeTransform)
|
||||
localToDestination = localToDestination.Then(rangeTransform);
|
||||
|
||||
RgbaImage? source = resolveSource(item);
|
||||
float opacity = item.Alpha / 255f;
|
||||
float tintStrength = item.TintStrength / 255f;
|
||||
if (source == null)
|
||||
{
|
||||
if (item.SurfaceResId != 0 || item.Blend == BlendKind.Opaque) continue;
|
||||
int width = item.W > 0 ? item.W : 800;
|
||||
int height = item.H > 0 ? item.H : 600;
|
||||
float fillOpacity = item.MultiplyTint ? opacity : opacity * tintStrength;
|
||||
SoftwareAffineRasterizer.FillRgba(
|
||||
destination.Pixels, destination.Width, destination.Height,
|
||||
width, height, localToDestination, item.Tint, fillOpacity);
|
||||
rendered++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.W <= 0 || item.H <= 0 || item.SrcX < 0 || item.SrcY < 0) continue;
|
||||
int widthToDraw = System.Math.Min(item.W, source.Width - item.SrcX);
|
||||
int heightToDraw = System.Math.Min(item.H, source.Height - item.SrcY);
|
||||
if (widthToDraw <= 0 || heightToDraw <= 0) continue;
|
||||
SoftwareAffineRasterizer.BlitRgba(
|
||||
destination.Pixels, destination.Width, destination.Height,
|
||||
source.Pixels, source.Width, source.Height,
|
||||
item.SrcX, item.SrcY, widthToDraw, heightToDraw,
|
||||
localToDestination, item.Tint, tintStrength, opacity,
|
||||
item.MultiplyTint, item.Blend);
|
||||
rendered++;
|
||||
}
|
||||
return rendered;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Age.Engine.Model;
|
||||
using Age.Engine.Persistence;
|
||||
namespace Age.Engine.Vm;
|
||||
|
||||
/// <summary>One script activation: the running script, its instruction cursor, its local slots,
|
||||
@@ -10,6 +11,10 @@ internal sealed class ExecFrame
|
||||
|
||||
public readonly Script Script;
|
||||
public int Pc; // entry instruction index
|
||||
// While a restored descendant is running, this activation is parked at the synthetic op-0xae
|
||||
// rendezvous rather than its native T1/T2/T3 coordinate. Retain the serialized coordinates until
|
||||
// the descendant returns so an intervening save can reproduce the native parent frame.
|
||||
public NativeSavedScriptFrame? RestoredSaveFrame;
|
||||
public int ReadMessageOffset = -1; // latest op-0x71 code DWORD coordinate
|
||||
public readonly Frame Locals = new();
|
||||
public readonly List<int> CallStack = new(); // intra-script `call` (op 0x8f) returns
|
||||
|
||||
@@ -595,12 +595,17 @@ public sealed class VirtualMachine
|
||||
Script root = _s;
|
||||
int rootEntry = root.IndexByOffset.TryGetValue(entryOffset, out var idx) ? idx : 0;
|
||||
FrameCause cause = FrameCause.TopScene;
|
||||
NativeSavedScriptFrame? restoredRootFrame = null;
|
||||
while (true)
|
||||
{
|
||||
FrameOutcome outcome;
|
||||
try
|
||||
{
|
||||
outcome = RunFrame(new ExecFrame(root, rootEntry), cause);
|
||||
ExecFrame rootFrame = restoredRootFrame == null
|
||||
? new ExecFrame(root, rootEntry)
|
||||
: CreateRestoredFrame(root, restoredRootFrame);
|
||||
restoredRootFrame = null;
|
||||
outcome = RunFrame(rootFrame, cause);
|
||||
}
|
||||
catch (NumberedRestoreRequestedException)
|
||||
{
|
||||
@@ -618,8 +623,9 @@ public sealed class VirtualMachine
|
||||
}
|
||||
}
|
||||
root = ResolveSavedScript(_loadedNumberedState.Frames[0]);
|
||||
rootEntry = FindRestoreRendezvous(root);
|
||||
rootEntry = 0;
|
||||
_restoreFrameIndex = 0;
|
||||
restoredRootFrame = _loadedNumberedState.Frames[0];
|
||||
cause = FrameCause.SaveRestore;
|
||||
continue;
|
||||
}
|
||||
@@ -800,19 +806,26 @@ public sealed class VirtualMachine
|
||||
for (int i = 0; i <= cutoff; i++)
|
||||
{
|
||||
ExecFrame frame = active[i];
|
||||
int[] returns = frame.CallStack
|
||||
.Where(returnPc => (uint)returnPc < (uint)frame.Script.Instructions.Count)
|
||||
.Select(returnPc =>
|
||||
{
|
||||
int returnOffset = frame.Script.Instructions[returnPc].Offset;
|
||||
return FindTableIndex(frame.Script.LocalCallOffsets, returnOffset - 3);
|
||||
})
|
||||
.Where(index => index >= 0)
|
||||
.ToArray();
|
||||
int resumeIndex = CurrentReadMessageIndex(frame);
|
||||
int callTargetIndex = i == cutoff || (uint)frame.Pc >= (uint)frame.Script.Instructions.Count
|
||||
NativeSavedScriptFrame? restored = frame.RestoredSaveFrame;
|
||||
int[] returns = restored?.ReturnIndices.ToArray()
|
||||
?? frame.CallStack
|
||||
.Where(returnPc => (uint)returnPc < (uint)frame.Script.Instructions.Count)
|
||||
.Select(returnPc =>
|
||||
{
|
||||
int returnOffset = frame.Script.Instructions[returnPc].Offset;
|
||||
return FindTableIndex(frame.Script.LocalCallOffsets, returnOffset - 3);
|
||||
})
|
||||
.Where(index => index >= 0)
|
||||
.ToArray();
|
||||
int resumeIndex = restored?.ResumeIndex ?? CurrentReadMessageIndex(frame);
|
||||
int callTargetIndex = i == cutoff
|
||||
? -1
|
||||
: FindTableIndex(frame.Script.ScriptCallOffsets, frame.Script.Instructions[frame.Pc].Offset);
|
||||
: restored?.CallTargetIndex
|
||||
?? ((uint)frame.Pc >= (uint)frame.Script.Instructions.Count
|
||||
? -1
|
||||
: FindTableIndex(
|
||||
frame.Script.ScriptCallOffsets,
|
||||
frame.Script.Instructions[frame.Pc].Offset));
|
||||
frames[i] = new NativeSavedScriptFrame(
|
||||
i - 1, frame.Script.PackedId, returns, resumeIndex, callTargetIndex);
|
||||
}
|
||||
@@ -941,6 +954,29 @@ public sealed class VirtualMachine
|
||||
$"Saved script {script.Name} has no opcode 0xae restore rendezvous.");
|
||||
}
|
||||
|
||||
private static ExecFrame CreateRestoredFrame(Script script, NativeSavedScriptFrame saved)
|
||||
{
|
||||
// Native creates each saved script context at its ordinary entrypoint. The script runs its
|
||||
// local-array/constants/resource prologue and rendezvouses at 0xae itself; jumping directly
|
||||
// to 0xae leaves those frame-local tables zeroed (FIELD then collapses its map zoom to 0%).
|
||||
_ = FindRestoreRendezvous(script);
|
||||
int entry = script.IndexByOffset.TryGetValue(0, out int index) ? index : 0;
|
||||
var frame = new ExecFrame(script, entry)
|
||||
{
|
||||
RestoredSaveFrame = saved,
|
||||
};
|
||||
if ((uint)saved.ResumeIndex < (uint)script.ReadMessageOffsets.Count)
|
||||
frame.ReadMessageOffset = script.ReadMessageOffsets[saved.ResumeIndex];
|
||||
foreach (int returnIndex in saved.ReturnIndices)
|
||||
{
|
||||
if ((uint)returnIndex >= (uint)script.LocalCallOffsets.Count) continue;
|
||||
int returnOffset = checked(script.LocalCallOffsets[returnIndex] + 3);
|
||||
if (script.IndexByOffset.TryGetValue(returnOffset, out int returnPc))
|
||||
frame.CallStack.Add(returnPc);
|
||||
}
|
||||
return frame;
|
||||
}
|
||||
|
||||
private static int ResolveTableOffset(
|
||||
Script script, IReadOnlyList<int> table, int index, int fallback)
|
||||
{
|
||||
@@ -1039,6 +1075,9 @@ public sealed class VirtualMachine
|
||||
{
|
||||
case "script-entry":
|
||||
Gfx.ClearSurfaceReloadPolicies(); return pc + 1;
|
||||
case "set-surface-persistence-flags": // 0x258 (slot)(flags): bit 0 = numbered-load reload
|
||||
Gfx.SetSurfaceReloadOnRestore(unchecked((int)Read(a[0])), (Read(a[1]) & 1) != 0);
|
||||
return pc + 1;
|
||||
case "add": Write(a[0], Read(a[1]) + Read(a[2])); return pc + 1;
|
||||
case "sub": Write(a[0], Read(a[1]) - Read(a[2])); return pc + 1;
|
||||
case "mul": Write(a[0], Read(a[1]) * Read(a[2])); return pc + 1;
|
||||
@@ -1139,6 +1178,11 @@ public sealed class VirtualMachine
|
||||
bool terminal = _restoreFrameIndex == _loadedNumberedState.Frames.Count - 1;
|
||||
if (terminal)
|
||||
{
|
||||
// Native restores the saved top frame as the numbered-save boundary selected by
|
||||
// opcode 0x1ad. Re-establish that identity so a subsequent save excludes transient
|
||||
// SAVE/menu helper frames instead of serializing the currently open modal stack.
|
||||
lock (_debugControlLock) _saveResumeFrame = _cur;
|
||||
_cur.RestoredSaveFrame = null;
|
||||
_loadedNumberedState = null;
|
||||
_restoreFrameIndex = -1;
|
||||
return ResolveTableOffset(_cur.Script, _cur.Script.ReadMessageOffsets, saved.ResumeIndex, pc + 1);
|
||||
@@ -1149,11 +1193,14 @@ public sealed class VirtualMachine
|
||||
Script child = ResolveSavedScript(childSaved);
|
||||
_restoreFrameIndex = parentIndex + 1;
|
||||
FrameOutcome childOutcome = RunFrame(
|
||||
new ExecFrame(child, FindRestoreRendezvous(child)), FrameCause.SaveRestore,
|
||||
CreateRestoredFrame(child, childSaved), FrameCause.SaveRestore,
|
||||
childSaved.ScriptId);
|
||||
_restoreFrameIndex = parentIndex;
|
||||
if (childOutcome is FrameOutcome.Halted or FrameOutcome.ExitRequested)
|
||||
return HALT;
|
||||
if (childOutcome == FrameOutcome.Halted) return HALT;
|
||||
if (childOutcome == FrameOutcome.RootReload) return ROOT_RELOAD;
|
||||
if (childOutcome == FrameOutcome.ExitRequested)
|
||||
throw new ProcessExitRequestedException();
|
||||
_cur.RestoredSaveFrame = null;
|
||||
return ResolveTableOffset(
|
||||
_cur.Script, _cur.Script.ScriptCallOffsets, saved.CallTargetIndex, pc) + 1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user