Implement History timed callback scrolling

This commit is contained in:
gamer147
2026-07-19 21:15:15 -04:00
parent f928572152
commit 81155c5e1c
10 changed files with 291 additions and 37 deletions

View File

@@ -36,6 +36,16 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch-
| `0x55248` | `ret_stack_a` | `void*` | per-frame return stack (op 0x8f call pushes) | | `0x55248` | `ret_stack_a` | `void*` | per-frame return stack (op 0x8f call pushes) |
| `0x552e8` | `ret_stack_b` | `void*` | per-frame return stack (companion) | | `0x552e8` | `ret_stack_b` | `void*` | per-frame return stack (companion) |
| `0x5f304` | `sleep_timer` | `int` | sleep timer object (op 0xc8; +8 active, +0x14 start-ms, +0x18 duration) | | `0x5f304` | `sleep_timer` | `int` | sleep timer object (op 0xc8; +8 active, +0x14 start-ms, +0x18 duration) |
| `0x5f3ac` | `timed_callback_timer` | `int` | elapsed timer object armed by op 0xd5 and sampled by the timed callback scheduler |
| `0x5f690` | `timed_callback_abort_pc` | `int` | optional local fallback PC supplied to op 0xd5; -1 disables abort redirection |
| `0x5f694` | `timed_callback_entries_vector` | `int` | MSVC vector object base for 16-byte schedule entries; begin/end/capacity pointers follow |
| `0x5f698` | `timed_callback_entries_begin` | `void*` | begin pointer for 16-byte entries {deadline_ms, field_100, primary_pc, catchup_pc} |
| `0x5f69c` | `timed_callback_entries_end` | `void*` | end pointer for the timed callback entry vector |
| `0x5f6a0` | `timed_callback_entries_capacity` | `void*` | capacity-end pointer for the timed callback entry vector |
| `0x5f6a4` | `timed_callback_last_index` | `int` | last appended schedule index; op 0xd3 resets to -1 and op 0xd4 increments per entry |
| `0x5f6a8` | `timed_callback_cursor` | `int` | next schedule entry consumed by timed_callback_sequence_tick; op 0xd3 resets to zero |
| `0x5f6b4` | `timed_callback_script_resource_id` | `uint` | active frame script resource retained by op 0xd5 for local callback dispatch safety |
| `0x5f6b8` | `timed_callback_script_resource_guard` | `uint` | companion current-script identity checked before redirecting to a scheduled callback |
| `0x6da78` | `message_skip_activation_guard` | `int` | set when op 0x88 enables Skip; suppresses the enabling click from immediately becoming a hotspot activation | | `0x6da78` | `message_skip_activation_guard` | `int` | set when op 0x88 enables Skip; suppresses the enabling click from immediately becoming a hotspot activation |
| `0x6da80` | `message_skip_cancel_click_phase` | `int` | press/release phase used only when set:CancelMesSkipOnClick is nonzero | | `0x6da80` | `message_skip_cancel_click_phase` | `int` | press/release phase used only when set:CancelMesSkipOnClick is nonzero |
| `0x6da88` | `coroutine_yield_a` | `void*` | op 0x7b yield-state save (op1 -> +ctxidx*4) | | `0x6da88` | `coroutine_yield_a` | `void*` | op 0x7b yield-state save (op1 -> +ctxidx*4) |
@@ -51,3 +61,4 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch-
| `0xa0cc4` | `screen_h` | `int` | screen height (480) | | `0xa0cc4` | `screen_h` | `int` | screen height (480) |
| `0xa0cc8` | `screen_bpp` | `int` | screen bpp (8) | | `0xa0cc8` | `screen_bpp` | `int` | screen bpp (8) |
| `0xa0ce4` | `run_state_flags` | `uint` | interpreter run-state flags (bit1 sleeping; 0x8000000 skip/fast-forward) | | `0xa0ce4` | `run_state_flags` | `uint` | interpreter run-state flags (bit1 sleeping; 0x8000000 skip/fast-forward) |
| `0xa0ce8` | `timed_callback_abort_signal` | `int` | scheduler-polled abort/input signal cleared when op 0xd5 starts; redirects to abort PC when configured |

