diff --git a/docs/opcode-reference.md b/docs/opcode-reference.md
index 1fa95c4..23423fd 100644
--- a/docs/opcode-reference.md
+++ b/docs/opcode-reference.md
@@ -64,10 +64,10 @@ This also names the whole call graph statically (build/callscript-names.json).
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra: real handler FUN_0042d360 (via dispatch table ctx[0x26c93+op]); sets *(ctx+0x53d88+ctx[0x53d14]*0x78)=3, sprintf("%c%8.8x",3,op1), FUN_0042cf70 (hash insert; counterpart of op 0x215 find). NOT save/scene (raw Kelebek VA 0x428010 drifted to op 0x1ac save handler). See docs/engine-re.md
-### 0x1f7 `gfx-elem-create` (gfx-elem-create, argc 2)
-- **summary:** 0x1f7 (handle)(count) — gfx cmd-type 5. Handler gfx_op_0x1f7_elem_create @0x422270: if count>1 → FUN_0047d8b0(handle,count) (array element), else FUN_0047d850(handle) (single). In label_12649 it selects the object after a 0x215 slot-query, before 0x1fa clears the slot. See docs/engine-re.md gfx op-contract table.
+### 0x1f7 `gfx-elem-erase` (gfx-elem-erase, argc 2)
+- **summary:** 0x1f7 (handle)(count) — gfx cmd-type 5. Handler gfx_op_0x1f7_elem_erase @0x422270: ERASES registry handles — if count>1 → gfx_registry_erase_range(handle,count) [erase [handle, handle+count)], else gfx_registry_erase(handle). It is a TEARDOWN/erase, NOT a create (corrects the earlier 'gfx-elem-create' reading). In label_12649 it runs after a 0x215 slot-query, before 0x1fa releases the slot. Objects are created lazily by the geometry SET ops (gfx_object_get_or_create). See docs/engine-re.md gfx op-contract table.
- **grounding:** source=investigation, confidence=high
-- **evidence:** Ghidra handler 0x422270 (dispatch ctx[0x26c93+0x1f7]); FUN_0047d8b0/FUN_0047d850 on operand1 gated by operand2>1.
+- **evidence:** Ghidra handler 0x422270 (dispatch ctx[0x26c93+0x1f7]); count>1 → gfx_registry_erase_range @0x47d8b0 (loops gfx_registry_erase @0x47d850 over [op1,op1+op2)), else gfx_registry_erase(op1). gfx_registry_erase does map.find+erase on the ctx+0x408 registry.
### 0x1f8 `create-texture` (create-texture, argc 4)
- **summary:** Allocate/prepare a texture slot: (slot, width, height, flag). e.g. `create-texture 0xd 0x190 0x1e 0x0` = slot 13, 400x30.
diff --git a/engine/Age.Engine.Tests/GfxStateTests.cs b/engine/Age.Engine.Tests/GfxStateTests.cs
index 13d9e1c..802a785 100644
--- a/engine/Age.Engine.Tests/GfxStateTests.cs
+++ b/engine/Age.Engine.Tests/GfxStateTests.cs
@@ -38,4 +38,25 @@ public class GfxStateTests
[Fact]
public void PackColorPacksArgb()
=> Assert.Equal(0x80_112233L, GfxState.PackColor(0x80, 0x112233));
+
+ [Fact]
+ public void EraseRangeRemovesHandlesInRange()
+ {
+ var g = new GfxState();
+ g.GetOrCreate(0x10); g.GetOrCreate(0x11); g.GetOrCreate(0x12); g.GetOrCreate(0x20);
+ g.EraseRange(0x10, 3); // count>1 → erase [0x10, 0x13)
+ Assert.Equal(-1, g.QuerySlot(0x10));
+ Assert.Equal(-1, g.QuerySlot(0x12));
+ Assert.NotEqual(-1, g.QuerySlot(0x20)); // outside the range, kept
+ }
+
+ [Fact]
+ public void EraseRangeCountLeOneErasesSingleHandle()
+ {
+ var g = new GfxState();
+ g.GetOrCreate(0x10); g.GetOrCreate(0x11);
+ g.EraseRange(0x10, 1); // count<=1 → single handle
+ Assert.Equal(-1, g.QuerySlot(0x10));
+ Assert.NotEqual(-1, g.QuerySlot(0x11));
+ }
}
diff --git a/engine/Age.Engine/Model/GfxState.cs b/engine/Age.Engine/Model/GfxState.cs
index f95fef7..65f9e0c 100644
--- a/engine/Age.Engine/Model/GfxState.cs
+++ b/engine/Age.Engine/Model/GfxState.cs
@@ -54,6 +54,15 @@ public sealed class GfxState
if (_objects.TryGetValue(handle, out var o)) { if (o.Slot >= 0) _free.Add(o.Slot); _objects.Remove(handle); }
}
+ /// Op 0x1f7 semantics (native gfx_registry_erase_range @0x47d8b0): erase handles in
+ /// [handle, handle+count) when count>1, else just . It is a teardown/erase,
+ /// 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);
+ }
+
/// 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)
diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs
index 5850f3b..47d79da 100644
--- a/engine/Age.Engine/Vm/VirtualMachine.cs
+++ b/engine/Age.Engine/Vm/VirtualMachine.cs
@@ -253,9 +253,10 @@ 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 (val) — register/select
- case "gfx-elem-create": // 0x1f7 (handle)(count) — ensure object + slot
+ case "gfx-cmd-register": // 0x1a2 (val) — register/insert
Gfx.GetOrCreate(Read(a[0])); return pc + 1;
+ case "gfx-elem-erase": // 0x1f7 (handle)(count) — erase registry range (teardown, NOT create)
+ 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-blit-color": // 0x202 (handle)(x)(y)(alpha)(color) — blend deferred
diff --git a/vm-map/opcodes.toml b/vm-map/opcodes.toml
index 749ac85..0069787 100644
--- a/vm-map/opcodes.toml
+++ b/vm-map/opcodes.toml
@@ -4500,19 +4500,19 @@ evidence = ""
[[opcode]]
op = 0x1f7
-label = "gfx-elem-create"
+label = "gfx-elem-erase"
argc = 2
abi_source = "kelebek+decode-validated"
[opcode.semantics]
-name = "gfx-elem-create"
+name = "gfx-elem-erase"
category = "draw"
-summary = "0x1f7 (handle)(count) — gfx cmd-type 5. Handler gfx_op_0x1f7_elem_create @0x422270: if count>1 → FUN_0047d8b0(handle,count) (array element), else FUN_0047d850(handle) (single). In label_12649 it selects the object after a 0x215 slot-query, before 0x1fa clears the slot. See docs/engine-re.md gfx op-contract table."
+summary = "0x1f7 (handle)(count) — gfx cmd-type 5. Handler gfx_op_0x1f7_elem_erase @0x422270: ERASES registry handles — if count>1 → gfx_registry_erase_range(handle,count) [erase [handle, handle+count)], else gfx_registry_erase(handle). It is a TEARDOWN/erase, NOT a create (corrects the earlier 'gfx-elem-create' reading). In label_12649 it runs after a 0x215 slot-query, before 0x1fa releases the slot. Objects are created lazily by the geometry SET ops (gfx_object_get_or_create). See docs/engine-re.md gfx op-contract table."
noop_headless = false
source = "investigation"
confidence = "high"
depends_on = []
-evidence = "Ghidra handler 0x422270 (dispatch ctx[0x26c93+0x1f7]); FUN_0047d8b0/FUN_0047d850 on operand1 gated by operand2>1."
+evidence = "Ghidra handler 0x422270 (dispatch ctx[0x26c93+0x1f7]); count>1 → gfx_registry_erase_range @0x47d8b0 (loops gfx_registry_erase @0x47d850 over [op1,op1+op2)), else gfx_registry_erase(op1). gfx_registry_erase does map.find+erase on the ctx+0x408 registry."
[[opcode.semantics.args]]
i = 1