Refresh retained presentation references

This commit is contained in:
gamer147
2026-07-10 21:56:57 -04:00
parent 82afcf5506
commit 8bee45f483
10 changed files with 102 additions and 100 deletions

View File

@@ -88,11 +88,12 @@ highest-risk area of the port. This doc is the steering state; it feeds the A2b
(Resolve + BMP path) + `GodotAdvHost` texture ops → `TextureRect` compositing behind the dialogue; (Resolve + BMP path) + `GodotAdvHost` texture ops → `TextureRect` compositing behind the dialogue;
`IHost.DrawTexture` extended with dst x/y; 800×600 window; `convert_agf.py --scene` pre-converts a `IHost.DrawTexture` extended with dst x/y; 800×600 window; `convert_agf.py --scene` pre-converts a
scene's manifest AGFs → BMP. The full-screen **event-CG layer renders end-to-end** from the executed scene's manifest AGFs → BMP. The full-screen **event-CG layer renders end-to-end** from the executed
bytecode. **Limitations (next chunk = graphics geometry/blend):** sprites + `BG*` (routed through the bytecode. **Historical limitations at first landing (subsequently resolved in the Phase-A graphics
CG-load subroutine) have garbage geometry because native graphics ops are stubbed (`0x208` slices):** sprites + `BG*` (routed through the CG-load subroutine) had garbage geometry because native
get-texture-size + the sprite position/animation chain); fades (`AE*`) draw opaque (no alpha); slot graphics ops were stubbed (`0x208` get-texture-size + the sprite position/animation chain); fades
model approximates the game's immediate-mode blit-onto-slot-0 canvas. See `docs/phase-a-slice-plan.md` (`AE*`) drew opaque (no alpha); the slot
(A2b section) for the full write-up + the graphics-subsystem plan. model approximated the game's immediate-mode blit-onto-slot-0 canvas. See `docs/phase-a-slice-plan.md`
(A2b section) for the implementation history and current retained-object model.
4. **Audio.** **✅ WIRED (2026-07-06) — no Frida needed.** Same rule as textures: 4. **Audio.** **✅ WIRED (2026-07-06) — no Frida needed.** Same rule as textures:
`play-bgm(id)`/`play-voice(id)``files[section_base(scene)+id]` → OGG. `IHost.PlayBgm/PlayVoice` + `play-bgm(id)`/`play-voice(id)``files[section_base(scene)+id]` → OGG. `IHost.PlayBgm/PlayVoice` +
VM dispatch (`play-bgm` 0xbf / `play-voice` 0xc4, both argc 1); `ResourceMap.AudioPath` → loose VM dispatch (`play-bgm` 0xbf / `play-voice` 0xc4, both argc 1); `ResourceMap.AudioPath` → loose

View File