View File

@@ -1416,8 +1416,36 @@ A live replay trace exposed a corrected prerequisite on that path. The clicked S
and type at `+0x18`. Thus SC0000's `0x1d2(2,0x11)` means type 2, character/value `0x11`, not the reverse. and type at `+0x18`. Thus SC0000's `0x1d2(2,0x11)` means type 2, character/value `0x11`, not the reverse.
The port now writes that pair in the native order, allowing HISTORY's type-2 gate to reach voice playback. The port now writes that pair in the native order, allowing HISTORY's type-2 gate to reach voice playback.
History's remaining work is the `0xd3/0xd4/0xd5` smooth-scroll callback scheduler and its small supporting #### Timed local-callback scheduler and History smooth scrolling (2026-07-19)
gaps. None changes backlog ownership or requires choosing a save/profile backend.
Ops `0xd3/0xd4/0xd5` are a generic frame-local timed callback sequence, not a History-specific animation
primitive. The native state is a vector of 16-byte entries at `ctx+0x5f694`, with begin/end/capacity at
`+0x5f698/+0x5f69c/+0x5f6a0`; each entry is `{deadline_ms,aux,primary_pc,catchup_pc}`. The playback cursor is
`ctx+0x5f6a8`, the last appended index is `ctx+0x5f6a4`, and op `0xd5` uses the elapsed timer rooted at
`ctx+0x5f3ac`. These names are canonical in `vm-map/engine-ctx.toml` and applied to the `/v2` `EngineCtx`.
- op `0xd3` clears the vector, resets the cursor to zero, and initializes the last index and optional abort
PC to `-1`;
- op `0xd4(interval,count,primary,catchup)` appends `count` entries, accumulating each deadline relative to
the preceding one and storing the two local callback PCs;
- op `0xd5(abort_pc)` retains the active script identity, starts the timer, sorts the entries, and enables
scheduler run-state bit `0x40` while `cursor < last_index`. The final entry is therefore a non-dispatched
look-ahead sentinel, not another callback.
`timed_callback_sequence_tick@0x408170` sleeps until an entry is due. If the following entry's deadline is
already behind elapsed time, it dispatches the current entry's catch-up PC; otherwise it dispatches the
primary PC. It validates that the active frame still owns the recorded script, redirects that frame's PC,
and advances the cursor. The optional abort signal at `ctx+0xa0ce8` redirects to the op-`0xd5` fallback PC;
HISTORY passes `0xffffffff`, so that branch is outside its live route.
HISTORY builds deadlines 10, 20, 30, 40, 50, 51, and 52 ms. The first five advance a five-step scrollbar
interpolation; the catch-up form omits the expensive redraw/present call when the engine is late. The 51 ms
entry is a no-op callback and the 52 ms entry is its look-ahead sentinel. The port keeps the sequence on the
script frame, blocks only at these scheduler service boundaries using the host clock, and resumes op `0xd5`
after each local callback returns. Focused tests cover exact relative deadlines and the late catch-up choice.
Smooth-scroll scheduling is now implemented without changing History backlog ownership or choosing a
save/profile backend. HISTORY's remaining static gaps are five supporting opcodes across seven instructions.
The original dependency order was **Hide Window first** to establish reusable callback/coroutine input, then The original dependency order was **Hide Window first** to establish reusable callback/coroutine input, then
Read-message Skip, then History after both the input layer and message-completion seam exist. Hide Window is Read-message Skip, then History after both the input layer and message-completion seam exist. Hide Window is

View File

