diff --git a/engine/Age.Engine.Tests/GfxCommandBufferTests.cs b/engine/Age.Engine.Tests/GfxCommandBufferTests.cs index 07af03b..ada356e 100644 --- a/engine/Age.Engine.Tests/GfxCommandBufferTests.cs +++ b/engine/Age.Engine.Tests/GfxCommandBufferTests.cs @@ -52,4 +52,21 @@ public class GfxCommandBufferTests Assert.NotEqual(0, vm.Globals[10]); // not collapsed to slot 0 Assert.NotEqual(vm.Globals[10], vm.Globals[11]); // distinct slots => no collapse } + + private static (int, Operand[]) BlitColor(int h, int x, int y, int alpha, int color) + => (0x202, new[] { G(h), G(x), G(y), G(alpha), G(color) }); + + [Fact] + public void BlitColorStoresPackedArgbOnTheObject() + { + var t = T(); + var scene = ScriptAssembler.Assemble(t, "GFX", new List<(int, Operand[])> + { + MovGI(1, 0x1000), MovGI(2, 0), MovGI(3, 0), MovGI(4, 0x80), MovGI(5, 0x112233), + BlitColor(1, 2, 3, 4, 5), Exit(), + }, System.Array.Empty()); + var vm = new VirtualMachine(scene, t, new RecordingHost()); + vm.Run(); + Assert.Equal(0x80_112233L, vm.Gfx.TryGet(0x1000)!.Color); + } } diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs index c637d41..5850f3b 100644 --- a/engine/Age.Engine/Vm/VirtualMachine.cs +++ b/engine/Age.Engine/Vm/VirtualMachine.cs @@ -258,10 +258,26 @@ public sealed class VirtualMachine Gfx.GetOrCreate(Read(a[0])); return pc + 1; case "gfx-elem-release": // 0x1fa (handle) Gfx.Release(Read(a[0])); return pc + 1; + case "gfx-blit-color": // 0x202 (handle)(x)(y)(alpha)(color) — blend deferred + Gfx.GetOrCreate(Read(a[0])).Color = GfxState.PackColor(Read(a[3]), Read(a[4])); + WarnAlphaDeferredOnce(); return pc + 1; + case "gfx-draw-color": // 0x203 (handle)(v)(alpha)(color) — blend deferred + Gfx.GetOrCreate(Read(a[0])).Color = GfxState.PackColor(Read(a[2]), Read(a[3])); + WarnAlphaDeferredOnce(); 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. if (_sink.TracingSteps) _sink.Emit(TraceEvent.Stub(op, pc)); return pc + 1; } } + + // Colored-draw ops (0x202/0x203) store the packed color on the object now; the actual alpha/additive + // blend in the compositor is deferred. Surface it once (not silently) via the trace sink — observe-only, + // so parity holds. See docs/superpowers/specs/2026-07-07-gfx-command-buffer-design.md (Deferrals). + private bool _warnedAlpha; + private void WarnAlphaDeferredOnce() + { + if (_warnedAlpha) return; _warnedAlpha = true; + _sink.Emit(TraceEvent.Stub(0x202, -1)); + } }