@@ -341,8 +341,8 @@ inside the loop (it lives in the native video service) — we approximate it hos
(138 corpus scripts; all have the same terminal-check shape), synthesizes `G[0xaba5c]=1` on top-level (138 corpus scripts; all have the same terminal-check shape), synthesizes `G[0xaba5c]=1` on top-level
scene entry, forces one setup-body pass even if `G[0x6be]` is stale, then returns the terminal immediate scene entry, forces one setup-body pass even if `G[0x6be]` is stale, then returns the terminal immediate
discovered from the following `mov`/`eq` pair. Thus no SC0000 offset is hardcoded. Op `0x7b` retains the discovered from the following `mov`/`eq` pair. Thus no SC0000 offset is hardcoded. Op `0x7b` retains the
saved handler PCs as frame metadata; op `0x7c` is a host-scheduler marker because `IHost.FrameYield` and saved handler PCs as frame metadata; op `0x7c` is a host-scheduler marker because the host already owns
`FrameClock` already own per-frame pacing. `TITLE.BIN`'s unrelated `"BIN","SC????.BIN"` service remains service-boundary suspension and retained presentation. `TITLE.BIN`'s unrelated `"BIN","SC????.BIN"` service remains
stubbed. The real video-service timing remains intentionally unmodeled. stubbed. The real video-service timing remains intentionally unmodeled.
**Magic-circle retention fixed in the host model (2026-07-09).** The effect is `AE001H.AGF` **Magic-circle retention fixed in the host model (2026-07-09).** The effect is `AE001H.AGF`
@@ -409,7 +409,8 @@ Worker functions decoded + annotated in the Ghidra project (updated 2026-07-09):
#### The `0x21c0x243` sprite transform / ANIMATION cluster (2026-07-10, partial implementation) #### The `0x21c0x243` sprite transform / ANIMATION cluster (2026-07-10, partial implementation)
The scene-completeness tracker (`tools/scene_opcode_coverage.py`) flagged a dense band of GAP ops in The scene-completeness tracker (`tools/scene_opcode_coverage.py`) flagged a dense band of GAP ops in
`0x21c0x243` (+ `0x2bd/0x2bf`) — the **largest remaining rendering unknown** in SC0000 (e.g. `0x220`×66, `0x21c0x243` (+ `0x2bd/0x2bf`) — at that point the **largest remaining rendering unknown** in SC0000
(e.g. `0x220`×66,
`0x22f`×34, `0x228`×33, `0x21e`×25 static sites). Resolving every one through the dispatch table `0x22f`×34, `0x228`×33, `0x21e`×25 static sites). Resolving every one through the dispatch table
(`ctx[0x26c93+op]`, read from `FUN_00413860`) shows it is **one coherent subsystem: sprite transform + (`ctx[0x26c93+op]`, read from `FUN_00413860`) shows it is **one coherent subsystem: sprite transform +
animation/tween** — and two members were already named in prior RE (`0x234 gfx_op_0x234_anim_start`, animation/tween** — and two members were already named in prior RE (`0x234 gfx_op_0x234_anim_start`,
@@ -587,24 +588,22 @@ Handler resolved via the dispatch table (`ctx[0x26c93+0xc8]` = `param_1[0x26d5b]
thread for `duration` ms while the main-thread compositor (`Main.Recomposite` in `_Process`) keeps presenting is thread for `duration` ms while the main-thread compositor (`Main.Recomposite` in `_Process`) keeps presenting is
behaviorally equivalent to the native non-blocking timer. This correctly reproduces the **explicit one-shot behaviorally equivalent to the native non-blocking timer. This correctly reproduces the **explicit one-shot
sleeps** (the dramatic 1000/750/200 ms holds). Headless/CLI hosts no-op `Sleep` (parity). `IHost.Sleep(long)` + sleeps** (the dramatic 1000/750/200 ms holds). Headless/CLI hosts no-op `Sleep` (parity). `IHost.Sleep(long)` +
VM `case "sleep"`; see `vm-map/opcodes.toml` 0xc8. **⚠ It does NOT make the rapid opening `AE*` burst animate** VM `case "sleep"`; see `vm-map/opcodes.toml` 0xc8. **It does not pace ordinary opcode bursts.** The later
— execution trace shows the back-to-back `set-texture→draw-texture` swaps have no `sleep`/`wait`/`present`/ native presentation trace proved that back-to-back retained mutations execute within milliseconds and are
coroutine between them, so bare `sleep` was never their pacer; what advances that burst is still unknown (an first published at the following `0x20c`/`0x21c`, sleep, or stable input boundary. An earlier claim that the
earlier claim that "the opening is sleep-paced" was inherited from this doc and never execution-verified — opening was generally sleep-paced was inherited without execution evidence and is superseded.
corrected). Profile the real Godot run (`--trace-histogram`) to find it.
**Related — `present-frame` (op `0x20c`):** dispatch `param_1[0x26e9f] = gfx_op_0x20c_present_frame` **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 `gfx_render_frame`@`0x4820b0` (buffer flip). The port treats `0x20c` as an explicit retained-state publication
no-op (`noop_headless=true`); the Kelebek label `u00416200` was VA-drift. This corrects the earlier open item boundary in the interactive host while keeping it `noop_headless=true`; the Kelebek label `u00416200` was
("no per-frame present") above — present is host-implicit; only `sleep` timing was missing. VA-drift. This supersedes the earlier host-implicit/no-op presentation model.
### Frame cadence — the interpreter tick, and why our port "speeds through" (2026-07-08) ### Frame cadence — the interpreter tick, and why our port "speeds through" (2026-07-08)
Answers the open question the `sleep` section above left ("what advances the rapid opening burst is still **Historical reconstruction, corrected by the synchronized presentation trace below.** This pass correctly
unknown"). The pace is an **engine-level execution cadence**, not any bytecode primitive. Corroborated in-game identified the one-op interpreter tick and ADV Ctrl state, but incorrectly inferred a constant engine-level
by Ctrl fast-forwarding ADV (a speed governor). Motivated by the user's observation that our port visibly opcode cadence. The outer native service invokes that tick in bursts and publishes retained state only at
speeds through the opening — which contradicted, and correctly overturned, an earlier same-day overclaim that specific service boundaries.
"there is no missing pacer" (that was inferred from headless op-counts, which cannot render).
**Confirmed from the engine image (annotated in Ghidra):** **Confirmed from the engine image (annotated in Ghidra):**
@@ -626,7 +625,8 @@ speeds through the opening — which contradicted, and correctly overturned, an
and the anim clock (0x238) advance per frame. A CG transition therefore spreads over many real frames. and the anim clock (0x238) advance per frame. A CG transition therefore spreads over many real frames.
- **Timing source** = the ms-clock function pointer `*DAT_0056f3d4` (`timeGetTime`-class), used throughout. - **Timing source** = the ms-clock function pointer `*DAT_0056f3d4` (`timeGetTime`-class), used throughout.
**Model:** single-threaded, vsync-timed frame loop; each frame it steps opcodes until the context **yields** **Model proposed at this stage (partially superseded):** single-threaded frame loop; each service pass steps
opcodes until the context **yields**
(`sleep` armed / `wait-for-input` 0x72 / active frame-stepped transition/anim / present), renders (`sleep` armed / `wait-for-input` 0x72 / active frame-stepped transition/anim / present), renders
(`gfx_render_frame`), waits on the clock, continues. Back-to-back draws inside one page compose into a single (`gfx_render_frame`), waits on the clock, continues. Back-to-back draws inside one page compose into a single
frame (fine); the opening's CG-to-CG advances are gated by frame-stepped transitions + sleeps, which spread frame (fine); the opening's CG-to-CG advances are gated by frame-stepped transitions + sleeps, which spread
@@ -641,12 +641,10 @@ statement above is a **reconstruction** from those pieces, not a line read from
loop + its exact per-frame step budget / vsync wait needs a **live-debugger break** (attach + break in the loop + its exact per-frame step budget / vsync wait needs a **live-debugger break** (attach + break in the
frame loop), or a wider memory dump that includes the mode object. frame loop), or a wider memory dump that includes the mode object.
**Port relevance (the speed-through root cause).** Our Godot VM runs on a **free-running background thread** **Historical port conclusion (superseded):** this pass prescribed a bounded wall-clock opcode rate and led to
(`Task.Run(() => vm.Run())` in `Main.cs`) with no frame binding — it executes an entire page's ops in the frame-stepped implementation documented in the historical spec. The 2026-07-10 native trace disproved that
microseconds; only `WaitForInput` and `Sleep` pause it, and the compositor merely samples `GfxState` at 60fps. rate model: ordinary opcode work must remain burst-fast, while `0x20c`, `0x21c`, sleep, and stable input waits
So every no-`sleep` CG/state advance collapses to its end state → the speed-through. **Fix shape:** throttle own publication/pacing. Commit `85fc07d` implements the corrected boundary model.
the VM to a bounded wall-clock op rate (see the measured numbers below) via a per-opcode host yield; retire the
free-running thread. Spec: `docs/superpowers/specs/2026-07-08-frame-stepped-vm-design.md`.
### Frame cadence — live measurement (2026-07-08, Frida read-only) ### Frame cadence — live measurement (2026-07-08, Frida read-only)
@@ -657,23 +655,19 @@ way:** a first attempt with a **CModule** hook on the hyper-hot `adv_interpreter
instantly (bad native callback into the hottest path — *not* anti-tamper; our other scripts hook engine code instantly (bad native callback into the hottest path — *not* anti-tamper; our other scripts hook engine code
via plain JS and survive). Use plain-JS hooks on proven addresses + memory polling. via plain JS and survive). Use plain-JS hooks on proven addresses + memory polling.
Findings: Findings, corrected by the later synchronized presentation trace:
- **Execution is rate-limited, not free-running.** Normal active rate ≈ **1,788 operand-fetches/sec** (peak - Normal active capture measured about **1,788 operand fetches/sec** (peak ~5,796). This is neither a
~5,796) — far below an unthrottled interpreter (millions/sec), so the engine paces itself. Execution is completed-opcode count nor a scheduler budget; it mixes burst execution with native-service parking.
bursty (parked at `wait-for-input` prompts, then a bounded burst), confirming per-iteration op-budgeting. - **Fast-forward (Ctrl)** raised operand-fetch activity about 4× (≈7,738/sec avg, peak ~15,572), gated by
- **Fast-forward (Ctrl) scales the rate ~4×** (≈7,738/sec avg, peak ~15,572), gated by the engine skip bit **`ctx+0xa0ce4 & 0x8000000`**. This remains useful evidence that Ctrl is ADV-scoped, but it does not define
**`ctx+0xa0ce4 & 0x8000000`** (set only while fast-forwarding). It runs *more ops per unit time* — it does a constant opcode cadence.
not skip content. (Ctrl is **ADV-scoped**; it does not speed up gameplay/menus.) - Rendering is Direct3D 9 and uncapped. The candidate D3D9 `Present` hook fired around 1,908/sec in the
- **Rendering = Direct3D 9, UNCAPPED.** `ddraw.dll` is not loaded; the game uses `d3d9.dll` (+ `nvd3dum.dll`). original probe; there is no fixed display refresh boundary to copy into the port.
`IDirect3DDevice9::Present` (device vtable slot 17, found by scanning ctx for a d3d9-vtable object with a - **Final scheduler implication (2026-07-10):** ordinary opcode work is burst-fast between explicit native
full ~119-method table) fires ~**1,908/sec** with **no vsync**; `BeginScene`/`EndScene` never fire → a **2D service boundaries. AE001D bind + mode + color-target writes completed within ~5 ms with no
StretchRect-style compositor**, not a 3D scene. So there is **no fixed display-frame rate**; `Present` rate `gfx_render_frame`; rendering began only at `0x21c`. The old 200-completed-op/s calibration folded service
≈ op rate (~1 op per present). ⇒ the pacing quantity is the **wall-clock op rate**, not a per-frame budget. waits into script time and is discarded. Godot `FrameYield` is non-blocking; `0x20c`, `0x21c`, sleep, and
- **Corrected implication for the port (2026-07-10):** 1,788/s is an operand-fetch rate, not an opcode rate. input own publication/pacing.
`IHost.FrameYield` runs once per completed opcode, so matching those quantities directly overclocked the
port by roughly ninefold. The native SC0000 transform lifetime pins the corresponding completed-op cadence
at about **200/s**. The limiter must be wall-clock based (not a per-render callback budget), with a future
ADV Ctrl multiplier applied to the same unified clock.
### The render drift's SECOND half: missing system-boot state (2026-07-07, resolved) ### The render drift's SECOND half: missing system-boot state (2026-07-07, resolved)
@@ -688,10 +682,10 @@ the CGs **de-collapse and render correctly** (screenshot-confirmed). **This is t
(a)-vs-(b) debate: the drift needed BOTH the native gfx ops (b) AND boot state (a) — specifically INIT2's (a)-vs-(b) debate: the drift needed BOTH the native gfx ops (b) AND boot state (a) — specifically INIT2's
handle array, never before identified (it is not a story flag).** Note two distinct boots: our Phase-B handle array, never before identified (it is not a story flag).** Note two distinct boots: our Phase-B
`--boot` runs the *data* `*INIT` scripts (skills/items/…); this is the *system* boot (`SYSTEM4` prefix) — a `--boot` runs the *data* `*INIT` scripts (skills/items/…); this is the *system* boot (`SYSTEM4` prefix) — a
"full boot" should run both. **Residual:** the `AE*` fade/flash effects still draw opaque (alpha/blend "full boot" should run both. **Historical residual (subsequently resolved):** this capture still had opaque
deferred — Phase 2 scope), and some object-slot CGs start with a zero anchor (cold gfx objects vs the real `AE*` fade/flash effects and zero-anchor object-slot CGs. The later blend, geometry, animation, and retained-
game's warm ones; default object geometry is confirmed `(0,0)` in `gfx_object_init_default`, so it is not a presentation subsections below supersede that state; default object geometry remains confirmed `(0,0)` in
missing-default bug). See `docs/phase-a-slice-plan.md` A2b-Geometry. `gfx_object_init_default`.
### The gfx animation/effects subsystem — the `AE*` fades (2026-07-07) ### The gfx animation/effects subsystem — the `AE*` fades (2026-07-07)
@@ -971,4 +965,5 @@ includes the scene's own early writes (can exclude by codebase for a pure pre-sc
- ~~**gfx command-buffer**~~ — **DONE** (the `0x2120x21a` positioned-object subsystem = the rendering - ~~**gfx command-buffer**~~ — **DONE** (the `0x2120x21a` positioned-object subsystem = the rendering
drift): all 14 ops reversed + implemented against a host-side `GfxState`, and the missing INIT2 boot drift): all 14 ops reversed + implemented against a host-side `GfxState`, and the missing INIT2 boot
state supplied via `--boot`. CGs render (screenshot-confirmed). See the op `0x215` finding + "The render state supplied via `--boot`. CGs render (screenshot-confirmed). See the op `0x215` finding + "The render
drift's SECOND half" above. Remaining: `AE*` alpha/blend (deferred) and cold-object anchors. drift's SECOND half" above. The then-remaining `AE*` blend and cold-anchor work is resolved by the later
blend, geometry, animation, and retained-presentation sections.

View File

@@ -68,11 +68,11 @@ This also names the whole call graph statically (build/callscript-names.json).
- **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. - **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) ### 0xc8 `sleep` (sleep, argc 1)
- **summary:** Pause the script for <duration> milliseconds while rendering continues (frame pacing). - **summary:** Pause the current script for <duration> milliseconds while retained presentation continues.
- **grounding:** source=investigation, confidence=high - **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. - **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 <duration> ms while the per-frame compositor keeps presenting -> correctly reproduces the explicit one-shot dramatic pauses. NOTE: does NOT pace the rapid opening AE* burst (those draws have no sleep between them; their real pacer is unknown). 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).
### 0x140 `coroutine-label-yield` (u0041F9C0, argc 4) ### 0x140 `coroutine-label-yield` (u0041F9C0, argc 4)
- **summary:** (out)(name_str)(sub_str)(in) — scene-coroutine LOOP ITERATOR / labeled yield. Handler copies name/sub strings + the int operand and calls the NATIVE video/transition service (*DAT_005c6018)(8, ctx[0x54fe8], &{name,sub,in}); writes the returned PC-like value to operand 1. In SC0000 label_462 'ループ開始' (@0x46d): `out=G[0x6be]=LABEL('J',G[0x6be])`; loop runs the intro-setup body (incl. call label_125bd = slot-table fill G[0x3239..0x324e]=4..11) and jmps back until out==G[0x6c3] (a per-scene exit-PC immediate) → mov aba5c 0 → content. The gate G[0xaba5c]==1 that opens this loop is NATIVE scene-entry state (no script sets it to 1). DAT_005c6018 is runtime-resolved (all xrefs READ) = SAME class as the DirectDraw workers we don't model. PORT = HOST-MODEL IMPLEMENTED: synthesize the ADV scene-entry gate, run the LABEL/J setup body once, then return the structurally discovered per-scene terminal; do not emulate the video service. See engine-re.md §Scene-coroutine framework. - **summary:** (out)(name_str)(sub_str)(in) — scene-coroutine LOOP ITERATOR / labeled yield. Handler copies name/sub strings + the int operand and calls the NATIVE video/transition service (*DAT_005c6018)(8, ctx[0x54fe8], &{name,sub,in}); writes the returned PC-like value to operand 1. In SC0000 label_462 'ループ開始' (@0x46d): `out=G[0x6be]=LABEL('J',G[0x6be])`; loop runs the intro-setup body (incl. call label_125bd = slot-table fill G[0x3239..0x324e]=4..11) and jmps back until out==G[0x6c3] (a per-scene exit-PC immediate) → mov aba5c 0 → content. The gate G[0xaba5c]==1 that opens this loop is NATIVE scene-entry state (no script sets it to 1). DAT_005c6018 is runtime-resolved (all xrefs READ) = SAME class as the DirectDraw workers we don't model. PORT = HOST-MODEL IMPLEMENTED: synthesize the ADV scene-entry gate, run the LABEL/J setup body once, then return the structurally discovered per-scene terminal; do not emulate the video service. See engine-re.md §Scene-coroutine framework.

View File

@@ -391,6 +391,9 @@ The chunk the animation-subsystem note above flagged as "the clearly-scoped next
> pauses, the `GfxState` race fix, and the tooling. Related: the "493k sleeps" that confused me were a **headless > pauses, the `GfxState` race fix, and the tooling. Related: the "493k sleeps" that confused me were a **headless
> artifact** (the name-entry poll loop), since fixed — see the "Headless divergence" note below. > artifact** (the name-entry poll loop), since fixed — see the "Headless divergence" note below.
> **Later resolution (2026-07-10):** the matching native presentation trace below established that ordinary
> mutations run burst-fast and first become visible at `0x20c`, `0x21c`, sleep, or stable input boundaries.
**What `sleep` actually is (RE-confirmed, correct):** the Godot compositor (`Main.Recomposite` in `_Process`) **What `sleep` actually is (RE-confirmed, correct):** the Godot compositor (`Main.Recomposite` in `_Process`)
presents live `GfxState` every frame; `sleep` (`0xc8`) was a GAP so the VM ran the whole burst in microseconds. presents live `GfxState` every frame; `sleep` (`0xc8`) was a GAP so the VM ran the whole burst in microseconds.
Implementing it makes the **explicit one-shot sleeps** (1000/750/200 ms) pause correctly — but those are the Implementing it makes the **explicit one-shot sleeps** (1000/750/200 ms) pause correctly — but those are the
@@ -560,6 +563,11 @@ yield/sync point) instead of the free-running thread. Confirm any fix against **
### A2b — Frame-stepped VM ✅ DONE & MERGED (2026-07-08) ### A2b — Frame-stepped VM ✅ DONE & MERGED (2026-07-08)
**Historical implementation, superseded on 2026-07-10 by the native retained-presentation trace below.**
The validation recorded here was accurate for that slice, but the inferred native per-op cadence was not:
service waits had been folded into an operand-fetch average. Commit `85fc07d` removed the production opcode
throttle; ordinary work now runs burst-fast to proven publication/pacing boundaries.
Executed the "Next (open)" list above (option 3: a per-frame yield/sync point). Spec Executed the "Next (open)" list above (option 3: a per-frame yield/sync point). Spec
`docs/superpowers/specs/2026-07-08-frame-stepped-vm-design.md`, plan `docs/superpowers/specs/2026-07-08-frame-stepped-vm-design.md`, plan
`docs/superpowers/plans/2026-07-08-frame-stepped-vm.md`; merged to `main` (`74a4221`). `docs/superpowers/plans/2026-07-08-frame-stepped-vm.md`; merged to `main` (`74a4221`).
@@ -665,7 +673,7 @@ video service behind op `0x140`.
stale prior-scene `out` value and avoids hardcoding SC0000's `0x45e`; all 138 corpus ADV sites share the stale prior-scene `out` value and avoids hardcoding SC0000's `0x45e`; all 138 corpus ADV sites share the
same shape. same shape.
4. Record op `0x7b`'s two saved handler PCs as frame metadata. Consume op `0x7c` as the host-scheduler 4. Record op `0x7b`'s two saved handler PCs as frame metadata. Consume op `0x7c` as the host-scheduler
resume marker: the port's existing `IHost.FrameYield`/`FrameClock` path supplies per-frame pacing, so it resume marker: the host supplies service-boundary suspension and retained presentation, so it
does not recursively execute the native render/poll/yield bytecode handlers. does not recursively execute the native render/poll/yield bytecode handlers.
**Acceptance gates:** a synthetic stale-terminal scene runs its setup body once and reaches content; a **Acceptance gates:** a synthetic stale-terminal scene runs its setup body once and reaches content; a
@@ -716,19 +724,18 @@ multiplication order, or 2D projection: normal playback still races past these s
reliable visual judgment. Treat that math as provisional until the pacing slice enables slow normal playback reliable visual judgment. Treat that math as provisional until the pacing slice enables slow normal playback
and a native-versus-port frame comparison. and a native-versus-port frame comparison.
### A2b — animation pacing + matrix validation ✅ (2026-07-10) ### A2b — animation pacing + matrix validation ✅ (2026-07-10; pacing model superseded)
The remaining race was a unit mismatch at the scheduler boundary. Native This slice initially interpreted roughly 1,788 `vm_operand_fetch` calls/s as evidence for a
`adv_interpreter_tick` advances one opcode, while the live cadence probe counted calls to refresh-independent 200 completed-opcode/s allowance. That scheduler interpretation is **superseded**:
`vm_operand_fetch` (about 1,788 operand reads/s). The port's `FrameYield` runs once per completed the probe counted operand reads and mixed burst execution with time parked in native services. The matching
opcode, so using 1,800 there overclocked script teardown by roughly ninefold. `FrameClock` now supplies a presentation trace below proves ordinary work is burst-fast; `--speed` now scales sleeps and retained
refresh-independent 200 completed-opcode/s allowance; sleep and input waits discard parked-time credit, and presentation clocks, not opcode throughput. Input remains ignored unless the VM is actually at
input is ignored unless the VM is actually at `wait-for-input`. `--speed` scales the unified VM, `wait-for-input`.
sleep, and animation clock for inspection without `--shot-sequence` auto-advance.
Live native capture recorded the complete `0xcbc0` scale ramp (1→5 over 1,890 ms), including the exact Live native capture recorded the complete `0xcbc0` scale ramp (1→5 over 1,890 ms), including the exact
composed matrices. The port previously deleted the object at scale 1.44 after 226 ms. At 215/s calibration it composed matrices. The 215/s and 200/s port replays were useful historical diagnostics showing that object
survived 1,798 ms to scale 4.72; the final 200/s replay retained it for 2,014 virtual ms, past the endpoint. lifetime had been coupled incorrectly to script progress, but those rates are not native scheduler constants.
Native matrix terms and the port's focused tests agree on row-vector Native matrix terms and the port's focused tests agree on row-vector
`anchor + (point-anchor)*scale + translation`; for base `(0,600)`, anchor `(400,1000)`, `anchor + (point-anchor)*scale + translation`; for base `(0,600)`, anchor `(400,1000)`,
scale 5, both project to `(-1600,-1000)`. Exact axis-aligned anchor/order/projection is validated; scale 5, both project to `(-1600,-1000)`. Exact axis-aligned anchor/order/projection is validated;
@@ -738,7 +745,7 @@ Verification: engine **92/92** after the opcode-clock reset test, corpus sweep u
**284 exit / 13 STEP-LIMIT**, Godot build and threaded `SELFTEST OK`. The transform capture tool and **284 exit / 13 STEP-LIMIT**, Godot build and threaded `SELFTEST OK`. The transform capture tool and
transform-aware `--gfx-log` are documented in `docs/tools-reference.md`. transform-aware `--gfx-log` are documented in `docs/tools-reference.md`.
### A2b — ADV transition/lifecycle diagnosis plan ⏳ OPEN (2026-07-10) ### A2b — ADV transition/lifecycle diagnosis plan ✅ COMPLETED BELOW (2026-07-10)
This is the next SC0000 correctness slice. It is driven by live A/B observations, not by static opcode This is the next SC0000 correctness slice. It is driven by live A/B observations, not by static opcode
coverage alone. The direct SC0000 histogram is currently **80/129 distinct ops handled (62.0%)** and coverage alone. The direct SC0000 histogram is currently **80/129 distinct ops handled (62.0%)** and
@@ -938,7 +945,7 @@ in the object timeline and are not being mistaken for foreground CG transition c
apart from the pre-existing nullable warning and threaded `SELFTEST OK`; all six Python test scripts, apart from the pre-existing nullable warning and threaded `SELFTEST OK`; all six Python test scripts,
opcode/ctx lint, 481-script decode validation, RECOVER, and Windows CR-aware `git diff --check` clean. opcode/ctx lint, 481-script decode validation, RECOVER, and Windows CR-aware `git diff --check` clean.
The plain check reports only the generated reference's CRLF on its newly added row. SC0000 coverage is The plain check reports only the generated reference's CRLF on its newly added row. SC0000 coverage is
**85/129 handled (65.9%)**, 44 GAP ops / 598 GAP instructions. No commit was made. **85/129 handled (65.9%)**, 44 GAP ops / 598 GAP instructions. This slice was later committed as `c9be9c5`.
### A2b — op 0x202 one-shot color/presentation ✅ DONE (2026-07-10) ### A2b — op 0x202 one-shot color/presentation ✅ DONE (2026-07-10)
@@ -960,7 +967,8 @@ fill at `(0,373)`; repeated show/hide passages now produce intermediate samples
`0.00, 0.26, 0.54, 0.82, 1.00` and the reverse), synchronized to the executing `0x202` sites around `0.00, 0.26, 0.54, 0.82, 1.00` and the reverse), synchronized to the executing `0x202` sites around
`0x12283..0x12342`. `0x12283..0x12342`.
The remaining full-white runs are not a stuck color clock or d2f0 endpoint: pixel windows place them after **Predecessor diagnosis, resolved by the next subsection.** The remaining full-white runs were not a stuck
color clock or d2f0 endpoint: pixel windows place them after
d2f0 is transparent, while object timelines show `AE*` handles first becoming visible in mode 0 with default d2f0 is transparent, while object timelines show `AE*` handles first becoming visible in mode 0 with default
`0xffffffff` before their later mode-1 initializer. Native call-site disassembly proves op `0x203`'s `0xffffffff` before their later mode-1 initializer. Native call-site disassembly proves op `0x203`'s
`obj+0x30` is passed directly as the D3D blend selector and mode 1 uses `SRCALPHA/INVSRCALPHA`; the port now `obj+0x30` is passed directly as the D3D blend selector and mode 1 uses `SRCALPHA/INVSRCALPHA`; the port now
@@ -1007,4 +1015,4 @@ separate slices.
threaded `SELFTEST OK`; all seven Python suites, opcode/ctx lint, 481-script decode, and RECOVER clean. threaded `SELFTEST OK`; all seven Python suites, opcode/ctx lint, 481-script decode, and RECOVER clean.
Normal-speed windowed capture wrote 220 PNGs and progressed through the complete AE sequence; at the target Normal-speed windowed capture wrote 220 PNGs and progressed through the complete AE sequence; at the target
frame, bind + mode-1 + `0x202` setup share one VM frame and the first published object state is mode 1. frame, bind + mode-1 + `0x202` setup share one VM frame and the first published object state is mode 1.
Ghidra `/v2` comments were updated and saved. No commit was made. Ghidra `/v2` comments were updated and saved. This slice was committed as `85fc07d`.

