From fc9e46ee85dadae1389c7111c871977125faeb1b Mon Sep 17 00:00:00 2001 From: gamer147 Date: Tue, 7 Jul 2026 19:45:28 -0400 Subject: [PATCH] feat(gfx): retained DrawLayer list in GfxState (thread-safe) Co-Authored-By: Claude Opus 4.8 --- engine/Age.Engine.Tests/GfxStateTests.cs | 26 ++++++++++++++++++++ engine/Age.Engine/Model/GfxState.cs | 30 ++++++++++++++++++++++-- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/engine/Age.Engine.Tests/GfxStateTests.cs b/engine/Age.Engine.Tests/GfxStateTests.cs index 802a785..fc7be97 100644 --- a/engine/Age.Engine.Tests/GfxStateTests.cs +++ b/engine/Age.Engine.Tests/GfxStateTests.cs @@ -59,4 +59,30 @@ public class GfxStateTests Assert.Equal(-1, g.QuerySlot(0x10)); Assert.NotEqual(-1, g.QuerySlot(0x11)); } + + [Fact] + public void LayersAppendInOrderAndUpdateInPlace() + { + var g = new GfxState(); + g.AddOrUpdateLayer(new DrawLayer(0xA, 4, 0, 0, 800, 600, 0, 0)); + g.AddOrUpdateLayer(new DrawLayer(0xB, 5, 0, 0, 200, 200, 100, 100)); + g.AddOrUpdateLayer(new DrawLayer(0xA, 4, 0, 0, 800, 600, 0, 50)); // re-draw A -> update in place + var s = g.SnapshotLayers(); + Assert.Equal(2, s.Count); + Assert.Equal(0xA, s[0].Handle); // order preserved (A still first) + Assert.Equal(50, s[0].DstY); // updated + Assert.Equal(0xB, s[1].Handle); + } + + [Fact] + public void EraseRangeAlsoDropsLayers() + { + var g = new GfxState(); + g.AddOrUpdateLayer(new DrawLayer(0x10, 4, 0, 0, 10, 10, 0, 0)); + g.AddOrUpdateLayer(new DrawLayer(0x20, 5, 0, 0, 10, 10, 0, 0)); + g.EraseRange(0x10, 1); + var s = g.SnapshotLayers(); + Assert.Single(s); + Assert.Equal(0x20, s[0].Handle); + } } diff --git a/engine/Age.Engine/Model/GfxState.cs b/engine/Age.Engine/Model/GfxState.cs index 65f9e0c..95bd83e 100644 --- a/engine/Age.Engine/Model/GfxState.cs +++ b/engine/Age.Engine/Model/GfxState.cs @@ -1,5 +1,10 @@ namespace Age.Engine.Model; +/// A retained composite layer captured when the VM executes draw-texture — the host clears and +/// re-composites the layer set each frame (replacing the immediate-mode permanent canvas). Keyed by the +/// object so a re-draw updates in place and an erase (op 0x1f7) removes it. +public readonly record struct DrawLayer(long Handle, int Slot, int SrcX, int SrcY, int W, int H, int DstX, int DstY); + /// Host-agnostic model of the AGE native gfx command-buffer (reversed in /// docs/engine-re.md, gfx op-contract table). One registry maps an object handle to a GfxObject — the /// native ctx+0x408 map that op 0x215 queries and the geometry get/set ops share. Each object carries a @@ -59,10 +64,31 @@ public sealed class GfxState /// NOT a create — objects are created lazily by the geometry SET ops (gfx_object_get_or_create). public void EraseRange(long handle, long count) { - if (count > 1) for (long i = handle; i < handle + count; i++) Release(i); - else Release(handle); + if (count > 1) for (long i = handle; i < handle + count; i++) { Release(i); RemoveLayers(i); } + else { Release(handle); RemoveLayers(handle); } } + // ---- retained composite layers (mutated on the VM thread; snapshotted on the host render thread) ---- + private readonly List _layers = new(); + private readonly object _lock = new(); + + /// Record a draw: update the layer with this handle in place (preserving composite order), else + /// append. Order = execution order = paint order. + public void AddOrUpdateLayer(DrawLayer l) + { + lock (_lock) + { + for (int i = 0; i < _layers.Count; i++) + if (_layers[i].Handle == l.Handle) { _layers[i] = l; return; } + _layers.Add(l); + } + } + + public void RemoveLayers(long handle) { lock (_lock) { _layers.RemoveAll(l => l.Handle == handle); } } + + /// An immutable snapshot of the current layers, in composite order (for the host render loop). + public IReadOnlyList SnapshotLayers() { lock (_lock) { return _layers.ToArray(); } } + /// Pack (alpha, rgb) → 0xAARRGGBB, matching op 0x202/0x203's handler bit-manipulation for the /// common (non-negative-sentinel) case. The alpha<0 / color<0 native-fetch path is deferred. public static long PackColor(long alpha, long color)