Correct ADV History layout addressing

This commit is contained in:
gamer147
2026-07-19 10:43:52 -04:00
parent 1fdedfb41f
commit 280705d69c
9 changed files with 200 additions and 48 deletions

View File

@@ -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, 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. 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 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. callback scheduler. None changes backlog ownership or requires choosing a save/profile backend.

View File

@@ -149,6 +149,13 @@
## compute ## 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) ### 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. - **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 - **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:** — - **summary:** —
- **grounding:** source=kelebek, confidence=low - **grounding:** source=kelebek, confidence=low
### 0x61 `lookup-array` (lookup-array, argc 3)
- **summary:** —
- **grounding:** source=kelebek, confidence=med
### 0x63 `u00414A60` (u00414A60, argc 2) ### 0x63 `u00414A60` (u00414A60, argc 2)
- **summary:** — - **summary:** —
- **grounding:** source=kelebek, confidence=low - **grounding:** source=kelebek, confidence=low

View File

@@ -1775,3 +1775,20 @@ effectful gaps total 13 instructions.
**Next:** implement stored History voice replay (`0x1bd`) against the existing voice host path. Then take **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. `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.

View File

@@ -5,11 +5,12 @@ using Age.Engine.Vm;
public class HistoryInteractionOpsTests 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 readonly OpcodeTable Table = OpcodeTableJson.Load(Paths.OpcodesJson);
private static Operand I(long value) => new(T_IMM, value); private static Operand I(long value) => new(T_IMM, value);
private static Operand G(int address) => new(T_GINT, address); private static Operand G(int address) => new(T_GINT, address);
private static Operand L(int address) => new(T_LINT, 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 { } private sealed class StopAfterHistoryReturnsException : Exception { }
@@ -20,6 +21,7 @@ public class HistoryInteractionOpsTests
private int _modalSleeps; private int _modalSleeps;
public bool HistoryReturned; public bool HistoryReturned;
public bool SawRenderedText; public bool SawRenderedText;
public IReadOnlyList<RenderObject> FirstHistoryFrame = Array.Empty<RenderObject>();
public override long InputClockMilliseconds => _now; public override long InputClockMilliseconds => _now;
public override void Sleep(long duration) public override void Sleep(long duration)
@@ -30,6 +32,7 @@ public class HistoryInteractionOpsTests
_modalSleeps++; _modalSleeps++;
if (_modalSleeps == 1) if (_modalSleeps == 1)
{ {
FirstHistoryFrame = Vm.Gfx.SnapshotVisibleObjects(_now);
Vm.UpdatePointer(790, 570); // HISTORY candidate 8: visible bottom-right close region Vm.UpdatePointer(790, 570); // HISTORY candidate 8: visible bottom-right close region
Vm.UpdateMouseButtonState(0x1, true); Vm.UpdateMouseButtonState(0x1, true);
Vm.QueueInputCallback(4); 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<Operand>()),
}, Array.Empty<string>());
var vm = new VirtualMachine(script, Table, new RecordingHost());
vm.Run();
Assert.Equal(121, vm.Globals[0x100]);
Assert.Equal(179, vm.Globals[0x101]);
}
[Fact] [Fact]
public void FindHitRectangleScansAfterTheIncomingIndexWithInclusiveEdges() public void FindHitRectangleScansAfterTheIncomingIndexWithInclusiveEdges()
{ {
@@ -105,5 +130,24 @@ public class HistoryInteractionOpsTests
Assert.True(host.SawRenderedText); Assert.True(host.SawRenderedText);
Assert.True(host.HistoryReturned); Assert.True(host.HistoryReturned);
Assert.Equal(1, host.Waits); // the enclosing ADV page was never released or re-entered 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));
} }
} }

View File

@@ -1,9 +1,27 @@
namespace Age.Engine.Vm; 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 sealed class Frame
{ {
public Dictionary<int, long> I = new(); // local-int public Dictionary<int, long> I = new(); // local-int
public Dictionary<int, long> F = new(); // local-float (raw) public Dictionary<int, long> F = new(); // local-float (raw)
public Dictionary<int, string> S = new(); // local-string public Dictionary<int, string> S = new(); // local-string
public Dictionary<int, long> P = new(); // local-ptr (holds a global address) public Dictionary<int, VmAddress> P = new(); // local-ptr (retains local/global address domain)
public Dictionary<int, long> SP = new(); // local-string-ptr (holds a global-string address) public Dictionary<int, VmAddress> SP = new(); // local-string-ptr (same, for the string banks)
} }

View File

@@ -167,7 +167,7 @@ public sealed class VirtualMachine
T_GPTR => Gi(Globals, (int)Gi(Globals, (int)op.Value)), T_GPTR => Gi(Globals, (int)Gi(Globals, (int)op.Value)),
T_LINT => Gi(_cur.Locals.I, (int)op.Value), T_LINT => Gi(_cur.Locals.I, (int)op.Value),
T_LFLOAT => Gi(_cur.Locals.F, (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, _ => op.Value,
}; };
@@ -179,7 +179,7 @@ public sealed class VirtualMachine
case T_GPTR: Globals[(int)Gi(Globals, (int)op.Value)] = val; break; 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_LINT: _cur.Locals.I[(int)op.Value] = val; break;
case T_LFLOAT: _cur.Locals.F[(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_GSTR => Gs(GlobalStrings, (int)op.Value),
T_GSTRPTR => Gs(GlobalStrings, (int)Gi(Globals, (int)op.Value)), T_GSTRPTR => Gs(GlobalStrings, (int)Gi(Globals, (int)op.Value)),
T_LSTR => Gs(_cur.Locals.S, (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_GSTR: GlobalStrings[(int)op.Value] = val; break;
case T_GSTRPTR: GlobalStrings[(int)Gi(Globals, (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_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<int, VmAddress> 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, VmAddressSpace.LocalInteger => Gi(_cur.Locals.I, address.Address),
T_LINT => Gi(_cur.Locals.I, (int)op.Value), VmAddressSpace.LocalFloat => Gi(_cur.Locals.F, address.Address),
T_LPTR => Gi(_cur.Locals.P, (int)op.Value), _ => Gi(Globals, address.Address),
_ => op.Value,
}; };
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) switch (dst.Type)
{ {
case T_LPTR: _cur.Locals.P[(int)dst.Value] = addr; break; case T_LPTR: _cur.Locals.P[(int)dst.Value] = addr; break;
case T_LSTRPTR: _cur.Locals.SP[(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_GPTR: Globals[(int)dst.Value] = addr.Address; break;
case T_GSTRPTR: Globals[(int)dst.Value] = addr; break; case T_GSTRPTR: Globals[(int)dst.Value] = addr.Address; break;
default: Write(dst, Gi(Globals, (int)addr)); 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_LINT: _cur.Locals.I[address] = value; break;
case T_LFLOAT: _cur.Locals.F[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_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_LINT => Gi(_cur.Locals.I, checked((int)operand.Value + offset)),
T_LFLOAT => Gi(_cur.Locals.F, 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_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)), T_GPTR => Gi(Globals, checked((int)Gi(Globals, (int)operand.Value) + offset)),
_ => Gi(Globals, checked((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 => operand.Type switch
{ {
T_LINT or T_LFLOAT => (true, checked((int)operand.Value + offset)), T_LINT => (VmAddressSpace.LocalInteger, checked((int)operand.Value + offset)),
T_LPTR => (false, checked((int)Gi(_cur.Locals.P, (int)operand.Value) + offset)), T_LFLOAT => (VmAddressSpace.LocalFloat, checked((int)operand.Value + offset)),
T_GPTR => (false, checked((int)Gi(Globals, (int)operand.Value) + offset)), T_LPTR => PointerIdentity(Ga(_cur.Locals.P, (int)operand.Value).Offset(offset)),
_ => (false, checked((int)operand.Value + 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) private string FormatSwitchValue(Operand operand)
=> IsStr(operand) => IsStr(operand)
? ReadStr(operand) ? ReadStr(operand)
@@ -383,9 +423,9 @@ public sealed class VirtualMachine
else Write(a[0], Read(a[1])); else Write(a[0], Read(a[1]));
return pc + 1; return pc + 1;
case "lookup-array": 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": 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 case "copy-inline-int-array": // 0x64: count dword followed by plain file values
{ {
int offset = checked((int)Read(a[1])); int offset = checked((int)Read(a[1]));

View File

@@ -586,7 +586,6 @@ public partial class Main : Godot.Control
AutowrapMode = TextServer.AutowrapMode.WordSmart, AutowrapMode = TextServer.AutowrapMode.WordSmart,
ClipText = true, ClipText = true,
}; };
label.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
label.AddThemeFontOverride("font", _text.GetThemeFont("font")); label.AddThemeFontOverride("font", _text.GetThemeFont("font"));
AddChild(label); AddChild(label);
return label; return label;

View File

@@ -13,9 +13,9 @@ Purpose (see docs/phase-a-slice-plan.md):
Memory model: Memory model:
* one flat GLOBAL bank G (dict addr->int); globals are raw offsets into one space. * 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). * 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 * a `-ptr` variable retains an address plus its local/global storage domain. lookup-array/2d
address (take-reference); reading a ptr derefs (G[addr]); writing through a ptr writes G[addr]. with a ptr dst stores that reference (take-reference); reads/writes dereference the matching
This is the model RECOVER forces; the unit test is its litmus. 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). * 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 * call/ret (0x8F/0x05) are intra-script subroutine calls (shared frame); call-script (0x03) is
the inter-script one and is stubbed. the inter-script one and is stubbed.
@@ -53,7 +53,7 @@ class Frame:
self.i = collections.defaultdict(int) # local-int self.i = collections.defaultdict(int) # local-int
self.f = collections.defaultdict(int) # local-float (stored raw) self.f = collections.defaultdict(int) # local-float (stored raw)
self.s = collections.defaultdict(str) # local-string 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: class VM:
@@ -96,7 +96,7 @@ class VM:
if t == T_LINT: return self.fr.i[v] if t == T_LINT: return self.fr.i[v]
if t == T_LFLOAT: return self.fr.f[v] if t == T_LFLOAT: return self.fr.f[v]
if t == T_LSTR: return self.fr.s[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 self.log[f"read?t{t:#x}"] += 1
return v return v
@@ -108,23 +108,37 @@ class VM:
elif t == T_LINT: self.fr.i[v] = val elif t == T_LINT: self.fr.i[v] = val
elif t == T_LFLOAT: self.fr.f[v] = val elif t == T_LFLOAT: self.fr.f[v] = val
elif t == T_LSTR: self.fr.s[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 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): def base_addr(self, op):
"""The base ADDRESS an operand names, for array lookups.""" """The base ADDRESS an operand names, for array lookups."""
t, v = op t, v = op
if t in (T_IMM, T_GINT, T_GFLOAT, T_GSTR, T_GPTR): return v # global's own offset if t in (T_IMM, T_GINT, T_GFLOAT, T_GSTR, T_GPTR): return ("g", v)
if t == T_LINT: return self.fr.i[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] if t == T_LPTR: return self.fr.p[v]
return v return ("g", v)
def lookup_store(self, dst, addr): def lookup_store(self, dst, addr):
"""lookup result: ptr dst gets the reference (address); non-ptr gets the element value.""" """lookup result: ptr dst gets the reference (address); non-ptr gets the element value."""
t, v = dst t, v = dst
if t == T_LPTR: self.fr.p[v] = addr if t == T_LPTR: self.fr.p[v] = addr
elif t == T_GPTR: self.G[v] = addr elif t == T_GPTR: self.G[v] = addr[1]
else: self.write(dst, self.G[addr]) else: self.write(dst, self.read_cell(addr))
# ---- execution ----------------------------------------------------------- # ---- execution -----------------------------------------------------------
def run(self, entry_off=0, max_steps=2_000_000): def run(self, entry_off=0, max_steps=2_000_000):
@@ -171,10 +185,12 @@ class VM:
if lbl == "set-string": if lbl == "set-string":
self.write(a[0], self.read(a[1])); return pc + 1 self.write(a[0], self.read(a[1])); return pc + 1
if lbl == "lookup-array": # dst = base[idx] 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 self.lookup_store(a[0], addr); return pc + 1
if lbl == "lookup-array-2d": # dst = base[i*stride + col] 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 self.lookup_store(a[0], addr); return pc + 1
if lbl == "bit-set": if lbl == "bit-set":
bit = self.read(a[1]) bit = self.read(a[1])

View File

@@ -796,13 +796,14 @@ abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "lookup-array" name = "lookup-array"
category = "unknown" category = "compute"
summary = "" summary = "Take a typed reference to base[index], preserving whether the base belongs to local or global storage."
noop_headless = false noop_headless = false
source = "kelebek" source = "investigation"
confidence = "med" confidence = "high"
depends_on = [] 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]] [[opcode.semantics.args]]
i = 1 i = 1