View File

@@ -133,9 +133,9 @@ texture ops (no GPU context) — run windowed for real scenes. User args (after
- `--seed 0xADDR=VAL` (repeatable) — seed initial global state, e.g. `--seed 0xa57=1` unlocks Lily's form-A voiced dialogue. - `--seed 0xADDR=VAL` (repeatable) — seed initial global state, e.g. `--seed 0xa57=1` unlocks Lily's form-A voiced dialogue.
- `--boot` — run SYSTEM4's state prefix (`INITCONFIG/INIT2/INIT`) via `GameSession` before the scene, so scene-assumed boot state (chiefly INIT2's gfx handle array) is present. **Needed for the gfx CGs to render** (without it the opening event CGs collapse/drift). e.g. `godot --path godot -- --boot`. - `--boot` — run SYSTEM4's state prefix (`INITCONFIG/INIT2/INIT`) via `GameSession` before the scene, so scene-assumed boot state (chiefly INIT2's gfx handle array) is present. **Needed for the gfx CGs to render** (without it the opening event CGs collapse/drift). e.g. `godot --path godot -- --boot`.
- `--shot <png> [--shot-page N]` — capture page N to a PNG then quit (dev screenshot). At scene end it also prints the call-scripts executed as nested frames. - `--shot <png> [--shot-page N]` — capture page N to a PNG then quit (dev screenshot). At scene end it also prints the call-scripts executed as nested frames.
- `--shot-sequence <dir> [--frames N]` — dump one PNG per rendered frame (`frame_0000.png…`, default N=180 ≈ 3s @60fps) then quit, auto-advancing past input waits. Verifies **time-based (sleep-paced) effects** — e.g. the opening `AE*` burst — as distinct frames, which a single `--shot` cannot. CPU/IO-heavy by design (a PNG every frame); a dev diagnostic, not a normal run. e.g. `godot --path godot -- --boot --shot-sequence out/seq --frames 300`. - `--shot-sequence <dir> [--frames N]` — dump one PNG per rendered frame (`frame_0000.png…`, default N=180 ≈ 3s @60fps) then quit, auto-advancing past input waits. Verifies time-based retained effects and publication boundaries as distinct frames, which a single `--shot` cannot. CPU/IO-heavy by design (a PNG every frame); a dev diagnostic, not a normal run. e.g. `godot --path godot -- --boot --shot-sequence out/seq --frames 300`.
- `--sleep-scale <f>` — multiply every `sleep` (op 0xc8) duration by `f` (default 1.0). The authentic opening burst is only ~2 s, too fast to eyeball live; `--sleep-scale 5` stretches it to ~10 s so the paced sequence (arcane `AE*` → character CGs → settled BG) is watchable. Debug-only; leave at 1.0 for real playback. - `--sleep-scale <f>` — multiply every explicit `sleep` (op 0xc8) duration by `f` (default 1.0). This stretches only script-authored sleep holds; it does not slow ordinary opcode bursts or replace `0x20c`/`0x21c` presentation pacing. Debug-only; leave at 1.0 for real playback.
- `--speed <f>` — scale the unified runtime clock (VM cadence, sleeps, and retained animation) without auto-advancing input waits. Values 0.058 are accepted; `--speed 0.25` is useful for transform inspection, while 1.0 is normal playback. - `--speed <f>` — scale sleeps and retained presentation clocks without throttling ordinary opcode bursts or auto-advancing input waits. Values 0.058 are accepted; `--speed 0.25` is useful for transform inspection, while 1.0 is normal playback.
- `--gfx-log <file>`**compositor + op diagnostic** (the tool that root-caused the grey background). Logs, per rendered frame, only the objects whose draw outcome **CHANGED** (drawn↔skip↔gone, resId, resolved file, `slot`, `src`/`dst`, `op`acity, `tintStr`ength) — quiet until something actually changes, so the exact frame a layer drops out (and why) stands out. Also traces every `set-texture`/`create-texture` **slot assignment** (via `GodotAdvHost.TraceOps`). Works live or with `--shot-sequence`. Use it before theorising about layering/blend/geometry: it showed the grey BG = the slot-selecting globals resolving to 0 → every texture collapsing into slot 0 (see engine-re.md §"Grey-background root cause"). e.g. `godot --path godot -- --boot --gfx-log out/gfx.log` then click to the bad page. - `--gfx-log <file>`**compositor + op diagnostic** (the tool that root-caused the grey background). Logs, per rendered frame, only the objects whose draw outcome **CHANGED** (drawn↔skip↔gone, resId, resolved file, `slot`, `src`/`dst`, `op`acity, `tintStr`ength) — quiet until something actually changes, so the exact frame a layer drops out (and why) stands out. Also traces every `set-texture`/`create-texture` **slot assignment** (via `GodotAdvHost.TraceOps`). Works live or with `--shot-sequence`. Use it before theorising about layering/blend/geometry: it showed the grey BG = the slot-selecting globals resolving to 0 → every texture collapsing into slot 0 (see engine-re.md §"Grey-background root cause"). e.g. `godot --path godot -- --boot --gfx-log out/gfx.log` then click to the bad page.
Matrix-channel outcomes also include `base`, `anchor`, projected `dst`, sampled `scale`/`trans`, and Matrix-channel outcomes also include `base`, `anchor`, projected `dst`, sampled `scale`/`trans`, and
one-shot-plus-cyclic `rot`ation angles. Active op-`0x202` outcomes include packed `color=current->target` one-shot-plus-cyclic `rot`ation angles. Active op-`0x202` outcomes include packed `color=current->target`

