From 648ccca2e73fd051bac11ef470bce5273f956823 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Tue, 7 Jul 2026 17:48:31 -0400 Subject: [PATCH] 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 --- .../Age.Engine.Tests/GfxCommandBufferTests.cs | 55 +++++++++++++++++++ engine/Age.Engine/Vm/VirtualMachine.cs | 33 +++++++++++ 2 files changed, 88 insertions(+) create mode 100644 engine/Age.Engine.Tests/GfxCommandBufferTests.cs diff --git a/engine/Age.Engine.Tests/GfxCommandBufferTests.cs b/engine/Age.Engine.Tests/GfxCommandBufferTests.cs new file mode 100644 index 0000000..07af03b --- /dev/null +++ b/engine/Age.Engine.Tests/GfxCommandBufferTests.cs @@ -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()); + + [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()); + 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()); + 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 + } +} diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs index 6426db4..c637d41 100644 --- a/engine/Age.Engine/Vm/VirtualMachine.cs +++ b/engine/Age.Engine/Vm/VirtualMachine.cs @@ -23,6 +23,7 @@ public sealed class VirtualMachine public Dictionary Globals { get; } = new(); public Dictionary GlobalStrings { get; } = new(); + public GfxState Gfx { get; } = new(); public List<(int Offset, string Text, string Script)> Emitted { get; } = new(); public string? HaltReason { 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-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: // 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.