re(gfx): decode sleep 0xc8 timing (non-blocking ms timer) + 0x20c present

- sleep_op_0xc8 @0x420ec0: arms a non-blocking main-loop-polled timer
  (sleep_timer_arm @0x44cff0); operand = milliseconds. Also carries
  anti-tamper + gfx cmd-type 3 (not needed host-side).
- 0x20c = gfx_op_0x20c_present_frame (host presents continuously -> noop_headless).
- Ghidra annotated + saved; opcodes.toml rebuilt (lint clean); engine-re.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-07-08 08:57:22 -04:00
parent afb76c66aa
commit 6ce4da05a1
3 changed files with 57 additions and 22 deletions

View File

@@ -378,6 +378,33 @@ CG draws as "slot 0"). Verified against native code + the raw bytecode:
0xc8`), a separate subsystem from the transform/alpha channel. **Lesson: never characterise the engine's render
mechanism from our own VM's oracle output — use native code + raw bytecode.**
### `sleep` (op `0xc8`) — the frame-pacing primitive (2026-07-08, decoded)
Handler resolved via the dispatch table (`ctx[0x26c93+0xc8]` = `param_1[0x26d5b]` in `FUN_00413860`) →
**`sleep_op_0xc8`@`0x420ec0`** (was `LAB_00420ec0`; created + annotated). It is **NON-BLOCKING**:
- It **arms a timer** — `sleep_timer_arm`@`0x44cff0` on the object at `ctx+0x5f304`: `+8 = 1` (active),
`+0x14 = (*DAT_0056f3d4)()` (start tick — an **ms** source, `timeGetTime`/`GetTickCount` class, same
`DAT_0056f3d4` the boot uses to seed `srand` via `time/100`), `+0x18 = duration` (operand, min 1). The engine's
main loop polls `elapsed ≥ duration` and resumes the script — rendering continues in the meantime. This is the
native confirmation that the engine paces animation in its per-frame loop, not by blocking.
- **Operand unit = MILLISECONDS.** `duration < 10` fast-paths through import `[0x56f0b8]`; every real scene sleep
(`100`/`750`/`1000` in SC0000) is `≥ 10` → the timer-arm path.
- The handler also writes gfx **cmd-type 3** into the current object record (`ctx+0x53d88+curidx*0x78`) and runs
two **anti-tamper** checks (call `[ctx+0x5512c]`; a rotate-checksum compare of `ctx+0x55120/0x55124`;
`__CxxThrowException` on mismatch — integrity work piggybacked on a hot op). Neither is needed by our model.
**Port equivalent (implemented):** our VM runs on a background thread (like `wait-for-input`), so blocking that
thread for `duration` ms while the main-thread compositor (`Main.Recomposite` in `_Process`) keeps presenting is
behaviorally equivalent to the native non-blocking timer — the `sleep`-paced opening `AE*` burst now gets frames
to display. Headless/CLI hosts no-op `Sleep` (parity). `IHost.Sleep(long)` + VM `case "sleep"`; see
`vm-map/opcodes.toml` 0xc8.
**Related — `present-frame` (op `0x20c`):** dispatch `param_1[0x26e9f] = gfx_op_0x20c_present_frame` →
`gfx_render_frame`@`0x4820b0` (buffer flip). Our compositor presents every frame regardless, so `0x20c` is a VM
no-op (`noop_headless=true`); the Kelebek label `u00416200` was VA-drift. This corrects the earlier open item
("no per-frame present") above — present is host-implicit; only `sleep` timing was missing.
### The render drift's SECOND half: missing system-boot state (2026-07-07, resolved)
Implementing the gfx ops (above) was necessary but not sufficient — a cold single-scene run of SC0000 still