Implement History timed callback scrolling

This commit is contained in:
gamer147
2026-07-19 21:15:15 -04:00
parent 3fa09b8b42
commit 5ba16505d6
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) |
| `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) |
| `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 |
| `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) |
@@ -51,3 +61,4 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch-
| `0xa0cc4` | `screen_h` | `int` | screen height (480) |
| `0xa0cc8` | `screen_bpp` | `int` | screen bpp (8) |
| `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.
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
gaps. None changes backlog ownership or requires choosing a save/profile backend.
#### Timed local-callback scheduler and History smooth scrolling (2026-07-19)
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
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).
### 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)
- **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
@@ -847,18 +862,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
### 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)
- **summary:** —
- **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.
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.
### 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`.