@@ -233,6 +233,21 @@ This also names the whole call graph statically (build/callscript-names.json).
Native handler sleep_op_0xc8 @0x420ec0 is NON-BLOCKING: it arms a timer (sleep_timer_arm @0x44cff0 at ctx+0x5f304 = active flag + start tick + duration) that the engine main loop polls, resuming the script when elapsed. Operand UNIT = MILLISECONDS (start = ms tick source DAT_0056f3d4, timeGetTime/GetTickCount class). duration<10 fast-paths via [0x56f0b8]; all real scene sleeps (100/750/1000) are >=10. The handler also writes gfx cmd-type 3 + runs anti-tamper checks, neither needed host-side. Port equivalent: the Godot host parks the VM thread for duration ms while the presentation compositor continues. Sleep is one proven presentation-capable service boundary; ordinary AE setup runs burst-fast to 0x21c and is not paced per opcode. Headless hosts no-op it (parity). Native handler sleep_op_0xc8 @0x420ec0 is NON-BLOCKING: it arms a timer (sleep_timer_arm @0x44cff0 at ctx+0x5f304 = active flag + start tick + duration) that the engine main loop polls, resuming the script when elapsed. Operand UNIT = MILLISECONDS (start = ms tick source DAT_0056f3d4, timeGetTime/GetTickCount class). duration<10 fast-paths via [0x56f0b8]; all real scene sleeps (100/750/1000) are >=10. The handler also writes gfx cmd-type 3 + runs anti-tamper checks, neither needed host-side. Port equivalent: the Godot host parks the VM thread for duration ms while the presentation compositor continues. Sleep is one proven presentation-capable service boundary; ordinary AE setup runs burst-fast to 0x21c and is not paced per opcode. Headless hosts no-op it (parity).
### 0xd3 `begin-timed-callback-sequence` (u00425960, argc 0)
- **summary:** Clear and initialize the current script frame's timed local-callback sequence.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0xd3_handler@0x41a430 clears the 16-byte-entry vector at ctx+0x5f694, sets last index ctx+0x5f6a4 and abort target ctx+0x5f690 to -1, and resets playback cursor ctx+0x5f6a8 to zero. HISTORY.BIN@0xbf9 begins its seven-event scrollbar interpolation schedule.
### 0xd4 `append-relative-timed-callbacks` (u004266F0, argc 4)
- **summary:** Append repeated relative deadlines and their on-time/catch-up local callback targets to the current frame's timed sequence.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0xd4_handler@0x428890 loops operand 2 times, accumulates operand 1 onto the preceding deadline, stores operand 3 at entry+8 and operand 4 at entry+0xc, and appends each 16-byte entry. HISTORY.BIN@0xbfa/0xc03/0xc0c produces deadlines 10,20,30,40,50,51,52 ms.
### 0xd5 `run-timed-callback-sequence` (u004262C0, argc 1)
- **summary:** Start or service the frame-local timed sequence, dispatching scheduled local callbacks through the penultimate entry and selecting the catch-up target when the following deadline is already late.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0xd5_handler@0x421090 retains the current script resource, starts the ctx+0x5f3ac timer, sorts the schedule, and keeps run-state bit 0x40 active only while cursor < last_index, making the final entry a non-dispatched look-ahead sentinel. timed_callback_sequence_tick@0x408170 waits to each deadline, compares the following entry deadline with elapsed time to select entry+8 versus entry+0xc, redirects the frame PC, and advances ctx+0x5f6a8. Operand 1 is an optional abort/fallback PC; HISTORY passes 0xffffffff.
### 0xd9 `clear-run-state-0x1000` (u00415880, argc 0) ### 0xd9 `clear-run-state-0x1000` (u00415880, argc 0)
- **summary:** Clear native run/service bit 0x1000; if the secondary context is active, clear the same bit there. SC0000 executes it once after the initial SFX-channel reset, with no VM-visible result. - **summary:** Clear native run/service bit 0x1000; if the secondary context is active, clear the same bit there. SC0000 executes it once after the initial SFX-channel reset, with no VM-visible result.
- **grounding:** source=investigation, confidence=high, noop_headless=True - **grounding:** source=investigation, confidence=high, noop_headless=True
@@ -847,18 +862,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
### 0xd3 `u00425960` (u00425960, argc 0)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0xd4 `u004266F0` (u004266F0, argc 4)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0xd5 `u004262C0` (u004262C0, argc 1)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0xfe `u0041E360` (u0041E360, argc 1) ### 0xfe `u0041E360` (u0041E360, argc 1)
- **summary:** — - **summary:** —
- **grounding:** source=kelebek, confidence=low - **grounding:** source=kelebek, confidence=low

