feat(gfx): execute gfx command-buffer query/set/lifecycle ops in the VM

Gfx property on VirtualMachine + dispatch cases for the query ops (0x215/0x216/
0x218/0x21a), set ops (0x217/0x219/0x1ff/0x212/0x213) and lifecycle (0x1a2/0x1f7/
0x1fa). Query ops write results to operands like get-texture-size; set ops mutate
GfxState. Round-trip + distinct-slot tests pass; full suite 36 green (parity held).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-07-07 17:48:31 -04:00
parent 7c03371519
commit 648ccca2e7
2 changed files with 88 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
using System.Collections.Generic;
using Age.Engine.Model;
using Age.Engine.Sys4;
using Age.Engine.Vm;
using Xunit;
public class GfxCommandBufferTests
{
private static OpcodeTable T() => OpcodeTableJson.Load(Paths.OpcodesJson);
private static Operand G(int addr) => new(3, addr); // global-int
private static Operand I(long v) => new(0, v); // immediate
// op ctor helpers (0x217 set-geom3, 0x218 get-geom3, 0x215 query, 0x55 mov, 0x2 exit)
private static (int, Operand[]) SetGeom3(int handle, int a, int b, int c) => (0x217, new[] { G(handle), G(a), G(b), G(c) });
private static (int, Operand[]) GetGeom3(int handle, int a, int b, int c) => (0x218, new[] { G(handle), G(a), G(b), G(c) });
private static (int, Operand[]) Query(int outAddr, int handle) => (0x215, new[] { G(outAddr), G(handle) });
private static (int, Operand[]) MovGI(int dst, long v) => (0x55, new[] { G(dst), I(v) });
private static (int, Operand[]) Exit() => (0x2, System.Array.Empty<Operand>());
[Fact]
public void SetThenGetGeom3RoundTripsThroughTheObject()
{
var t = T();
// g[1]=handle; set V18 from g[2,3,4]=(10,20,30); read V18 back into g[5,6,7]; then exit.
var scene = ScriptAssembler.Assemble(t, "GFX", new List<(int, Operand[])>
{
MovGI(1, 0x1000), MovGI(2, 10), MovGI(3, 20), MovGI(4, 30),
SetGeom3(1, 2, 3, 4),
GetGeom3(1, 5, 6, 7),
Exit(),
}, System.Array.Empty<string>());
var vm = new VirtualMachine(scene, t, new RecordingHost());
vm.Run();
Assert.Equal(10, vm.Globals[5]);
Assert.Equal(20, vm.Globals[6]);
Assert.Equal(30, vm.Globals[7]);
}
[Fact]
public void QueryReturnsDistinctSlotsPerHandle_NotZero()
{
var t = T();
// create two objects via set-geom, then query each into g[10], g[11].
var scene = ScriptAssembler.Assemble(t, "GFX", new List<(int, Operand[])>
{
MovGI(1, 0x1000), MovGI(2, 0x2000), MovGI(3, 0),
SetGeom3(1, 3, 3, 3), SetGeom3(2, 3, 3, 3),
Query(10, 1), Query(11, 2), Exit(),
}, System.Array.Empty<string>());
var vm = new VirtualMachine(scene, t, new RecordingHost());
vm.Run();
Assert.NotEqual(0, vm.Globals[10]); // not collapsed to slot 0
Assert.NotEqual(vm.Globals[10], vm.Globals[11]); // distinct slots => no collapse
}
}

View File

@@ -23,6 +23,7 @@ public sealed class VirtualMachine
public Dictionary<int, long> Globals { get; } = new(); public Dictionary<int, long> Globals { get; } = new();
public Dictionary<int, string> GlobalStrings { get; } = new(); public Dictionary<int, string> GlobalStrings { get; } = new();
public GfxState Gfx { get; } = new();
public List<(int Offset, string Text, string Script)> Emitted { get; } = new(); public List<(int Offset, string Text, string Script)> Emitted { get; } = new();
public string? HaltReason { get; private set; } public string? HaltReason { get; private set; }
public long Steps { get; private set; } public long Steps { get; private set; }
@@ -225,6 +226,38 @@ public sealed class VirtualMachine
} }
case "play-bgm": _host.PlayBgm(Read(a[0])); return pc + 1; case "play-bgm": _host.PlayBgm(Read(a[0])); return pc + 1;
case "play-voice": _host.PlayVoice(Read(a[0])); return pc + 1; case "play-voice": _host.PlayVoice(Read(a[0])); return pc + 1;
// ---- gfx command-buffer ops (VM-internal GfxState; docs/engine-re.md op-contract table) ----
case "query-gfx-object?": // 0x215 (out)(handle) -> slot | -1
Write(a[0], Gfx.QuerySlot(Read(a[1]))); return pc + 1;
case "query-gfx-field?": // 0x216 (out)(idx)
Write(a[0], Gfx.QueryField(Read(a[1]))); return pc + 1;
case "get-gfx-geom3?": // 0x218 (handle)(outA)(outB)(outC) <- V18
{
var v = Gfx.TryGet(Read(a[0]))?.V18 ?? default;
Write(a[1], v.X); Write(a[2], v.Y); Write(a[3], v.Z); return pc + 1;
}
case "get-gfx-geom3-b?": // 0x21a (handle)(outA)(outB)(outC) <- V24
{
var v = Gfx.TryGet(Read(a[0]))?.V24 ?? default;
Write(a[1], v.X); Write(a[2], v.Y); Write(a[3], v.Z); return pc + 1;
}
case "set-gfx-geom3": // 0x217 (handle)(a)(b)(c) -> V18
Gfx.GetOrCreate(Read(a[0])).V18 = (Read(a[1]), Read(a[2]), Read(a[3])); return pc + 1;
case "set-gfx-geom3-b": // 0x219 (handle)(a)(b)(c) -> V24
Gfx.GetOrCreate(Read(a[0])).V24 = (Read(a[1]), Read(a[2]), Read(a[3])); return pc + 1;
case "set-gfx-geom3-c": // 0x1ff (handle)(a)(b)(c) -> V16c
Gfx.GetOrCreate(Read(a[0])).V16c = (Read(a[1]), Read(a[2]), Read(a[3])); return pc + 1;
case "set-gfx-field64": // 0x212 (idx)(val)
Gfx.GetOrCreate(Read(a[0])).Field64 = Read(a[1]); return pc + 1;
case "set-gfx-xy": // 0x213 (idx)(x)(y)
{
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 (val) — register/select
case "gfx-elem-create": // 0x1f7 (handle)(count) — ensure object + slot
Gfx.GetOrCreate(Read(a[0])); return pc + 1;
case "gfx-elem-release": // 0x1fa (handle)
Gfx.Release(Read(a[0])); return pc + 1;
default: default:
// Stub is per-instruction frequency (the VM handles ~30 ops; the rest hit here, e.g. // Stub is per-instruction frequency (the VM handles ~30 ops; the rest hit here, e.g.
// 0x258/0x259 stmt markers appear en masse), so gate it with Step — else --trace floods. // 0x258/0x259 stmt markers appear en masse), so gate it with Step — else --trace floods.