View File

@@ -1,7 +1,8 @@
namespace Age.Engine.Hosting; namespace Age.Engine.Hosting;
/// <summary>Host-owned virtual clock. Godot advances it from real elapsed time; VM pacing, sleeps, and /// <summary>Host-owned virtual clock. Godot advances it from real elapsed time; presentation-service waits,
/// retained graphics all consume this same timebase. Fractional milliseconds are retained so diagnostic /// sleeps, and retained graphics consume this timebase. Ordinary opcode bursts are not clock-throttled.
/// Fractional milliseconds are retained so diagnostic
/// slow motion does not stall on high-refresh displays.</summary> /// slow motion does not stall on high-refresh displays.</summary>
public sealed class FrameClock public sealed class FrameClock
{ {
@@ -11,14 +12,12 @@ public sealed class FrameClock
/// <summary>Monotonic virtual time in milliseconds (scaled by Speed).</summary> /// <summary>Monotonic virtual time in milliseconds (scaled by Speed).</summary>
public long NowMs => System.Threading.Interlocked.Read(ref _nowMs); public long NowMs => System.Threading.Interlocked.Read(ref _nowMs);
/// <summary>Speed multiplier. 1.0 = normal. A lower diagnostic value slows VM progress, sleeps, and /// <summary>Speed multiplier. 1.0 = normal. A lower diagnostic value slows sleeps and retained
/// graphics together; a future ADV-scoped Ctrl hook can drive the same seam.</summary> /// presentation clocks; ordinary opcode bursts still run to the next service boundary.</summary>
public double Speed = 1.0; public double Speed = 1.0;
/// <summary>Native normal-playback interpreter cadence. The old 1,800 figure counted calls to /// <summary>Legacy calibration retained for the isolated WallClockOpPacer tests. Production Godot no
/// vm_operand_fetch, not completed opcodes. A live 1,890 ms transform section executes about 407 /// longer uses an opcode-rate limiter; native presentation tracing disproved this as scheduler state.</summary>
/// port opcodes. A normal-speed replay at 215/s retained the object for 1,798 ms; 200/s reaches
/// the native 1,890 ms endpoint before the same teardown path.</summary>
public double OpsPerSecond = 200.0; public double OpsPerSecond = 200.0;
/// <summary>Advance the clock by one rendered frame's real delta (seconds), scaled by Speed.</summary> /// <summary>Advance the clock by one rendered frame's real delta (seconds), scaled by Speed.</summary>
@@ -31,9 +30,8 @@ public sealed class FrameClock
} }
} }
/// <summary>Pure wall-clock opcode rate limiter. The VM thread records completed opcodes and waits whenever /// <summary>Legacy isolated rate-limiter utility. It is not used by production hosts; ordinary native opcode
/// it has consumed the allowance earned from <see cref=FrameClock.NowMs/>. Reset after a blocking wait so /// execution is burst-fast between explicit presentation/sleep/input service boundaries.</summary>
/// parked time never turns into a catch-up burst.</summary>
public sealed class WallClockOpPacer public sealed class WallClockOpPacer
{ {
private readonly FrameClock _clock; private readonly FrameClock _clock;

View File

@@ -126,21 +126,20 @@ public sealed class GodotAdvHost : IHost
_frameSignal.Set(); _frameSignal.Set();
} }
// Main thread, once per rendered frame: releases a VM thread parked in FrameYield/Sleep. // Main thread, once per rendered frame: releases a VM thread parked in Sleep or a presentation/input wait.
public void PulseFrame() => _frameSignal.Set(); public void PulseFrame() => _frameSignal.Set();
// Native presentation trace: ordinary opcode bursts run to the next service boundary in a few // Native presentation trace: ordinary opcode bursts run to the next service boundary in a few
// milliseconds and are not frame-paced. Pacing belongs to 0x21c, sleep, and input waits below. // milliseconds and are not frame-paced. Pacing belongs to 0x21c, sleep, and input waits below.
public void FrameYield() { } public void FrameYield() { }
// op 0xc8: block the VM background thread so the main-thread compositor (Main.Recomposite in _Process) // op 0xc8: block the VM background thread while the main-thread compositor keeps presenting retained state.
// presents the current retained GfxState — this is what makes the sleep-paced opening burst animate.
// Time-based sibling of WaitForInput's suspend. The native op arms a non-blocking main-loop-polled timer; // Time-based sibling of WaitForInput's suspend. The native op arms a non-blocking main-loop-polled timer;
// blocking this throwaway task thread is behaviorally equivalent given our threading model. Operand is // blocking this throwaway task thread is behaviorally equivalent given our threading model. Operand is
// MILLISECONDS (docs/engine-re.md sleep section + opcodes.toml 0xc8). Headless CLI hosts no-op it (parity). // MILLISECONDS (docs/engine-re.md sleep section + opcodes.toml 0xc8). Headless CLI hosts no-op it (parity).
public double SleepScale = 1.0; // --sleep-scale <f>: debug multiplier to slow/speed the paced opening for inspection public double SleepScale = 1.0; // --sleep-scale <f>: debug multiplier for explicit op-0xc8 holds only
// Wait on the unified FrameClock timebase (not Thread.Sleep) so a future Speed multiplier scales // Wait on the unified FrameClock timebase (not Thread.Sleep) so the Speed multiplier scales
// sleeps together with the throttle and the tween. Main._Process advances the clock + pulses each frame. // sleeps together with retained presentation clocks. Main._Process advances the clock + pulses each frame.
public void Sleep(long duration) public void Sleep(long duration)
{ {
long ms = (long)System.Math.Clamp(duration * SleepScale, 0, 60_000); // cap so a pathological script can't hang the window long ms = (long)System.Math.Clamp(duration * SleepScale, 0, 60_000); // cap so a pathological script can't hang the window

View File

@@ -96,8 +96,8 @@ public partial class Main : Godot.Control
bool boot = System.Array.IndexOf(userArgs, "--boot") >= 0; // run SYSTEM4's state prefix first bool boot = System.Array.IndexOf(userArgs, "--boot") >= 0; // run SYSTEM4's state prefix first
string scene = "SC0000"; // --scene <NAME>: which scene to play (default SC0000) string scene = "SC0000"; // --scene <NAME>: which scene to play (default SC0000)
var seeds = new List<(int Addr, long Val)>(); // --seed 0xADDR=VAL (repeatable) — initial global state var seeds = new List<(int Addr, long Val)>(); // --seed 0xADDR=VAL (repeatable) — initial global state
double sleepScale = 1.0; // --sleep-scale <f>: slow/speed the paced opening for inspection double sleepScale = 1.0; // --sleep-scale <f>: scale explicit op-0xc8 holds
double speed = 1.0; // --speed <f>: whole-runtime diagnostic speed double speed = 1.0; // --speed <f>: sleeps + retained presentation clocks
long transitionClickMs = -1; // --transition-click-ms <n>: force active transitions after n virtual ms long transitionClickMs = -1; // --transition-click-ms <n>: force active transitions after n virtual ms
string? histFile = null; // --trace-histogram <file>: op/call-site execution counts of the REAL run string? histFile = null; // --trace-histogram <file>: op/call-site execution counts of the REAL run
for (int i = 0; i < userArgs.Length; i++) for (int i = 0; i < userArgs.Length; i++)
@@ -268,7 +268,7 @@ public partial class Main : Godot.Control
System.Collections.Generic.Dictionary<long, string>? decisions = _gfxLogPath != null || _timeline != null ? new() : null; System.Collections.Generic.Dictionary<long, string>? decisions = _gfxLogPath != null || _timeline != null ? new() : null;
int z = 0; int z = 0;
var visible = _vm.Gfx.SnapshotVisibleObjects(_clock.NowMs); // one synchronized sample for objects + ranges var visible = _vm.Gfx.SnapshotVisibleObjects(_clock.NowMs); // one synchronized sample for objects + ranges
foreach (var v in visible) // interpolate at the throttled clock foreach (var v in visible) // interpolate at the retained-presentation clock
{ {
var t = v.Transform; var t = v.Transform;
var affine = Age.Engine.Model.Transform2DMath.Build(t, v.Rotation); var affine = Age.Engine.Model.Transform2DMath.Build(t, v.Rotation);

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Live frame-cadence probe (docs/engine-re.md "Frame cadence"). Pins the native cadence — execution """Live frame-activity probe (docs/engine-re.md "Frame cadence"). Compares operand-fetch activity,
rate vs displayed-frame rate, and frame timing — so the frame-stepped-VM fix picks its mechanism from message-pump timing, and Ctrl state. It does not measure completed opcodes or establish an opcode budget;
data instead of by feel. the later matching presentation trace proved ordinary execution is burst-fast between service boundaries.
SAFE pattern (matches capture_gfx_objects.py, which runs without crashing): plain-JS hooks only, no SAFE pattern (matches capture_gfx_objects.py, which runs without crashing): plain-JS hooks only, no
CModule; the only engine-code hook is the PROVEN operand-fetch helper `0x41b940` (fires once per CModule; the only engine-code hook is the PROVEN operand-fetch helper `0x41b940` (fires once per
@@ -148,7 +148,7 @@ def report(samples):
rows.append(r) rows.append(r)
tot_ops = samples[-1]["ops"] - samples[0]["ops"] tot_ops = samples[-1]["ops"] - samples[0]["ops"]
print(f"\n=== frame-cadence report ({len(rows)} intervals, {tot_ops} operand-fetches) ===") print(f"\n=== frame-cadence report ({len(rows)} intervals, {tot_ops} operand-fetches) ===")
# candidate frame signals: prefer a timing source that sits ~30-120/sec (loop reads clock once/frame) # Candidate loop/presentation signals only; these do not imply a completed-opcode cadence.
for label, want in (("NORMAL", False), ("FAST-FWD (Ctrl / skip-bit)", True)): for label, want in (("NORMAL", False), ("FAST-FWD (Ctrl / skip-bit)", True)):
b = [r for r in rows if r["ff"] == want] b = [r for r in rows if r["ff"] == want]
# for cadence, use only ACTIVE intervals (ops>0) so parked time doesn't dilute the numbers # for cadence, use only ACTIVE intervals (ops>0) so parked time doesn't dilute the numbers
@@ -170,13 +170,14 @@ def report(samples):
", ".join(f"{nm}={v:.1f}" for nm, v in cand.items())) ", ".join(f"{nm}={v:.1f}" for nm, v in cand.items()))
if frame_name: if frame_name:
print(f" -> frame signal ~ {frame_name} @ {frame_rate:.1f}/sec ; " print(f" -> frame signal ~ {frame_name} @ {frame_rate:.1f}/sec ; "
f"exec/frame = {ops_active/frame_rate:.1f} (peak {peak/frame_rate:.1f})") f"operand-fetches/signal = {ops_active/frame_rate:.1f} (peak {peak/frame_rate:.1f})")
else: else:
print(" -> no timing source in 20-200/sec band; frame rate still unresolved.") print(" -> no timing-source candidate in the sampled band; use probe_present.py for presentation.")
flg = [f"0x{r['flags']:x}" for r in b if isinstance(r["flags"], int)] flg = [f"0x{r['flags']:x}" for r in b if isinstance(r["flags"], int)]
print(f" flags seen: {sorted(set(flg))}") print(f" flags seen: {sorted(set(flg))}")
print("\n Stable exec/frame => fixed op-budget cadence; peak >> active-avg with parking => run-until-yield.") print("\n Counts are operand fetches, not completed opcodes or a scheduler budget.")
print(" FAST-FWD vs NORMAL exec/frame shows how the ADV governor scales the slice.") print(" Native presentation tracing later proved burst-fast execution between 0x20c/0x21c/sleep/input boundaries.")
print(" FAST-FWD vs NORMAL still measures how Ctrl changes operand-fetch activity.")
def main(): def main():

View File

@@ -2037,8 +2037,8 @@ abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "sleep" name = "sleep"
category = "control" category = "control"
summary = "Pause the script for <duration> milliseconds while rendering continues (frame pacing)." summary = "Pause the current script for <duration> milliseconds while retained presentation continues."
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 <duration> ms while the per-frame compositor keeps presenting -> correctly reproduces the explicit one-shot dramatic pauses. NOTE: does NOT pace the rapid opening AE* burst (those draws have no sleep between them; their real pacer is unknown). Headless hosts no-op it (parity)." 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 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)."
noop_headless = false noop_headless = false
source = "investigation" source = "investigation"
confidence = "high" confidence = "high"