View File

@@ -1849,3 +1849,28 @@ voice request emitted by the original page, clicks the actual visible voiced His
request for SC0000 `0x24`/`MAN999.OGG`; this prevents a pre-History voice from producing a false positive. request for SC0000 `0x24`/`MAN999.OGG`; this prevents a pre-History voice from producing a false positive.
The `/v2` handler and helper comments are corrected and saved. Manual replay of that same retained row now The `/v2` handler and helper comments are corrected and saved. Manual replay of that same retained row now
plays audibly, confirming the full click-to-Godot path. plays audibly, confirming the full click-to-Godot path.
### ADV History smooth-scroll scheduler implemented (2026-07-19)
Native RE resolves `0xd3/0xd4/0xd5` as a generic frame-local timed local-callback sequence. Op `0xd3`
initializes it, op `0xd4` appends repeated relative deadlines with separate primary and catch-up targets,
and op `0xd5` starts the elapsed timer and services callbacks through the penultimate entry. The final entry
is a look-ahead sentinel. The scheduler chooses the catch-up target when the next deadline is already late,
which lets HISTORY advance interpolation state without performing every intermediate redraw.
HISTORY's concrete schedule is 10/20/30/40/50/51/52 ms: five scrollbar interpolation steps, a no-op
terminal callback, and the look-ahead sentinel. The VM now stores this state on the active script frame,
uses the host's existing clock/sleep boundary, redirects into the shipped local callbacks, and resumes the
same `0xd5` after `ret`. Focused regressions cover on-time execution and forced catch-up behavior.
The canonical opcode and `EngineCtx` sources are updated and regenerated; the `/v2` handlers, vector append
helper, timer helper, and main-loop scheduler are named/commented, the expanded 56-field `EngineCtx` is
reapplied, and the program is saved. HISTORY is now 73/78 distinct opcodes and 847/854 instructions handled
or safe-noop. Its five remaining effectful gaps total seven instructions: `0x10d` and `0x8b` twice each,
plus one each of `0x1ce`, `0x1cb`, and `0x20a`.
**Next:** investigate the five remaining HISTORY support opcodes together, then select the smallest coherent
implementation slice from their actual responsibilities rather than treating them as one feature by count.
Validation: all 203 engine tests pass, opcode and EngineCtx tests/lints are clean, vm0 RECOVER passes, the
Godot build has zero warnings, and threaded `SELFTEST OK`.

View File

@@ -0,0 +1,66 @@
using Age.Engine.Hosting;
using Age.Engine.Model;
using Age.Engine.Sys4;
using Age.Engine.Vm;
public class TimedCallbackOpsTests
{
private const int T_IMM = 0, T_GINT = 3;
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 sealed class ClockHost(long overshootMs = 0) : RecordingHost
{
private long _now;
public override long InputClockMilliseconds => _now;
public override void Sleep(long duration)
{
base.Sleep(duration);
_now += duration + overshootMs;
}
}
private static Script BuildTwoEventSequence()
=> ScriptAssembler.Assemble(Table, "TIMED_CALLBACKS",
new List<(int, Operand[])>
{
(0xd3, Array.Empty<Operand>()), // 0
(0xd4, new[] { I(10), I(3), I(14), I(22) }), // 1: two events + look-ahead sentinel
(0xd5, new[] { I(-1) }), // 10
(0x2, Array.Empty<Operand>()), // 13
(0x50, new[] { G(0x100), G(0x100), I(1) }), // 14: on-time callback
(0x5, Array.Empty<Operand>()), // 21: ret
(0x50, new[] { G(0x101), G(0x101), I(1) }), // 22: catch-up callback
(0x5, Array.Empty<Operand>()), // 29: ret
}, Array.Empty<string>());
[Fact]
public void RelativeSequenceRunsEveryPrimaryCallbackAtItsDeadline()
{
var host = new ClockHost();
var vm = new VirtualMachine(BuildTwoEventSequence(), Table, host);
vm.Run();
Assert.Equal("exit", vm.HaltReason);
Assert.Equal(2, vm.Globals[0x100]);
Assert.Equal(0, vm.Globals.GetValueOrDefault(0x101));
Assert.Equal(new long[] { 10, 10 }, host.SleptDurations);
}
[Fact]
public void SequenceUsesCatchUpTargetWhenTheFollowingDeadlineHasPassed()
{
var host = new ClockHost(overshootMs: 15);
var vm = new VirtualMachine(BuildTwoEventSequence(), Table, host);
vm.Run();
Assert.Equal("exit", vm.HaltReason);
Assert.Equal(1, vm.Globals[0x100]);
Assert.Equal(1, vm.Globals[0x101]);
Assert.Equal(new long[] { 10 }, host.SleptDurations);
}
}

