feat: model ADV coroutines and retained effect teardown
This commit is contained in:
114
engine/Age.Engine.Tests/CoroutineHostModelTests.cs
Normal file
114
engine/Age.Engine.Tests/CoroutineHostModelTests.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Age.Engine.Diagnostics;
|
||||
using Age.Engine.Hosting;
|
||||
using Age.Engine.Model;
|
||||
using Age.Engine.Sys4;
|
||||
using Age.Engine.Vm;
|
||||
using Xunit;
|
||||
|
||||
public class CoroutineHostModelTests
|
||||
{
|
||||
private const int T_IMM = 0, T_STR = 2, T_GINT = 3, T_LINT = 9;
|
||||
private const int SceneEntryGate = 0xaba5c;
|
||||
private static readonly OpcodeTable Table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||
|
||||
private static int Op(string label) => Table.ByLabel(label)!.Value;
|
||||
private static Instruction Ins(int offset, int opcode, params Operand[] args) => new(offset, opcode, args);
|
||||
|
||||
private static Script Script(params Instruction[] instructions) => new()
|
||||
{
|
||||
Name = "COROUTINE-TEST",
|
||||
Header = new ScriptHeader(0, 0, 0, 0, 0, 0),
|
||||
Instructions = instructions,
|
||||
IndexByOffset = instructions.Select((ins, i) => (ins.Offset, i)).ToDictionary(x => x.Offset, x => x.i),
|
||||
Strings = new Dictionary<int, string> { [0x1000] = "LABEL", [0x1001] = "J" },
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void AdvLabeledYieldRunsSetupExactlyOnceThenUsesStructuralTerminal()
|
||||
{
|
||||
const int output = 0x6be, terminal = 0x6c3, setupCount = 0x7000, slot = 0x7001, observed = 0x7002;
|
||||
var script = Script(
|
||||
Ins(0x00, Op("eq"), new(T_LINT, 0), new(T_GINT, SceneEntryGate), new(T_IMM, 1)),
|
||||
Ins(0x07, Op("jcc"), new(T_LINT, 0), new(T_IMM, 0x20), new(T_IMM, 0x80)),
|
||||
Ins(0x20, 0x140, new(T_GINT, output), new(T_STR, 0x1000), new(T_STR, 0x1001), new(T_GINT, output)),
|
||||
Ins(0x29, Op("mov"), new(T_GINT, terminal), new(T_IMM, 0x77)),
|
||||
Ins(0x2e, Op("eq"), new(T_LINT, 1), new(T_GINT, terminal), new(T_GINT, output)),
|
||||
Ins(0x35, Op("jcc"), new(T_LINT, 1), new(T_IMM, 0x60), new(T_IMM, 0x40)),
|
||||
Ins(0x40, Op("add"), new(T_GINT, setupCount), new(T_GINT, setupCount), new(T_IMM, 1)),
|
||||
Ins(0x47, Op("mov"), new(T_GINT, slot), new(T_IMM, 4)),
|
||||
Ins(0x4c, Op("jmp"), new Operand(T_IMM, 0x20)),
|
||||
Ins(0x60, Op("mov"), new(T_GINT, SceneEntryGate), new(T_IMM, 0)),
|
||||
Ins(0x65, Op("jmp"), new Operand(T_IMM, 0x80)),
|
||||
Ins(0x80, Op("mov"), new(T_GINT, observed), new(T_GINT, slot)),
|
||||
Ins(0x85, Op("exit")));
|
||||
|
||||
var sink = new RecordingTraceSink { TracingSteps = true };
|
||||
var vm = new VirtualMachine(script, Table, new CaptureHost(), sink: sink);
|
||||
vm.Globals[output] = 0x77; // stale value from a previous scene: already equal to this scene's terminal
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal("exit", vm.HaltReason);
|
||||
Assert.Equal(1, vm.Globals[setupCount]);
|
||||
Assert.Equal(4, vm.Globals[observed]);
|
||||
Assert.Equal(0, vm.Globals[SceneEntryGate]);
|
||||
Assert.Equal(0x77, vm.Globals[output]);
|
||||
Assert.Equal(2, sink.Events.Count(e => e.Kind == TraceEventKind.Step && e.Opcode == 0x140));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NonAdvLabeledServiceIsLeftStubbedAndDoesNotInjectSceneEntryGate()
|
||||
{
|
||||
var script = new Script
|
||||
{
|
||||
Name = "NON-ADV-140",
|
||||
Header = new ScriptHeader(0, 0, 0, 0, 0, 0),
|
||||
Instructions = new[]
|
||||
{
|
||||
Ins(0, 0x140, new(T_GINT, 0x699), new(T_STR, 0x2000), new(T_STR, 0x2001), new(T_GINT, 0x699)),
|
||||
Ins(9, Op("exit")),
|
||||
},
|
||||
IndexByOffset = new Dictionary<int, int> { [0] = 0, [9] = 1 },
|
||||
Strings = new Dictionary<int, string> { [0x2000] = "BIN", [0x2001] = "SC????.BIN" },
|
||||
};
|
||||
var sink = new RecordingTraceSink { TracingSteps = true };
|
||||
var vm = new VirtualMachine(script, Table, new CaptureHost(), sink: sink);
|
||||
vm.Globals[0x699] = 123;
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal(123, vm.Globals[0x699]);
|
||||
Assert.False(vm.Globals.ContainsKey(SceneEntryGate));
|
||||
Assert.Contains(sink.Events, e => e.Kind == TraceEventKind.Stub && e.Opcode == 0x140);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CoroutineHandlerOpsAreConsumedByTheHostSchedulerModel()
|
||||
{
|
||||
var script = Script(
|
||||
Ins(0, 0x7b, new(T_IMM, 0x30), new(T_IMM, 0x40)),
|
||||
Ins(5, 0x7c),
|
||||
Ins(6, Op("exit")));
|
||||
var sink = new RecordingTraceSink { TracingSteps = true };
|
||||
var vm = new VirtualMachine(script, Table, new CaptureHost(), sink: sink);
|
||||
vm.Run();
|
||||
|
||||
Assert.DoesNotContain(sink.Events, e => e.Kind == TraceEventKind.Stub && (e.Opcode == 0x7b || e.Opcode == 0x7c));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sc0000EntryRunsSetupAndFillsDistinctTextureSlots()
|
||||
{
|
||||
var script = Sys4Loader.Load(Paths.Scripts()["SC0000.BIN"], Table);
|
||||
var sink = new RecordingTraceSink { TracingSteps = true };
|
||||
var vm = new VirtualMachine(script, Table, new CaptureHost(),
|
||||
new VmOptions(HaltAtWaitForInput: true), sink: sink);
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal("wait-for-input", vm.HaltReason);
|
||||
Assert.Equal(0, vm.Globals[SceneEntryGate]);
|
||||
Assert.Equal(new long[] { 4, 5, 6, 7, 8, 9, 10, 11 },
|
||||
Enumerable.Range(0, 8).Select(i => vm.Globals[0x3239 + i * 3]).ToArray());
|
||||
Assert.Equal(2, sink.Events.Count(e => e.Kind == TraceEventKind.Step && e.Opcode == 0x140));
|
||||
}
|
||||
}
|
||||
@@ -39,24 +39,24 @@ public class GfxCommandBufferTests
|
||||
private static (int, Operand[]) Register(int handle) => (0x1a2, new[] { G(handle) });
|
||||
|
||||
[Fact]
|
||||
public void QueryReturnsMinusOneUntilRegistered_ThenTheHandle()
|
||||
public void QueryReturnsMinusOneUntilDrawBound_ThenSourceSlot()
|
||||
{
|
||||
// Native contract (docs/engine-re.md op 0x215/0x1a2): the query registry is populated ONLY by op 0x1a2
|
||||
// (gfx-cmd-register). Giving a handle geometry via set-geom (0x217) must NOT register it — query stays -1
|
||||
// so a CG handle takes label_12649's fresh branch. After 0x1a2, query returns the handle (native
|
||||
// map[handle]=handle; small system handles double as their surface slot).
|
||||
// Op 0x215 returns obj+4 from the retained gfx object. Geometry creates the object but leaves it unbound;
|
||||
// op 0x1a2's descriptor registry is unrelated. Draw-texture binds the source slot returned by the query.
|
||||
var t = T();
|
||||
var scene = ScriptAssembler.Assemble(t, "GFX", new List<(int, Operand[])>
|
||||
{
|
||||
MovGI(1, 0xcb2a), MovGI(2, 0xd), MovGI(3, 0),
|
||||
SetGeom3(1, 3, 3, 3), // 0xcb2a: geometry only, NOT registered
|
||||
Register(2), // 0xd: op 0x1a2 registers it
|
||||
Query(10, 1), Query(11, 2), Exit(),
|
||||
MovGI(1, 0xcb2a), MovGI(2, 6), MovGI(3, 0), MovGI(4, 200),
|
||||
SetGeom3(1, 3, 3, 3),
|
||||
Register(1),
|
||||
Query(10, 1),
|
||||
(0x1fb, new[] { G(1), G(2), I(0), I(0), G(4), G(4), G(3), G(3) }),
|
||||
Query(11, 1), Exit(),
|
||||
}, System.Array.Empty<string>());
|
||||
var vm = new VirtualMachine(scene, t, new RecordingHost());
|
||||
vm.Run();
|
||||
Assert.Equal(-1, vm.Globals[10]); // geometry-only CG handle -> -1 -> fresh branch (the bug fix)
|
||||
Assert.Equal(0xd, vm.Globals[11]); // 0x1a2-registered handle -> its value (== handle)
|
||||
Assert.Equal(-1, vm.Globals[10]);
|
||||
Assert.Equal(6, vm.Globals[11]);
|
||||
}
|
||||
|
||||
private static (int, Operand[]) BlitColor(int h, int x, int y, int alpha, int color)
|
||||
|
||||
@@ -4,19 +4,20 @@ using Xunit;
|
||||
public class GfxStateTests
|
||||
{
|
||||
[Fact]
|
||||
public void QueryRegistryIsPopulatedOnlyByRegister_NotByGeometryOps()
|
||||
public void QueryReturnsBoundSourceSlot_NotOperandRegistryValue()
|
||||
{
|
||||
// Native contract (docs/engine-re.md op 0x215/0x1a2): the op-0x215 query registry is populated ONLY by
|
||||
// op 0x1a2 (gfx-cmd-register). Merely giving a handle geometry (GetOrCreate, as the set-geom ops do)
|
||||
// must NOT make query-gfx-object return a slot for it — otherwise a CG handle (never 0x1a2-registered)
|
||||
// wrongly takes label_12649's existing branch and collapses off-screen.
|
||||
// Native op 0x215 queries the retained-object map and returns obj+4, the source slot set by draw-texture.
|
||||
// Geometry alone creates an object but leaves obj+4 at -1. Op 0x1a2 is a separate descriptor registry.
|
||||
var g = new GfxState();
|
||||
g.GetOrCreate(0xcb2a).V18 = (400, 600, 0); // geometry only, like the fresh CG-load branch
|
||||
Assert.Equal(-1, g.QuerySlot(0xcb2a)); // NOT registered => -1 => fresh branch (correct)
|
||||
g.GetOrCreate(0xcb2a).V18 = (400, 600, 0);
|
||||
Assert.Equal(-1, g.QuerySlot(0xcb2a));
|
||||
|
||||
g.Register(0xd); // op 0x1a2 registers a small system/UI handle
|
||||
Assert.Equal(0xd, g.QuerySlot(0xd)); // native map[handle]=handle; the value doubles as its slot
|
||||
Assert.Equal(-1, g.QuerySlot(0x9999)); // unknown -> -1 (matches native 0xffffffff)
|
||||
g.Register(0xcb2a);
|
||||
Assert.Equal(-1, g.QuerySlot(0xcb2a));
|
||||
|
||||
g.BindDraw(0xcb2a, 6, 0, 0, 200, 200, 10, 20);
|
||||
Assert.Equal(6, g.QuerySlot(0xcb2a));
|
||||
Assert.Equal(-1, g.QuerySlot(0x9999));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -31,12 +32,12 @@ public class GfxStateTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReleaseRemovesTheHandleFromTheQueryRegistry()
|
||||
public void ReleaseRemovesTheRetainedObject()
|
||||
{
|
||||
var g = new GfxState();
|
||||
g.Register(0x10);
|
||||
Assert.Equal(0x10, g.QuerySlot(0x10));
|
||||
g.Release(0x10); // op 0x1fa / 0x1f7 tear down the registration too
|
||||
g.BindDraw(0x10, 4, 0, 0, 10, 10, 0, 0);
|
||||
Assert.Equal(4, g.QuerySlot(0x10));
|
||||
g.Release(0x10);
|
||||
Assert.Equal(-1, g.QuerySlot(0x10));
|
||||
}
|
||||
|
||||
@@ -48,7 +49,8 @@ public class GfxStateTests
|
||||
public void EraseRangeRemovesHandlesInRange()
|
||||
{
|
||||
var g = new GfxState();
|
||||
g.Register(0x10); g.Register(0x11); g.Register(0x12); g.Register(0x20);
|
||||
g.BindDraw(0x10, 1, 0, 0, 1, 1, 0, 0); g.BindDraw(0x11, 2, 0, 0, 1, 1, 0, 0);
|
||||
g.BindDraw(0x12, 3, 0, 0, 1, 1, 0, 0); g.BindDraw(0x20, 4, 0, 0, 1, 1, 0, 0);
|
||||
g.EraseRange(0x10, 3); // count>1 → erase [0x10, 0x13)
|
||||
Assert.Equal(-1, g.QuerySlot(0x10));
|
||||
Assert.Equal(-1, g.QuerySlot(0x12));
|
||||
@@ -59,7 +61,7 @@ public class GfxStateTests
|
||||
public void EraseRangeCountLeOneErasesSingleHandle()
|
||||
{
|
||||
var g = new GfxState();
|
||||
g.Register(0x10); g.Register(0x11);
|
||||
g.BindDraw(0x10, 1, 0, 0, 1, 1, 0, 0); g.BindDraw(0x11, 2, 0, 0, 1, 1, 0, 0);
|
||||
g.EraseRange(0x10, 1); // count<=1 → single handle
|
||||
Assert.Equal(-1, g.QuerySlot(0x10));
|
||||
Assert.NotEqual(-1, g.QuerySlot(0x11));
|
||||
@@ -100,4 +102,20 @@ public class GfxStateTests
|
||||
g.EraseRange(0x10, 1);
|
||||
Assert.Empty(g.SnapshotVisibleObjects()); // erased => gone from the registry => not composited
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sc0000EffectCleanupQueryEnablesObjectEraseAndSurfaceRelease()
|
||||
{
|
||||
var g = new GfxState();
|
||||
g.SetSurface(6, 0x37, 0);
|
||||
g.BindDraw(0xcb8e, 6, 0, 0, 200, 200, 300, 180);
|
||||
|
||||
int slot = g.QuerySlot(0xcb8e); // mirrors post-effect cleanup at SC0000 0x3321
|
||||
Assert.Equal(6, slot);
|
||||
g.EraseRange(0xcb8e, 10); // op 0x1f7
|
||||
g.ClearSurface(slot); // op 0x1fa
|
||||
|
||||
Assert.Empty(g.SnapshotVisibleObjects());
|
||||
Assert.Equal(-1, g.QuerySlot(0xcb8e));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,15 +24,27 @@ public class TextureOpsTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SC0000FiresTextureOpsWithSlot0FullScreenSlideshow()
|
||||
public void SC0000FiresTextureOpsWithAssignedFullScreenSlot()
|
||||
{
|
||||
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||
var provider = Sys4ScriptProvider.Load(table);
|
||||
var session = new GameSession();
|
||||
foreach (var name in new[] { "INITCONFIG.BIN", "INIT2.BIN", "INIT.BIN" })
|
||||
session.RunScene(Sys4Loader.Load(Paths.Scripts()[name], table), table, new CaptureHost(), provider: provider);
|
||||
|
||||
var script = Sys4Loader.Load(Paths.Scripts()["SC0000.BIN"], table);
|
||||
var host = new RecHost();
|
||||
new VirtualMachine(script, table, host).Run();
|
||||
var vm = new VirtualMachine(script, table, host, new VmOptions(MaxSteps: 20_000_000), provider);
|
||||
foreach (var kv in session.Globals) vm.Globals[kv.Key] = kv.Value;
|
||||
foreach (var kv in session.GlobalStrings) vm.GlobalStrings[kv.Key] = kv.Value;
|
||||
vm.Run();
|
||||
|
||||
Assert.True(host.Creates > 0, "create-texture should fire");
|
||||
// The intro loads a sequence of full-screen images into slot 0; res 0x23 is the first bg.
|
||||
Assert.Contains(host.Sets, s => s.resId == 0x23 && s.slot == 0);
|
||||
Assert.Contains(host.Draws, d => d.slot == 0 && d.w == 0x320 && d.h == 0x258);
|
||||
// Boot + coroutine setup fill the handle/slot tables, so the first bg uses its assigned slot.
|
||||
Assert.Contains(host.Sets, s => s.resId == 0x23 && s.slot == 5);
|
||||
Assert.Contains(host.Draws, d => d.slot == 5 && d.w == 0x320 && d.h == 0x258);
|
||||
// AE001H is the ritual/magic-circle sheet. At the post-effect transition SC0000 explicitly queries its
|
||||
// retained object, erases the object group, and releases the returned slot; it must not survive the scene.
|
||||
Assert.DoesNotContain(vm.Gfx.SnapshotVisibleObjects(), o => o.SurfaceResId == 0x37);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,16 +63,14 @@ public sealed class GfxState
|
||||
}
|
||||
|
||||
// ---- geometry/draw object store (V18/V24/draw bind, the compositor's input) ----
|
||||
// Populated lazily by the geometry SET ops and draw-texture. Membership here does NOT mean the object is
|
||||
// in the op-0x215 query registry (that is a SEPARATE native structure; see _registry below).
|
||||
// Populated lazily by the geometry SET ops and draw-texture. Op 0x215 queries this same native map and
|
||||
// returns the object's live source slot (obj+4), or -1 when the handle has not been drawn/bound yet.
|
||||
private readonly Dictionary<long, GfxObject> _objects = new();
|
||||
|
||||
// ---- op-0x215 query registry (native std::map queried by gfx_op_0x215, populated ONLY by op 0x1a2
|
||||
// gfx-cmd-register -> FUN_0042cf70 hash insert). map[handle] = handle (native stores operand1 as the value;
|
||||
// small system/UI handles double as their surface slot). CG handles are NEVER 0x1a2-registered, so
|
||||
// query-gfx-object returns -1 for them and label_12649 takes its fresh branch (correct anchor from the
|
||||
// INIT2 arrays) instead of collapsing onto a fabricated slot. See docs/engine-re.md op 0x215/0x1a2. ----
|
||||
private readonly HashSet<long> _registry = new();
|
||||
// ---- opcode 0x1a2's operand-descriptor registry. Native op 0x1a2 hashes the lvalue descriptor string;
|
||||
// it is separate from the retained-object map queried by op 0x215. We retain membership for diagnostics
|
||||
// and teardown parity, but it does not make an undrawn gfx object queryable as a surface slot. ----
|
||||
private readonly HashSet<long> _operandRegistry = new();
|
||||
|
||||
private readonly Dictionary<long, long> _fieldTable = new(); // ctx+0x46d14 (0x216); no family writer -> default 0
|
||||
public long CurrentObject { get; private set; }
|
||||
@@ -103,35 +101,38 @@ public sealed class GfxState
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Op 0x1a2 (gfx-cmd-register, native FUN_0042d360 -> FUN_0042cf70 hash insert): add the handle to
|
||||
/// the op-0x215 query registry. Native inserts map[handle]=handle; QuerySlot returns that value (handle) or
|
||||
/// -1. Only this op populates the query registry — geometry/draw ops do not.</summary>
|
||||
public void Register(long handle) { lock (_lock) { _registry.Add(handle); } }
|
||||
/// <summary>Op 0x1a2: retain the operand's current value in the separate descriptor registry. This does
|
||||
/// not populate the retained-object map used by op 0x215.</summary>
|
||||
public void Register(long handle) { lock (_lock) { _operandRegistry.Add(handle); } }
|
||||
|
||||
public GfxObject? TryGet(long handle) => _objects.TryGetValue(handle, out var o) ? o : null;
|
||||
|
||||
/// <summary>Op 0x215 (query-gfx-object): native returns std::map::find(handle) — the registered value (=handle),
|
||||
/// or 0xffffffff (=-1) when the handle was never 0x1a2-registered. NOT a fabricated slot allocator.</summary>
|
||||
public int QuerySlot(long handle) => _registry.Contains(handle) ? (int)handle : -1;
|
||||
public bool IsRegistered(long handle) { lock (_lock) { return _registry.Contains(handle); } }
|
||||
/// <summary>Op 0x215: look up <paramref name="handle"/> in the retained gfx-object map and return obj+4,
|
||||
/// the live source-surface slot written by draw-texture, or -1 when absent/unbound.</summary>
|
||||
public int QuerySlot(long handle)
|
||||
{
|
||||
lock (_lock)
|
||||
return _objects.TryGetValue(handle, out var o) ? o.SourceSlot : -1;
|
||||
}
|
||||
public bool IsRegistered(long handle) { lock (_lock) { return _operandRegistry.Contains(handle); } }
|
||||
public long QueryField(long idx) => _fieldTable.TryGetValue(idx, out var v) ? v : 0;
|
||||
|
||||
public void Release(long handle)
|
||||
{
|
||||
lock (_lock) // re-entrant: EraseRange already holds _lock; op 0x1fa calls this directly
|
||||
lock (_lock) // re-entrant: EraseRange already holds _lock
|
||||
{
|
||||
_objects.Remove(handle);
|
||||
_registry.Remove(handle); // op 0x1fa/0x1f7 also tear down the query registration
|
||||
_operandRegistry.Remove(handle);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Op 0x1f7 semantics (native gfx_registry_erase_range @0x47d8b0): erase handles in
|
||||
/// <summary>Op 0x1f7 semantics (native gfx_object_erase_range @0x47d8b0): erase handles in
|
||||
/// [handle, handle+count) when count>1, else just <paramref name="handle"/>. It is a teardown/erase,
|
||||
/// NOT a create — objects are created lazily by the geometry SET ops (gfx_object_get_or_create).</summary>
|
||||
public void EraseRange(long handle, long count)
|
||||
{
|
||||
// Registry/slot cleanup (native gfx_registry_erase): removes the object from the registry, so it stops
|
||||
// compositing next frame. Faithful to the engine (the render loop iterates the registry).
|
||||
// Retained-object cleanup (native gfx_object_erase): removes the object from the map, so it stops
|
||||
// compositing next frame. Faithful to the engine (the render loop iterates the retained-object map).
|
||||
lock (_lock)
|
||||
{
|
||||
if (count > 1) for (long i = handle; i < handle + count; i++) Release(i);
|
||||
|
||||
@@ -11,5 +11,8 @@ internal sealed class ExecFrame
|
||||
public readonly Frame Locals = new();
|
||||
public readonly List<int> CallStack = new(); // intra-script `call` (op 0x8f) returns
|
||||
public readonly Dictionary<int, int> EmitSeen = new();
|
||||
public int? CoroutineYieldHandlerA; // op 0x7b: native per-frame handler PCs
|
||||
public int? CoroutineYieldHandlerB;
|
||||
public readonly Dictionary<int, int> CoroutineYieldVisits = new(); // instruction index -> visits
|
||||
public ExecFrame(Script script, int pc) { Script = script; Pc = pc; }
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ public sealed class VirtualMachine
|
||||
private const long NoJump = 0xFFFFFFFF;
|
||||
private const int HALT = int.MinValue;
|
||||
private const int FRAME_RETURN = int.MinValue + 1;
|
||||
private const int SceneEntryCoroutineGate = 0xaba5c;
|
||||
private const int T_IMM = 0, T_STR = 2, T_GINT = 3, T_GFLOAT = 4, T_GSTR = 5, T_GPTR = 6,
|
||||
T_LINT = 9, T_LFLOAT = 10, T_LSTR = 11, T_LPTR = 12;
|
||||
|
||||
@@ -40,6 +41,33 @@ public sealed class VirtualMachine
|
||||
private static long PyMod(long a, long b) { if (b == 0) return 0; long r = a % b; if (r != 0 && (r < 0) != (b < 0)) r += b; return r; }
|
||||
|
||||
private static bool IsStr(Operand o) => o.Type == T_STR || o.Type == T_GSTR || o.Type == T_LSTR;
|
||||
private static bool SameOperand(Operand a, Operand b) => a.Type == b.Type && a.Value == b.Value;
|
||||
|
||||
private static bool IsAdvLabeledYield(Script script, Instruction ins)
|
||||
=> ins.Opcode == 0x140 && ins.Args.Count >= 4
|
||||
&& ins.Args[1].Type == T_STR && ins.Args[2].Type == T_STR
|
||||
&& script.GetString((int)ins.Args[1].Value) == "LABEL"
|
||||
&& script.GetString((int)ins.Args[2].Value) == "J";
|
||||
|
||||
private bool TryGetAdvYieldTerminal(int pc, Operand output, out long terminal)
|
||||
{
|
||||
terminal = 0;
|
||||
if (pc + 2 >= _cur.Script.Instructions.Count) return false;
|
||||
var setTerminal = _cur.Script.Instructions[pc + 1];
|
||||
var compare = _cur.Script.Instructions[pc + 2];
|
||||
if (_t.Label(setTerminal.Opcode) != "mov" || setTerminal.Args.Count < 2
|
||||
|| setTerminal.Args[1].Type != T_IMM
|
||||
|| _t.Label(compare.Opcode) != "eq" || compare.Args.Count < 3)
|
||||
return false;
|
||||
|
||||
var terminalOperand = setTerminal.Args[0];
|
||||
bool comparesTerminalToOutput =
|
||||
(SameOperand(compare.Args[1], terminalOperand) && SameOperand(compare.Args[2], output))
|
||||
|| (SameOperand(compare.Args[2], terminalOperand) && SameOperand(compare.Args[1], output));
|
||||
if (!comparesTerminalToOutput) return false;
|
||||
terminal = Read(setTerminal.Args[1]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private long Read(Operand op) => op.Type switch
|
||||
{
|
||||
@@ -103,6 +131,11 @@ public sealed class VirtualMachine
|
||||
|
||||
public void Run(int entryOffset = 0)
|
||||
{
|
||||
// The native scheduler supplies this scene-entry state outside script-visible global writes.
|
||||
// Restrict it to the byte-identical ADV LABEL/J idiom; op 0x140 also has an unrelated TITLE use.
|
||||
if (entryOffset == 0 && _s.Instructions.Any(ins => IsAdvLabeledYield(_s, ins)))
|
||||
Globals[SceneEntryCoroutineGate] = 1;
|
||||
|
||||
var top = new ExecFrame(_s, _s.IndexByOffset.TryGetValue(entryOffset, out var idx) ? idx : 0);
|
||||
var outcome = RunFrame(top, FrameCause.TopScene);
|
||||
if (outcome == FrameOutcome.RanOff) HaltReason ??= "pc-out-of-range";
|
||||
@@ -177,6 +210,35 @@ public sealed class VirtualMachine
|
||||
long tgt = Read(a[0]) != 0 ? a[1].Value : a[2].Value;
|
||||
return tgt == NoJump ? pc + 1 : _cur.Script.IndexByOffset.GetValueOrDefault((int)tgt, pc + 1);
|
||||
}
|
||||
case "u0041ADB0":
|
||||
case "coroutine-save-yield-handlers": // 0x7b: retain native handler metadata
|
||||
_cur.CoroutineYieldHandlerA = (int)Read(a[0]);
|
||||
_cur.CoroutineYieldHandlerB = (int)Read(a[1]);
|
||||
return pc + 1;
|
||||
case "u00416A90":
|
||||
case "coroutine-resume": // 0x7c: host FrameYield/FrameClock owns re-entry
|
||||
return pc + 1;
|
||||
case "u0041F9C0":
|
||||
case "coroutine-label-yield": // 0x140: bounded host model for LABEL/J only
|
||||
{
|
||||
if (!IsAdvLabeledYield(_cur.Script, ins))
|
||||
{
|
||||
if (_sink.TracingSteps) _sink.Emit(TraceEvent.Stub(op, pc));
|
||||
return pc + 1;
|
||||
}
|
||||
if (!TryGetAdvYieldTerminal(pc, a[0], out long terminal))
|
||||
{
|
||||
HaltReason ??= $"coroutine-yield-pattern@0x{ins.Offset:x}";
|
||||
return HALT;
|
||||
}
|
||||
|
||||
int visits = _cur.CoroutineYieldVisits.GetValueOrDefault(pc);
|
||||
_cur.CoroutineYieldVisits[pc] = visits + 1;
|
||||
// First visit must enter setup even if out retained this same terminal from a prior scene.
|
||||
// Every later visit returns the script-encoded terminal and exits the bounded loop.
|
||||
Write(a[0], visits == 0 ? (terminal == 0 ? 1 : 0) : terminal);
|
||||
return pc + 1;
|
||||
}
|
||||
case "exit":
|
||||
case "exit-script": return FRAME_RETURN;
|
||||
case "call-script":
|
||||
@@ -246,7 +308,7 @@ public sealed class VirtualMachine
|
||||
{
|
||||
long h = Read(a[1]);
|
||||
System.Console.Error.WriteLine($"[query] handle=0x{h:x} handleOp=(type={a[1].Type} val=0x{a[1].Value:x}) " +
|
||||
$"-> QuerySlot={Gfx.QuerySlot(h)} registered={Gfx.IsRegistered(h)}");
|
||||
$"-> QuerySlot={Gfx.QuerySlot(h)} objectPresent={Gfx.TryGet(h) != null}");
|
||||
}
|
||||
Write(a[0], Gfx.QuerySlot(Read(a[1]))); return pc + 1;
|
||||
case "query-gfx-field?": // 0x216 (out)(idx)
|
||||
@@ -293,13 +355,13 @@ public sealed class VirtualMachine
|
||||
{
|
||||
var o = Gfx.GetOrCreate(Read(a[0])); o.Field68 = Read(a[1]); o.Field6c = Read(a[2]); return pc + 1;
|
||||
}
|
||||
case "gfx-cmd-register": // 0x1a2 (handle) — insert into the op-0x215 query registry (native
|
||||
// FUN_0042d360 -> FUN_0042cf70 hash insert; the ONLY populator of that map)
|
||||
case "gfx-cmd-register": // 0x1a2 (handle) — operand-descriptor hash insert; separate from
|
||||
// op 0x215's retained gfx-object/source-slot lookup
|
||||
Gfx.Register(Read(a[0])); return pc + 1;
|
||||
case "gfx-elem-erase": // 0x1f7 (handle)(count) — erase registry range (teardown, NOT create)
|
||||
case "gfx-elem-erase": // 0x1f7 (handle)(count) — erase retained-object range
|
||||
Gfx.EraseRange(Read(a[0]), Read(a[1])); return pc + 1;
|
||||
case "gfx-elem-release": // 0x1fa (handle)
|
||||
Gfx.Release(Read(a[0])); return pc + 1;
|
||||
case "gfx-elem-release": // 0x1fa (surface slot)
|
||||
Gfx.ClearSurface((int)Read(a[0])); return pc + 1;
|
||||
case "gfx-blit-color": // 0x202 (handle)(x)(y)(alpha)(color) — static alpha/tint (anim interp deferred)
|
||||
Gfx.SetObjectColor(Read(a[0]), GfxState.PackColor(Read(a[3]), Read(a[4]))); return pc + 1;
|
||||
case "gfx-draw-color": // 0x203 (handle)(v)(alpha)(color) — static alpha/tint
|
||||
|
||||
Reference in New Issue
Block a user