From 6044a0d412083a65426fe726b10f49a009f3fb73 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Wed, 8 Jul 2026 08:57:22 -0400 Subject: [PATCH] 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 --- docs/engine-re.md | 27 +++++++++++++++++++++++++++ docs/opcode-reference.md | 22 ++++++++++++++-------- vm-map/opcodes.toml | 30 ++++++++++++++++-------------- 3 files changed, 57 insertions(+), 22 deletions(-) diff --git a/docs/engine-re.md b/docs/engine-re.md index 193166a..1a5f094 100644 --- a/docs/engine-re.md +++ b/docs/engine-re.md @@ -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 diff --git a/docs/opcode-reference.md b/docs/opcode-reference.md index aff7867..1a11f1f 100644 --- a/docs/opcode-reference.md +++ b/docs/opcode-reference.md @@ -57,6 +57,13 @@ This also names the whole call graph statically (build/callscript-names.json). - **grounding:** source=investigation, confidence=high - **evidence:** native-RE (Ghidra): handler FUN_0041fba0 (= ctx[0x26c93+0x8f]) sets [frame PC @+0x53d2c] = [frame codebase @+0x53d28] + operand*4 and pushes ((pc-base)>>2)+3 onto the per-frame return stack ([ctx+0x552e8]/[ctx+0x55248]). Target is a code OFFSET within the current script (matches header table T3 tag 0x8F = local call targets), confirming it is a local JSR, not a script load. +### 0xc8 `sleep` (sleep, argc 1) +- **summary:** Pause the script for milliseconds while rendering continues (frame pacing). +- **grounding:** source=investigation, confidence=high +- **evidence:** Ghidra: dispatch ctx[0x26c93+0xc8]=0x420ec0; sleep_op_0xc8 + sleep_timer_arm decoded/annotated 2026-07-08. docs/engine-re.md sleep section. + +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 blocks the VM background thread ms while the per-frame compositor keeps presenting -> the sleep-paced opening AE* burst animates. Headless hosts no-op it (parity). + ## draw ### 0x1a2 `gfx-cmd-register` (gfx-cmd-register, argc 1) @@ -109,6 +116,13 @@ This also names the whole call graph statically (build/callscript-names.json). - **grounding:** source=inference, confidence=med - **evidence:** SC0000 label_12649: set-texture(resId,slot) then 0x208(slot)->w,h feeds w/2 horizontal-center + foot-anchor subtraction into draw-texture dst; stubbing yields 0x0 sizes / off-center draws +### 0x20c `present-frame` (present-frame, argc 0) +- **summary:** Present the composited frame (native gfx_render_frame). Host-implicit: our compositor presents every frame. +- **grounding:** source=investigation, confidence=high, noop_headless=True +- **evidence:** Ghidra: dispatch table FUN_00413860 param_1[0x26e9f]=gfx_op_0x20c_present_frame; 0x26e9f-0x26c93=0x20c. 2026-07-08. + +Native handler gfx_op_0x20c_present_frame (dispatch ctx[0x26c93+0x20c]) -> gfx_render_frame @0x4820b0 flips the composited buffers. Our Godot host runs a continuous per-frame compositor (Main.Recomposite in _Process), so an explicit present is redundant and the VM can skip it. noop_headless=true -> scene coverage classifies it safe-noop. Kelebek label u00416200 was VA-drift (unrelated fn); real handler resolved via the dispatch table. + ### 0x212 `set-gfx-field64` (set-gfx-field64, argc 2) - **summary:** 0x212 (obj_idx)(val) — gfx cmd-type 5. Handler gfx_op_0x212_set_field64 @0x4230c0: obj=[ctx+0x14d54 + obj_idx*4]; if obj: *(obj+0x64)=val. Sets one per-object field. See docs/engine-re.md gfx op-contract table. - **grounding:** source=investigation, confidence=high @@ -514,10 +528,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 -### 0xc8 `sleep` (sleep, argc 1) -- **summary:** — -- **grounding:** source=kelebek, confidence=med - ### 0xcc `mouse_callback` (mouse_callback, argc 2) - **summary:** — - **grounding:** source=kelebek, confidence=med @@ -914,10 +924,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 -### 0x20c `u00416200` (u00416200, argc 0) -- **summary:** — -- **grounding:** source=kelebek, confidence=low - ### 0x20d `u00420E10` (u00420E10, argc 1) - **summary:** — - **grounding:** source=kelebek, confidence=low diff --git a/vm-map/opcodes.toml b/vm-map/opcodes.toml index 9c76d72..f87e91d 100644 --- a/vm-map/opcodes.toml +++ b/vm-map/opcodes.toml @@ -2036,17 +2036,18 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "sleep" -category = "unknown" -summary = "" +category = "control" +summary = "Pause the script for milliseconds while rendering continues (frame pacing)." +details = "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 blocks the VM background thread ms while the per-frame compositor keeps presenting -> the sleep-paced opening AE* burst animates. Headless hosts no-op it (parity)." noop_headless = false -source = "kelebek" -confidence = "med" +source = "investigation" +confidence = "high" depends_on = [] -evidence = "" +evidence = "Ghidra: dispatch ctx[0x26c93+0xc8]=0x420ec0; sleep_op_0xc8 + sleep_timer_arm decoded/annotated 2026-07-08. docs/engine-re.md sleep section." [[opcode.semantics.args]] i = 1 -role = "" +role = "duration_ms" observed_types = ["imm"] [[opcode]] @@ -5101,19 +5102,20 @@ observed_types = ["imm", "l-int"] [[opcode]] op = 0x20c -label = "u00416200" +label = "present-frame" argc = 0 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "u00416200" -category = "unknown" -summary = "" -noop_headless = false -source = "kelebek" -confidence = "low" +name = "present-frame" +category = "draw" +summary = "Present the composited frame (native gfx_render_frame). Host-implicit: our compositor presents every frame." +details = "Native handler gfx_op_0x20c_present_frame (dispatch ctx[0x26c93+0x20c]) -> gfx_render_frame @0x4820b0 flips the composited buffers. Our Godot host runs a continuous per-frame compositor (Main.Recomposite in _Process), so an explicit present is redundant and the VM can skip it. noop_headless=true -> scene coverage classifies it safe-noop. Kelebek label u00416200 was VA-drift (unrelated fn); real handler resolved via the dispatch table." +noop_headless = true +source = "investigation" +confidence = "high" depends_on = [] -evidence = "" +evidence = "Ghidra: dispatch table FUN_00413860 param_1[0x26e9f]=gfx_op_0x20c_present_frame; 0x26e9f-0x26c93=0x20c. 2026-07-08." [[opcode]] op = 0x20d