View File

@@ -6,6 +6,8 @@ namespace Age.Engine.Vm;
/// are shared across frames; everything here is per-call and discarded on return.</summary> /// are shared across frames; everything here is per-call and discarded on return.</summary>
internal sealed class ExecFrame internal sealed class ExecFrame
{ {
internal readonly record struct TimedCallback(long DeadlineMs, int PrimaryOffset, int CatchUpOffset);
public readonly Script Script; public readonly Script Script;
public int Pc; // entry instruction index public int Pc; // entry instruction index
public readonly Frame Locals = new(); public readonly Frame Locals = new();
@@ -22,6 +24,10 @@ internal sealed class ExecFrame
public int MouseCallbackTarget = -1; // op 0xcc target dword offset public int MouseCallbackTarget = -1; // op 0xcc target dword offset
public long MouseCallbackIntervalMs; public long MouseCallbackIntervalMs;
public long MouseCallbackNextAtMs; public long MouseCallbackNextAtMs;
public readonly List<TimedCallback> TimedCallbacks = new(); // ops 0xd3/0xd4/0xd5
public int TimedCallbackCursor;
public long? TimedCallbackStartedAtMs;
public int TimedCallbackAbortOffset = -1;
public readonly HotspotRegistry Hotspots = new(); public readonly HotspotRegistry Hotspots = new();
public ExecFrame(Script script, int pc) { Script = script; Pc = pc; } public ExecFrame(Script script, int pc) { Script = script; Pc = pc; }
} }

View File

