From 280705d69cdf97a9dd50f0553dbd2f0a5b756aea Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sun, 19 Jul 2026 10:43:52 -0400 Subject: [PATCH] Correct ADV History layout addressing --- docs/engine-re.md | 14 ++++ docs/opcode-reference.md | 11 ++- docs/phase-a-slice-plan.md | 17 ++++ .../HistoryInteractionOpsTests.cs | 46 +++++++++- engine/Age.Engine/Vm/Frame.cs | 22 ++++- engine/Age.Engine/Vm/VirtualMachine.cs | 84 ++++++++++++++----- godot/Main.cs | 1 - tools/vm0.py | 42 +++++++--- vm-map/opcodes.toml | 11 +-- 9 files changed, 200 insertions(+), 48 deletions(-) diff --git a/docs/engine-re.md b/docs/engine-re.md index 19c29f8..e149218 100644 --- a/docs/engine-re.md +++ b/docs/engine-re.md @@ -1366,6 +1366,20 @@ page. This is frame-scoped callback state, not a HISTORY name/offset special cas existing HIDEWIN scheduler family. A real regression activates x=684 in SC0000, runs unmodified HISTORY, selects/closes region 8, observes retained text, and returns to the same single page wait. +Manual comparison exposed a separate typed-address bug in the first display build. `HISTORY.BIN` copies its +button x/y tables into local integer cells `0x4` and `0x68`, then op `0x61` takes local pointers to selected +elements for `draw-texture`. A local base operand names that local cell; it is not a local value containing a +global address. The VM now retains the local/global domain in pointer values, so reads and writes through a +local pointer reach the correct bank. This restores the six controls at x=768/y=121..411, the close control +at x=768/y=549, and the hovered-row highlight while preserving RECOVER's global-array pointer behavior. +The Python A0 oracle uses the same typed-address model. + +The History text batches already carry the native x origin 65 and cursor x 45. Their Godot labels were +created with a full-rect anchor preset before being parented, which discarded the intended absolute +placement in the live UI and caused a Godot parent/layout diagnostic. Dynamically composited ADV labels now +use the default top-left anchors and their explicit position/size, yielding the native text x=110 and +avoiding that diagnostic. + History's remaining work is stored voice replay through `0x1bd` and the `0xd3/0xd4/0xd5` smooth-scroll callback scheduler. None changes backlog ownership or requires choosing a save/profile backend. diff --git a/docs/opcode-reference.md b/docs/opcode-reference.md index d3b2bc6..a560e89 100644 --- a/docs/opcode-reference.md +++ b/docs/opcode-reference.md @@ -149,6 +149,13 @@ ## compute +### 0x61 `lookup-array` (lookup-array, argc 3) +- **summary:** Take a typed reference to base[index], preserving whether the base belongs to local or global storage. +- **grounding:** source=investigation, confidence=high +- **evidence:** HISTORY.BIN copies x/y tables into local-int cells 0x4 and 0x68, then lookup-array local-ptr <- local-int base supplies every right-side button and hovered-row draw coordinate. Treating the local operand's current value as a global base collapses those draws to (0,0); retaining the local address yields the native x=768/y=121..549 positions. RECOVER.BIN independently exercises the same pointer destination with global bases. + +Operand 2 names the base cell itself: a global-bank operand produces a global reference and a local-bank operand produces a local reference. Operand 3 is added as the element offset. Pointer destinations retain that address domain; reading or writing the pointer dereferences the corresponding bank. Non-pointer destinations receive the addressed value. The same domain-preserving address model applies to lookup-array-2d (0x12c). + ### 0x64 `copy-inline-int-array` (copy-inline-int-array, argc 2) - **summary:** (destination)(inline_blob_offset) - decode the count-prefixed integer literal blob at codebase + offset*4 and copy its values to consecutive VM integer cells beginning at destination. - **grounding:** source=investigation, confidence=high @@ -760,10 +767,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it " - **summary:** — - **grounding:** source=kelebek, confidence=low -### 0x61 `lookup-array` (lookup-array, argc 3) -- **summary:** — -- **grounding:** source=kelebek, confidence=med - ### 0x63 `u00414A60` (u00414A60, argc 2) - **summary:** — - **grounding:** source=kelebek, confidence=low diff --git a/docs/phase-a-slice-plan.md b/docs/phase-a-slice-plan.md index 4a81fd2..5230de2 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -1775,3 +1775,20 @@ effectful gaps total 13 instructions. **Next:** implement stored History voice replay (`0x1bd`) against the existing voice host path. Then take `0xd3/0xd4/0xd5` smooth-scroll interpolation as a separate scheduler/fidelity slice. + +### ADV History manual layout corrections (2026-07-19) + +Manual original/port comparison found that History's fixed rail was correct, but the lookup-driven buttons +were clustered at the top-left, the hovered-row artwork disagreed with its hit region, and rendered text +lost its x origin. The common VM cause was op `0x61`: `HISTORY.BIN` uses local-int operands as the bases of +its copied coordinate arrays, while the port treated the values in those cells as global addresses. Local +pointers now retain their local/global address domain, with matching dereference and write-through behavior +in both the C# VM and Python oracle. A real-script regression fixes the seven button coordinates at +x=768/y=121..549 and confirms every visible row retains layout origin x=65. + +Godot's dynamically created History/surface labels also no longer request full-rect anchors before they have +a parent. They use top-left absolute placement, matching the batch's origin+cursor coordinates and removing +the associated parent/layout diagnostic. + +**Next:** manually recheck History layout and hover against the original screenshot. If it matches, proceed +with stored History voice replay (`0x1bd`), followed by `0xd3/0xd4/0xd5` smooth-scroll fidelity. diff --git a/engine/Age.Engine.Tests/HistoryInteractionOpsTests.cs b/engine/Age.Engine.Tests/HistoryInteractionOpsTests.cs index 8976303..76a3db0 100644 --- a/engine/Age.Engine.Tests/HistoryInteractionOpsTests.cs +++ b/engine/Age.Engine.Tests/HistoryInteractionOpsTests.cs @@ -5,11 +5,12 @@ using Age.Engine.Vm; public class HistoryInteractionOpsTests { - private const int T_IMM = 0, T_GINT = 3, T_LINT = 9; + private const int T_IMM = 0, T_GINT = 3, T_LINT = 9, T_LPTR = 12; private static readonly OpcodeTable Table = OpcodeTableJson.Load(Paths.OpcodesJson); private static Operand I(long value) => new(T_IMM, value); private static Operand G(int address) => new(T_GINT, address); private static Operand L(int address) => new(T_LINT, address); + private static Operand P(int address) => new(T_LPTR, address); private sealed class StopAfterHistoryReturnsException : Exception { } @@ -20,6 +21,7 @@ public class HistoryInteractionOpsTests private int _modalSleeps; public bool HistoryReturned; public bool SawRenderedText; + public IReadOnlyList FirstHistoryFrame = Array.Empty(); public override long InputClockMilliseconds => _now; public override void Sleep(long duration) @@ -30,6 +32,7 @@ public class HistoryInteractionOpsTests _modalSleeps++; if (_modalSleeps == 1) { + FirstHistoryFrame = Vm.Gfx.SnapshotVisibleObjects(_now); Vm.UpdatePointer(790, 570); // HISTORY candidate 8: visible bottom-right close region Vm.UpdateMouseButtonState(0x1, true); Vm.QueueInputCallback(4); @@ -54,6 +57,28 @@ public class HistoryInteractionOpsTests } } + [Fact] + public void LookupArrayPreservesLocalStorageForLocalBases() + { + var script = ScriptAssembler.Assemble(Table, "LOCAL_LOOKUP", + new List<(int, Operand[])> + { + (0x55, new[] { L(10), I(768) }), + (0x55, new[] { L(11), I(121) }), + (0x61, new[] { P(0), L(10), I(1) }), + (0x55, new[] { G(0x100), P(0) }), + (0x55, new[] { P(0), I(179) }), + (0x55, new[] { G(0x101), L(11) }), + (0x2, Array.Empty()), + }, Array.Empty()); + var vm = new VirtualMachine(script, Table, new RecordingHost()); + + vm.Run(); + + Assert.Equal(121, vm.Globals[0x100]); + Assert.Equal(179, vm.Globals[0x101]); + } + [Fact] public void FindHitRectangleScansAfterTheIncomingIndexWithInclusiveEdges() { @@ -105,5 +130,24 @@ public class HistoryInteractionOpsTests Assert.True(host.SawRenderedText); Assert.True(host.HistoryReturned); Assert.Equal(1, host.Waits); // the enclosing ADV page was never released or re-entered + + var historyButtons = host.FirstHistoryFrame + .Where(render => render.Handle >= 0xd2fa && render.Handle <= 0xd300) + .OrderBy(render => render.Handle) + .ToArray(); + Assert.Equal(new[] + { + (0xd2faL, 768, 121), (0xd2fbL, 768, 179), (0xd2fcL, 768, 237), + (0xd2fdL, 768, 295), (0xd2feL, 768, 353), (0xd2ffL, 768, 411), + (0xd300L, 768, 549), + }, historyButtons.Select(render => (render.Handle, render.DstX, render.DstY)).ToArray()); + + var visibleRows = host.HistoryRenders + .Where(render => render.Text.Length > 0) + .GroupBy(render => render.LayoutSlot) + .Select(group => group.Last()) + .ToArray(); + Assert.NotEmpty(visibleRows); + Assert.All(visibleRows, render => Assert.Equal(65, render.Layout.OriginX)); } } diff --git a/engine/Age.Engine/Vm/Frame.cs b/engine/Age.Engine/Vm/Frame.cs index 1fa06a9..136298b 100644 --- a/engine/Age.Engine/Vm/Frame.cs +++ b/engine/Age.Engine/Vm/Frame.cs @@ -1,9 +1,27 @@ namespace Age.Engine.Vm; + +public enum VmAddressSpace +{ + Global, + LocalInteger, + LocalFloat, + LocalString, +} + +public readonly record struct VmAddress(VmAddressSpace Space, int Address) +{ + public static VmAddress Global(int address) => new(VmAddressSpace.Global, address); + public static VmAddress LocalInteger(int address) => new(VmAddressSpace.LocalInteger, address); + public static VmAddress LocalFloat(int address) => new(VmAddressSpace.LocalFloat, address); + public static VmAddress LocalString(int address) => new(VmAddressSpace.LocalString, address); + public VmAddress Offset(long offset) => new(Space, checked(Address + (int)offset)); +} + public sealed class Frame { public Dictionary I = new(); // local-int public Dictionary F = new(); // local-float (raw) public Dictionary S = new(); // local-string - public Dictionary P = new(); // local-ptr (holds a global address) - public Dictionary SP = new(); // local-string-ptr (holds a global-string address) + public Dictionary P = new(); // local-ptr (retains local/global address domain) + public Dictionary SP = new(); // local-string-ptr (same, for the string banks) } diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs index b9caf8a..f5b60f8 100644 --- a/engine/Age.Engine/Vm/VirtualMachine.cs +++ b/engine/Age.Engine/Vm/VirtualMachine.cs @@ -167,7 +167,7 @@ public sealed class VirtualMachine T_GPTR => Gi(Globals, (int)Gi(Globals, (int)op.Value)), T_LINT => Gi(_cur.Locals.I, (int)op.Value), T_LFLOAT => Gi(_cur.Locals.F, (int)op.Value), - T_LPTR => Gi(Globals, (int)Gi(_cur.Locals.P, (int)op.Value)), + T_LPTR => ReadIntCell(Ga(_cur.Locals.P, (int)op.Value)), _ => op.Value, }; @@ -179,7 +179,7 @@ public sealed class VirtualMachine case T_GPTR: Globals[(int)Gi(Globals, (int)op.Value)] = val; break; case T_LINT: _cur.Locals.I[(int)op.Value] = val; break; case T_LFLOAT: _cur.Locals.F[(int)op.Value] = val; break; - case T_LPTR: Globals[(int)Gi(_cur.Locals.P, (int)op.Value)] = val; break; + case T_LPTR: WriteIntCell(Ga(_cur.Locals.P, (int)op.Value), val); break; } } @@ -189,7 +189,7 @@ public sealed class VirtualMachine T_GSTR => Gs(GlobalStrings, (int)op.Value), T_GSTRPTR => Gs(GlobalStrings, (int)Gi(Globals, (int)op.Value)), T_LSTR => Gs(_cur.Locals.S, (int)op.Value), - T_LSTRPTR => Gs(GlobalStrings, (int)Gi(_cur.Locals.SP, (int)op.Value)), + T_LSTRPTR => ReadStringCell(Ga(_cur.Locals.SP, (int)op.Value)), _ => "", }; @@ -200,27 +200,63 @@ public sealed class VirtualMachine case T_GSTR: GlobalStrings[(int)op.Value] = val; break; case T_GSTRPTR: GlobalStrings[(int)Gi(Globals, (int)op.Value)] = val; break; case T_LSTR: _cur.Locals.S[(int)op.Value] = val; break; - case T_LSTRPTR: GlobalStrings[(int)Gi(_cur.Locals.SP, (int)op.Value)] = val; break; + case T_LSTRPTR: WriteStringCell(Ga(_cur.Locals.SP, (int)op.Value), val); break; } } - private long BaseAddr(Operand op) => op.Type switch + private static VmAddress Ga(Dictionary d, int k) + => d.TryGetValue(k, out var value) ? value : VmAddress.Global(0); + + private long ReadIntCell(VmAddress address) => address.Space switch { - T_IMM or T_GINT or T_GFLOAT or T_GSTR or T_GPTR or T_GSTRPTR => op.Value, - T_LINT => Gi(_cur.Locals.I, (int)op.Value), - T_LPTR => Gi(_cur.Locals.P, (int)op.Value), - _ => op.Value, + VmAddressSpace.LocalInteger => Gi(_cur.Locals.I, address.Address), + VmAddressSpace.LocalFloat => Gi(_cur.Locals.F, address.Address), + _ => Gi(Globals, address.Address), }; - private void LookupStore(Operand dst, long addr) + private void WriteIntCell(VmAddress address, long value) + { + switch (address.Space) + { + case VmAddressSpace.LocalInteger: _cur.Locals.I[address.Address] = value; break; + case VmAddressSpace.LocalFloat: _cur.Locals.F[address.Address] = value; break; + default: Globals[address.Address] = value; break; + } + } + + private string ReadStringCell(VmAddress address) + => address.Space == VmAddressSpace.LocalString + ? Gs(_cur.Locals.S, address.Address) + : Gs(GlobalStrings, address.Address); + + private void WriteStringCell(VmAddress address, string value) + { + if (address.Space == VmAddressSpace.LocalString) _cur.Locals.S[address.Address] = value; + else GlobalStrings[address.Address] = value; + } + + private VmAddress BaseAddr(Operand op) => op.Type switch + { + T_LINT => VmAddress.LocalInteger((int)op.Value), + T_LFLOAT => VmAddress.LocalFloat((int)op.Value), + T_LSTR => VmAddress.LocalString((int)op.Value), + T_LPTR => Ga(_cur.Locals.P, (int)op.Value), + T_LSTRPTR => Ga(_cur.Locals.SP, (int)op.Value), + _ => VmAddress.Global((int)op.Value), + }; + + private void LookupStore(Operand dst, VmAddress addr) { switch (dst.Type) { case T_LPTR: _cur.Locals.P[(int)dst.Value] = addr; break; case T_LSTRPTR: _cur.Locals.SP[(int)dst.Value] = addr; break; - case T_GPTR: Globals[(int)dst.Value] = addr; break; - case T_GSTRPTR: Globals[(int)dst.Value] = addr; break; - default: Write(dst, Gi(Globals, (int)addr)); break; + case T_GPTR: Globals[(int)dst.Value] = addr.Address; break; + case T_GSTRPTR: Globals[(int)dst.Value] = addr.Address; break; + default: + if (IsStr(dst)) WriteStr(dst, ReadStringCell(addr)); + else Write(dst, ReadIntCell(addr)); + break; } } @@ -233,7 +269,7 @@ public sealed class VirtualMachine case T_LINT: _cur.Locals.I[address] = value; break; case T_LFLOAT: _cur.Locals.F[address] = value; break; case T_GPTR: Globals[checked((int)Gi(Globals, (int)destination.Value) + index)] = value; break; - case T_LPTR: Globals[checked((int)Gi(_cur.Locals.P, (int)destination.Value) + index)] = value; break; + case T_LPTR: WriteIntCell(Ga(_cur.Locals.P, (int)destination.Value).Offset(index), value); break; } } @@ -244,21 +280,25 @@ public sealed class VirtualMachine T_LINT => Gi(_cur.Locals.I, checked((int)operand.Value + offset)), T_LFLOAT => Gi(_cur.Locals.F, checked((int)operand.Value + offset)), T_GINT or T_GFLOAT => ReadGlobal(checked((int)operand.Value + offset)), - T_LPTR => Gi(Globals, checked((int)Gi(_cur.Locals.P, (int)operand.Value) + offset)), + T_LPTR => ReadIntCell(Ga(_cur.Locals.P, (int)operand.Value).Offset(offset)), T_GPTR => Gi(Globals, checked((int)Gi(Globals, (int)operand.Value) + offset)), _ => Gi(Globals, checked((int)operand.Value + offset)), }; } - private (bool IsLocal, int Address) AddressedCellIdentity(Operand operand, int offset) + private (VmAddressSpace Space, int Address) AddressedCellIdentity(Operand operand, int offset) => operand.Type switch { - T_LINT or T_LFLOAT => (true, checked((int)operand.Value + offset)), - T_LPTR => (false, checked((int)Gi(_cur.Locals.P, (int)operand.Value) + offset)), - T_GPTR => (false, checked((int)Gi(Globals, (int)operand.Value) + offset)), - _ => (false, checked((int)operand.Value + offset)), + T_LINT => (VmAddressSpace.LocalInteger, checked((int)operand.Value + offset)), + T_LFLOAT => (VmAddressSpace.LocalFloat, checked((int)operand.Value + offset)), + T_LPTR => PointerIdentity(Ga(_cur.Locals.P, (int)operand.Value).Offset(offset)), + T_GPTR => (VmAddressSpace.Global, checked((int)Gi(Globals, (int)operand.Value) + offset)), + _ => (VmAddressSpace.Global, checked((int)operand.Value + offset)), }; + private static (VmAddressSpace Space, int Address) PointerIdentity(VmAddress address) + => (address.Space, address.Address); + private string FormatSwitchValue(Operand operand) => IsStr(operand) ? ReadStr(operand) @@ -383,9 +423,9 @@ public sealed class VirtualMachine else Write(a[0], Read(a[1])); return pc + 1; case "lookup-array": - LookupStore(a[0], BaseAddr(a[1]) + Read(a[2])); return pc + 1; + LookupStore(a[0], BaseAddr(a[1]).Offset(Read(a[2]))); return pc + 1; case "lookup-array-2d": - LookupStore(a[0], BaseAddr(a[1]) + Read(a[2]) * Read(a[3]) + Read(a[4])); return pc + 1; + LookupStore(a[0], BaseAddr(a[1]).Offset(Read(a[2]) * Read(a[3]) + Read(a[4]))); return pc + 1; case "copy-inline-int-array": // 0x64: count dword followed by plain file values { int offset = checked((int)Read(a[1])); diff --git a/godot/Main.cs b/godot/Main.cs index d4c8eb4..66515e9 100644 --- a/godot/Main.cs +++ b/godot/Main.cs @@ -586,7 +586,6 @@ public partial class Main : Godot.Control AutowrapMode = TextServer.AutowrapMode.WordSmart, ClipText = true, }; - label.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect); label.AddThemeFontOverride("font", _text.GetThemeFont("font")); AddChild(label); return label; diff --git a/tools/vm0.py b/tools/vm0.py index 781582a..7a0c2a2 100644 --- a/tools/vm0.py +++ b/tools/vm0.py @@ -13,9 +13,9 @@ Purpose (see docs/phase-a-slice-plan.md): Memory model: * one flat GLOBAL bank G (dict addr->int); globals are raw offsets into one space. * per-call local frame with sparse typed banks (int/float/string/ptr). - * a `-ptr` variable holds an ADDRESS into G. lookup-array/2d with a ptr dst stores that - address (take-reference); reading a ptr derefs (G[addr]); writing through a ptr writes G[addr]. - This is the model RECOVER forces; the unit test is its litmus. + * a `-ptr` variable retains an address plus its local/global storage domain. lookup-array/2d + with a ptr dst stores that reference (take-reference); reads/writes dereference the matching + bank. RECOVER exercises global references; HISTORY's copied coordinate arrays exercise local ones. * jcc(cond, tA, tB): cond truthy -> goto tA else tB; 0xFFFFFFFF = fall through (from RECOVER+SCJUMP). * call/ret (0x8F/0x05) are intra-script subroutine calls (shared frame); call-script (0x03) is the inter-script one and is stubbed. @@ -53,7 +53,7 @@ class Frame: self.i = collections.defaultdict(int) # local-int self.f = collections.defaultdict(int) # local-float (stored raw) self.s = collections.defaultdict(str) # local-string - self.p = collections.defaultdict(int) # local-ptr (holds a G address) + self.p = collections.defaultdict(lambda: ("g", 0)) # local-ptr: (bank, address) class VM: @@ -96,7 +96,7 @@ class VM: if t == T_LINT: return self.fr.i[v] if t == T_LFLOAT: return self.fr.f[v] if t == T_LSTR: return self.fr.s[v] - if t == T_LPTR: return self.G[self.fr.p[v]] # deref ptr + if t == T_LPTR: return self.read_cell(self.fr.p[v]) # deref ptr self.log[f"read?t{t:#x}"] += 1 return v @@ -108,23 +108,37 @@ class VM: elif t == T_LINT: self.fr.i[v] = val elif t == T_LFLOAT: self.fr.f[v] = val elif t == T_LSTR: self.fr.s[v] = val - elif t == T_LPTR: self.G[self.fr.p[v]] = val # write through + elif t == T_LPTR: self.write_cell(self.fr.p[v], val) else: self.log[f"write?t{t:#x}"] += 1 + def read_cell(self, address): + bank, addr = address + if bank == "i": return self.fr.i[addr] + if bank == "f": return self.fr.f[addr] + return self.G[addr] + + def write_cell(self, address, value): + bank, addr = address + if bank == "i": self.fr.i[addr] = value + elif bank == "f": self.fr.f[addr] = value + else: self.G[addr] = value + def base_addr(self, op): """The base ADDRESS an operand names, for array lookups.""" t, v = op - if t in (T_IMM, T_GINT, T_GFLOAT, T_GSTR, T_GPTR): return v # global's own offset - if t == T_LINT: return self.fr.i[v] + if t in (T_IMM, T_GINT, T_GFLOAT, T_GSTR, T_GPTR): return ("g", v) + if t == T_LINT: return ("i", v) + if t == T_LFLOAT: return ("f", v) + if t == T_LSTR: return ("s", v) if t == T_LPTR: return self.fr.p[v] - return v + return ("g", v) def lookup_store(self, dst, addr): """lookup result: ptr dst gets the reference (address); non-ptr gets the element value.""" t, v = dst if t == T_LPTR: self.fr.p[v] = addr - elif t == T_GPTR: self.G[v] = addr - else: self.write(dst, self.G[addr]) + elif t == T_GPTR: self.G[v] = addr[1] + else: self.write(dst, self.read_cell(addr)) # ---- execution ----------------------------------------------------------- def run(self, entry_off=0, max_steps=2_000_000): @@ -171,10 +185,12 @@ class VM: if lbl == "set-string": self.write(a[0], self.read(a[1])); return pc + 1 if lbl == "lookup-array": # dst = base[idx] - addr = self.base_addr(a[1]) + self.read(a[2]) + bank, base = self.base_addr(a[1]) + addr = (bank, base + self.read(a[2])) self.lookup_store(a[0], addr); return pc + 1 if lbl == "lookup-array-2d": # dst = base[i*stride + col] - addr = self.base_addr(a[1]) + self.read(a[2]) * self.read(a[3]) + self.read(a[4]) + bank, base = self.base_addr(a[1]) + addr = (bank, base + self.read(a[2]) * self.read(a[3]) + self.read(a[4])) self.lookup_store(a[0], addr); return pc + 1 if lbl == "bit-set": bit = self.read(a[1]) diff --git a/vm-map/opcodes.toml b/vm-map/opcodes.toml index f8d43fb..03ea818 100644 --- a/vm-map/opcodes.toml +++ b/vm-map/opcodes.toml @@ -796,13 +796,14 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "lookup-array" -category = "unknown" -summary = "" +category = "compute" +summary = "Take a typed reference to base[index], preserving whether the base belongs to local or global storage." noop_headless = false -source = "kelebek" -confidence = "med" +source = "investigation" +confidence = "high" depends_on = [] -evidence = "" +evidence = "HISTORY.BIN copies x/y tables into local-int cells 0x4 and 0x68, then lookup-array local-ptr <- local-int base supplies every right-side button and hovered-row draw coordinate. Treating the local operand's current value as a global base collapses those draws to (0,0); retaining the local address yields the native x=768/y=121..549 positions. RECOVER.BIN independently exercises the same pointer destination with global bases." +details = "Operand 2 names the base cell itself: a global-bank operand produces a global reference and a local-bank operand produces a local reference. Operand 3 is added as the element offset. Pointer destinations retain that address domain; reading or writing the pointer dereferences the corresponding bank. Non-pointer destinations receive the addressed value. The same domain-preserving address model applies to lookup-array-2d (0x12c)." [[opcode.semantics.args]] i = 1