@@ -739,6 +739,63 @@ public sealed class VirtualMachine
UpdatePointer((int)Read(a[0]), (int)Read(a[1])); return pc + 1; UpdatePointer((int)Read(a[0]), (int)Read(a[1])); return pc + 1;
case "sleep": // 0xc8 (duration) — pause the host duration ms; headless hosts no-op (parity). Frame pacing. case "sleep": // 0xc8 (duration) — pause the host duration ms; headless hosts no-op (parity). Frame pacing.
_host.Sleep(Read(a[0])); return pc + 1; _host.Sleep(Read(a[0])); return pc + 1;
case "u00425960":
case "begin-timed-callback-sequence": // 0xd3: clear the frame-local relative schedule
_cur.TimedCallbacks.Clear();
_cur.TimedCallbackCursor = 0;
_cur.TimedCallbackStartedAtMs = null;
_cur.TimedCallbackAbortOffset = -1;
return pc + 1;
case "u004266F0":
case "append-relative-timed-callbacks": // 0xd4: (interval ms, count, on-time PC, catch-up PC)
{
long intervalMs = Read(a[0]);
int count = System.Math.Max(0, unchecked((int)Read(a[1])));
int primaryOffset = unchecked((int)Read(a[2]));
int catchUpOffset = unchecked((int)Read(a[3]));
long deadlineMs = _cur.TimedCallbacks.Count == 0 ? 0 : _cur.TimedCallbacks[^1].DeadlineMs;
for (int i = 0; i < count; i++)
{
deadlineMs += intervalMs;
_cur.TimedCallbacks.Add(new ExecFrame.TimedCallback(
deadlineMs, primaryOffset, catchUpOffset));
}
return pc + 1;
}
case "u004262C0":
case "run-timed-callback-sequence": // 0xd5: dispatch each scheduled local callback, resuming here after ret
{
if (_cur.TimedCallbackStartedAtMs == null)
{
_cur.TimedCallbackStartedAtMs = _host.InputClockMilliseconds;
_cur.TimedCallbackAbortOffset = unchecked((int)Read(a[0]));
}
// Native op 0xd5 stops at last_index rather than count. The final entry is a
// look-ahead sentinel: it supplies the next deadline for the preceding event but
// is not itself dispatched.
if (_cur.TimedCallbackCursor >= _cur.TimedCallbacks.Count - 1)
{
_cur.TimedCallbackStartedAtMs = null;
return pc + 1;
}
var callback = _cur.TimedCallbacks[_cur.TimedCallbackCursor];
long elapsedMs = _host.InputClockMilliseconds - _cur.TimedCallbackStartedAtMs.Value;
if (elapsedMs < callback.DeadlineMs)
{
_host.Sleep(callback.DeadlineMs - elapsedMs);
elapsedMs = _host.InputClockMilliseconds - _cur.TimedCallbackStartedAtMs.Value;
}
bool fellBehind = _cur.TimedCallbackCursor + 1 < _cur.TimedCallbacks.Count
&& _cur.TimedCallbacks[_cur.TimedCallbackCursor + 1].DeadlineMs < elapsedMs;
int targetOffset = fellBehind ? callback.CatchUpOffset : callback.PrimaryOffset;
_cur.TimedCallbackCursor++;
if (targetOffset < 0 || !_cur.Script.IndexByOffset.TryGetValue(targetOffset, out int target))
return pc;
_cur.CallStack.Add(pc);
return target;
}
case "u0041B290": case "u0041B290":
case "set-message-skip": // 0x88: persistent all-message fast-forward service state case "set-message-skip": // 0x88: persistent all-message fast-forward service state
_messageSkipEnabled = Read(a[0]) != 0; _messageSkipEnabled = Read(a[0]) != 0;

View File

@@ -19,6 +19,9 @@ INFERRED: dict[int, dict] = {
0xc2: dict(name='fade-bgm', category='audio', noop=False, confidence='high', source='investigation', summary='(target_percent)(duration_ms) — block script service while linearly fading current BGM volume to 0..100%. Durations >=1000 ms use 100 steps; shorter fades use 10. Target 0 releases the current BGM source at completion.'), 0xc2: dict(name='fade-bgm', category='audio', noop=False, confidence='high', source='investigation', summary='(target_percent)(duration_ms) — block script service while linearly fading current BGM volume to 0..100%. Durations >=1000 ms use 100 steps; shorter fades use 10. Target 0 releases the current BGM source at completion.'),
0xcc: dict(name='register-mouse-callback', category='input', noop=False, confidence='high', source='investigation', summary='(poll_interval_ms)(target_pc) - register a timed per-frame mouse callback in the current script.'), 0xcc: dict(name='register-mouse-callback', category='input', noop=False, confidence='high', source='investigation', summary='(poll_interval_ms)(target_pc) - register a timed per-frame mouse callback in the current script.'),
0xcd: dict(name='dispatch-mouse-callback', category='input', noop=False, confidence='high', source='investigation', summary='Dispatch the registered mouse callback when its polling interval elapses.'), 0xcd: dict(name='dispatch-mouse-callback', category='input', noop=False, confidence='high', source='investigation', summary='Dispatch the registered mouse callback when its polling interval elapses.'),
0xd3: dict(name='begin-timed-callback-sequence', category='control', noop=False, confidence='high', source='investigation', summary="Clear and initialize the current script frame's timed local-callback sequence."),
0xd4: dict(name='append-relative-timed-callbacks', category='control', noop=False, confidence='high', source='investigation', summary="Append repeated relative deadlines and their on-time/catch-up local callback targets to the current frame's timed sequence."),
0xd5: dict(name='run-timed-callback-sequence', category='control', noop=False, confidence='high', source='investigation', summary='Start or service the frame-local timed sequence, dispatching scheduled local callbacks through the penultimate entry and selecting the catch-up target when the following deadline is already late.'),
0xd9: dict(name='clear-run-state-0x1000', category='control', noop=True, confidence='high', source='investigation', summary='Clear native run/service bit 0x1000; if the secondary context is active, clear the same bit there. SC0000 executes it once after the initial SFX-channel reset, with no VM-visible result.'), 0xd9: dict(name='clear-run-state-0x1000', category='control', noop=True, confidence='high', source='investigation', summary='Clear native run/service bit 0x1000; if the secondary context is active, clear the same bit there. SC0000 executes it once after the initial SFX-channel reset, with no VM-visible result.'),
0xfb: dict(name='register-joy-callback', category='input', noop=False, confidence='high', source='investigation', summary='(input_index)(target_pc) - register one of 32 per-frame joy/input callback targets.'), 0xfb: dict(name='register-joy-callback', category='input', noop=False, confidence='high', source='investigation', summary='(input_index)(target_pc) - register one of 32 per-frame joy/input callback targets.'),
0xff: dict(name='poll-joy-callback-input', category='input', noop=False, confidence='high', source='investigation', summary='Poll the current joy/input callback bitmask and initialize the per-dispatch scan state.'), 0xff: dict(name='poll-joy-callback-input', category='input', noop=False, confidence='high', source='investigation', summary='Poll the current joy/input callback bitmask and initialize the per-dispatch scan state.'),

View File

@@ -158,6 +158,56 @@ name = "sleep_timer"
type = "int" type = "int"
note = "sleep timer object (op 0xc8; +8 active, +0x14 start-ms, +0x18 duration)" note = "sleep timer object (op 0xc8; +8 active, +0x14 start-ms, +0x18 duration)"
[[field]] [[field]]
offset = 0x5f3ac
name = "timed_callback_timer"
type = "int"
note = "elapsed timer object armed by op 0xd5 and sampled by the timed callback scheduler"
[[field]]
offset = 0x5f690
name = "timed_callback_abort_pc"
type = "int"
note = "optional local fallback PC supplied to op 0xd5; -1 disables abort redirection"
[[field]]
offset = 0x5f694
name = "timed_callback_entries_vector"
type = "int"
note = "MSVC vector object base for 16-byte schedule entries; begin/end/capacity pointers follow"
[[field]]
offset = 0x5f698
name = "timed_callback_entries_begin"
type = "void*"
note = "begin pointer for 16-byte entries {deadline_ms, field_100, primary_pc, catchup_pc}"
[[field]]
offset = 0x5f69c
name = "timed_callback_entries_end"
type = "void*"
note = "end pointer for the timed callback entry vector"
[[field]]
offset = 0x5f6a0
name = "timed_callback_entries_capacity"
type = "void*"
note = "capacity-end pointer for the timed callback entry vector"
[[field]]
offset = 0x5f6a4
name = "timed_callback_last_index"
type = "int"
note = "last appended schedule index; op 0xd3 resets to -1 and op 0xd4 increments per entry"
[[field]]
offset = 0x5f6a8
name = "timed_callback_cursor"
type = "int"
note = "next schedule entry consumed by timed_callback_sequence_tick; op 0xd3 resets to zero"
[[field]]
offset = 0x5f6b4
name = "timed_callback_script_resource_id"
type = "uint"
note = "active frame script resource retained by op 0xd5 for local callback dispatch safety"
[[field]]
offset = 0x5f6b8
name = "timed_callback_script_resource_guard"
type = "uint"
note = "companion current-script identity checked before redirecting to a scheduled callback"
[[field]]
offset = 0x6da78 offset = 0x6da78
name = "message_skip_activation_guard" name = "message_skip_activation_guard"
type = "int" type = "int"
@@ -232,3 +282,8 @@ offset = 0xa0ce4
name = "run_state_flags" name = "run_state_flags"
type = "uint" type = "uint"
note = "interpreter run-state flags (bit1 sleeping; 0x8000000 skip/fast-forward)" note = "interpreter run-state flags (bit1 sleeping; 0x8000000 skip/fast-forward)"
[[field]]
offset = 0xa0ce8
name = "timed_callback_abort_signal"
type = "int"
note = "scheduler-polled abort/input signal cleared when op 0xd5 starts; redirects to abort PC when configured"

View File

@@ -2128,14 +2128,14 @@ argc = 0
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "u00425960" name = "begin-timed-callback-sequence"
category = "unknown" category = "control"
summary = "" summary = "Clear and initialize the current script frame's timed local-callback sequence."
noop_headless = false noop_headless = false
source = "kelebek" source = "investigation"
confidence = "low" confidence = "high"
depends_on = [] depends_on = []
evidence = "" evidence = "Ghidra /v2: op_0xd3_handler@0x41a430 clears the 16-byte-entry vector at ctx+0x5f694, sets last index ctx+0x5f6a4 and abort target ctx+0x5f690 to -1, and resets playback cursor ctx+0x5f6a8 to zero. HISTORY.BIN@0xbf9 begins its seven-event scrollbar interpolation schedule."
[[opcode]] [[opcode]]
op = 0xd4 op = 0xd4
@@ -2144,33 +2144,33 @@ argc = 4
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "u004266F0" name = "append-relative-timed-callbacks"
category = "unknown" category = "control"
summary = "" summary = "Append repeated relative deadlines and their on-time/catch-up local callback targets to the current frame's timed sequence."
noop_headless = false noop_headless = false
source = "kelebek" source = "investigation"
confidence = "low" confidence = "high"
depends_on = [] depends_on = []
evidence = "" evidence = "Ghidra /v2: op_0xd4_handler@0x428890 loops operand 2 times, accumulates operand 1 onto the preceding deadline, stores operand 3 at entry+8 and operand 4 at entry+0xc, and appends each 16-byte entry. HISTORY.BIN@0xbfa/0xc03/0xc0c produces deadlines 10,20,30,40,50,51,52 ms."
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 1 i = 1
role = "" role = "relative interval (ms)"
observed_types = ["imm", "l-int"] observed_types = ["imm", "l-int"]
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 2 i = 2
role = "" role = "repeat count"
observed_types = ["imm", "l-int"] observed_types = ["imm", "l-int"]
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 3 i = 3
role = "" role = "on-time local callback code target"
observed_types = ["imm"] observed_types = ["imm"]
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 4 i = 4
role = "" role = "catch-up local callback code target"
observed_types = ["imm"] observed_types = ["imm"]
[[opcode]] [[opcode]]
@@ -2180,18 +2180,18 @@ argc = 1
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "u004262C0" name = "run-timed-callback-sequence"
category = "unknown" category = "control"
summary = "" summary = "Start or service the frame-local timed sequence, dispatching scheduled local callbacks through the penultimate entry and selecting the catch-up target when the following deadline is already late."
noop_headless = false noop_headless = false
source = "kelebek" source = "investigation"
confidence = "low" confidence = "high"
depends_on = [] depends_on = []
evidence = "" evidence = "Ghidra /v2: op_0xd5_handler@0x421090 retains the current script resource, starts the ctx+0x5f3ac timer, sorts the schedule, and keeps run-state bit 0x40 active only while cursor < last_index, making the final entry a non-dispatched look-ahead sentinel. timed_callback_sequence_tick@0x408170 waits to each deadline, compares the following entry deadline with elapsed time to select entry+8 versus entry+0xc, redirects the frame PC, and advances ctx+0x5f6a8. Operand 1 is an optional abort/fallback PC; HISTORY passes 0xffffffff."
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 1 i = 1
role = "" role = "abort/fallback local code target (-1 disables)"
observed_types = ["imm"] observed_types = ["imm"]
[[opcode]] [[opcode]]