Implement ADV foreground transition lifecycle
This commit is contained in:
@@ -36,6 +36,7 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch-
|
||||
| `0x6db28` | `coroutine_yield_b` | `void*` | op 0x7b yield-state save (op2 -> +ctxidx*4) |
|
||||
| `0x6dbc8` | `coroutine_runstate` | `int` | op 0x7c resume gate (run-state bit 0x2000000) |
|
||||
| `0x6dbcc` | `coroutine_resume_off` | `int` | op 0x7c resume PC offset |
|
||||
| `0x6dbd4` | `adv_read_skip_state` | `int` | current ADV read/click-skip service state; queried by op 0x1cc, maintained from ReadTextSkip + per-PC read history |
|
||||
| `0x9b24c` | `dispatch_table` | `void*` | opcode->handler table base [0x400]; handler(op) = *(0x9b24c + op*4) |
|
||||
| `0xa0cc0` | `screen_w` | `int` | screen width (640) |
|
||||
| `0xa0cc4` | `screen_h` | `int` | screen height (480) |
|
||||
|
||||
@@ -515,9 +515,10 @@ Op `0x234` is the independent rotation cycle above. Op `0x238` still configures
|
||||
##### `label_1235a` animation-section boundary (2026-07-10)
|
||||
|
||||
The section helper computes the maximum configured duration and arms it with `0x238`, then reads
|
||||
message-skip through `0x1c7` and ADV service state through `0x1cc`. Normal playback (both zero)
|
||||
executes `0x243` (reset the separate global animation-service clock) followed by `0x20c` present;
|
||||
the skip/service branch executes `0x21c` (set run-state bit `0x400`). Both branches finish with
|
||||
message-skip through `0x1c7` and ADV read/click-skip state through `0x1cc`. The raw `jcc` order matters:
|
||||
normal playback (both zero) branches to `0x21c` (set run-state bit `0x400`), while a nonzero skip/read
|
||||
state executes `0x243` (reset the separate global animation-service clock) followed by `0x20c` present.
|
||||
Both branches finish with
|
||||
`0x224`, which clears the native gfx command queue at `ctx+0x418`. These handlers are now named,
|
||||
commented, and saved in the Ghidra image.
|
||||
|
||||
@@ -805,8 +806,33 @@ into the target and ramps alpha 0→1. Its SC0000 site `0x129e7` passes `(handle
|
||||
handle+1,1,handle,1,G[0x6249f],G[0x624a0])`. It remains a render-target/transition slice dependency rather
|
||||
than being approximated in the affine object compositor.
|
||||
|
||||
#### ADV foreground surface-transition lifecycle (2026-07-10)
|
||||
|
||||
The completed native chain is `0x21d` snapshot -> `0x223` queue -> `label_1235a` skip queries ->
|
||||
`0x21c` normal yield or `0x20c` skip endpoint-present:
|
||||
|
||||
- `gfx_object_clone` (`0x47e4f0`, op `0x21d` handler `0x423310`) copies the complete retained-object
|
||||
record: exactly `0xb5` dwords / `0x2d4` bytes. SC0000 `0x128fc` clones the current CG handle to
|
||||
`handle+1`; the loader then rebinds the source handle to the new CG, and `0x223 @ 0x129e7` uses
|
||||
`handle+1` as old range A, the updated handle as new range B, and `handle+2` as the target presenter.
|
||||
- `0x1c7` is the `run_state_flags & 0x08000000` message-skip query. `0x1cc` reads
|
||||
`ctx+0x6dbd4`, now named `adv_read_skip_state`; `adv_refresh_read_skip_state` (`0x406cd0`) and the
|
||||
text/label/wait handlers maintain it from `message_ReadTextSkip` plus per-PC read history. It is not
|
||||
surface-transition progress.
|
||||
- Zero OR-state is normal playback and reaches `0x21c`; run-state bit `0x400` parks interpreter
|
||||
progression while the type-0 surface command advances from the frame clock. Nonzero skip/read state
|
||||
reaches `0x243 + 0x20c`, exposing the completed endpoint without the normal wait.
|
||||
- Op `0x203` stores operand 2 at object `+0x30`. Transition sources use mode 2; their common
|
||||
`0xffffffff` color is opaque identity modulation, not a solid-white tint. Negative alpha/RGB operands
|
||||
separately preserve the corresponding byte(s) of current static color via the `FUN_0047f3e0` lookup.
|
||||
|
||||
The port mirrors this with an explicit pending/start/progress/natural-or-forced-complete state. A click
|
||||
while `0x21c` is parked completes only the active foreground surface transition and is consumed; it does
|
||||
not pre-arm the following `wait-for-input`, and it does not complete independent retained rotation,
|
||||
matrix, spritesheet, or color-animation channels.
|
||||
|
||||
**Still deferred:** `0x236` (`gfx_op_0x236` @`0x423ee0`) a
|
||||
**timed/animated-surface (movie-like) op**; plus the unclassified `0x21c/0x21d/0x224/0x242/0x23d/0x20a/0x20e/0x243`
|
||||
**timed/animated-surface (movie-like) op**; plus the unclassified `0x242/0x23d/0x20a/0x20e`
|
||||
tail (2-arg flags / inline). These stay GAP until a follow-up slice or are safe-noop'd if the opening tolerates it.
|
||||
|
||||
### Grey-background root cause — slot collision + tint-strength (2026-07-08, gfx-log)
|
||||
|
||||
@@ -79,15 +79,20 @@ Native handler sleep_op_0xc8 @0x420ec0 is NON-BLOCKING: it arms a timer (sleep_t
|
||||
- **grounding:** source=investigation, confidence=med
|
||||
- **evidence:** Ghidra: handler 0x4299c0 (dispatch ctx[0x9b74c]=0x4299c0; created+typed EngineCtx*+annotated; Kelebek u0041F9C0 = VA-drift). Writes gfx cmd-type 9; op2→local_204, op3→local_104, op4→local_208; (*DAT_005c6018)(8, ctx[0x54fe8], &local_210) → FUN_00425fb0(1,ret). DAT_005c6018: 6 xrefs all READ, no static writer; FUN_00405740 (screen-fade) calls it w/ cmd 3, branches on ret 1/2 = transition progress = native video service.
|
||||
|
||||
### 0x1cc `get-adv-service-state` (get-adv-service-state, argc 1)
|
||||
- **summary:** (out) - copy native ADV service state ctx+0x6dbd4; label_1235a ORs it with message-skip to select its yield branch.
|
||||
### 0x1cc `get-adv-read-skip-state` (get-adv-read-skip-state, argc 1)
|
||||
- **summary:** (out) - copy the current ADV read/click-skip service state from ctx+0x6dbd4. label_1235a ORs it with 0x1c7's Ctrl/message-skip bit: zero takes 0x21c's normal transition/yield path; nonzero resets the animation service and presents the completed endpoint through 0x20c.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra handler 0x427330 calls vm_operand_write(1, ctx+0x6dbd4). Exact service-state producer remains outside this opcode.
|
||||
- **depended on by:** 0x20c, 0x20d, 0x21c, 0x223
|
||||
- **evidence:** Ghidra handler 0x427330 calls vm_operand_write(1, ctx+0x6dbd4). Producer recovered 2026-07-10: adv_update_read_text_skip_state@0x406cd0 and op 0x6e/0x71/0x72 maintain the field from message_ReadTextSkip plus current-PC read-history lookup; adv_interpreter_tick consumes it in click/read-skip control. It is not the 0x223 surface-transition progress flag.
|
||||
|
||||
### 0x21c `mark-frame-yield` (mark-frame-yield, argc 0)
|
||||
- **summary:** Set native run-state bit 0x400. Host-implicit: the port already offers a scheduler yield after every completed opcode.
|
||||
- **grounding:** source=investigation, confidence=high, noop_headless=True
|
||||
- **evidence:** Ghidra handler 0x417520 sets cmd-type 1 and ORs ctx+0xa0ce4 with 0x400. SC0000 label_1235a reaches it only when op 0x1c7 or 0x1cc is nonzero.
|
||||
- **summary:** Set native run-state bit 0x400; in normal ADV playback this is the queued foreground-transition yield/resume boundary.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **depends on:** 0x223, 0x1c7, 0x1cc
|
||||
- **depended on by:** 0x223
|
||||
- **evidence:** Ghidra handler 0x417520 sets cmd-type 1 and ORs ctx+0xa0ce4 with 0x400. SC0000 label_1235a's jcc reaches it when the OR of op 0x1c7/0x1cc is zero; synchronized port trace confirmed this is the normal path after 0x223.
|
||||
|
||||
SC0000 label_1235a reaches this when the OR of 0x1c7 message-skip and 0x1cc read-skip state is zero (normal playback). Native run-state bit 0x400 yields the interpreter while the queued foreground presentation advances. The interactive port starts pending 0x223 commands here, parks only the VM thread while per-frame compositing continues, and resumes after natural or click-forced completion.
|
||||
|
||||
## draw
|
||||
|
||||
@@ -132,9 +137,9 @@ Native handler sleep_op_0xc8 @0x420ec0 is NON-BLOCKING: it arms a timer (sleep_t
|
||||
- **evidence:** Ghidra handler 0x4228d0 (dispatch ctx[0x26c93+0x202]); packs (alpha<<24|rgb) from operands 4/5, FUN_0047ea00(op1,op2,op3,packed).
|
||||
|
||||
### 0x203 `gfx-draw-color` (gfx-draw-color, argc 4)
|
||||
- **summary:** 0x203 (handle)(v)(alpha)(color) — gfx cmd-type 9. Handler gfx_op_0x203_draw_color @0x4229a0: worker gfx_op_0x203_worker_set_color @0x47e9b0 sets a STATIC color/alpha (obj+0x60), no anim bit; packs ARGB from alpha(op3)/color(op4). Sibling of 0x202 (one fewer positional arg). C# VM (2026-07-08 blend slice): routes through GfxState.SetObjectColor → compositor applies static alpha/tint (BlendKind.Alpha). See docs/engine-re.md §Blend & transparency.
|
||||
- **summary:** 0x203 (handle)(mode)(alpha)(color) — gfx cmd-type 9. Worker stores mode at obj+0x30 and STATIC packed color at obj+0x60. Negative alpha/RGB preserve current static bytes through FUN_0047f3e0. Mode 2 is the 0x223 transition-source alpha/identity path (0xffffffff = opaque identity, not solid white); mode 0 retains the established tint-strength behavior.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra handler 0x4229a0 (dispatch ctx[0x26c93+0x203]); packs color from operands 3/4, FUN_0047e9b0(op1,op2,packed).
|
||||
- **evidence:** Ghidra handler 0x4229a0 (dispatch ctx[0x26c93+0x203]); negative operands read current obj+0x60 via FUN_0047f3e0, then gfx_op_0x203_worker_set_color stores op2 at obj+0x30 and packed ARGB at obj+0x60. SC0000 transition setup uses mode 2 with -1/-1 before 0x21d/0x223; synchronized window pixels prove treating 0xffffffff as generic white tint is wrong.
|
||||
|
||||
### 0x208 `get-texture-size` (get-texture-size, argc 3)
|
||||
- **summary:** 0x208 (slot)(out_w)(out_h) — writes the loaded texture's width/height into two output globals; keystone for bytecode-computed sprite/bg geometry (SC0000 label_12649)
|
||||
@@ -142,11 +147,13 @@ Native handler sleep_op_0xc8 @0x420ec0 is NON-BLOCKING: it arms a timer (sleep_t
|
||||
- **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
|
||||
- **summary:** Present the composited frame; label_1235a uses this on the read/message-skip branch to expose the completed foreground endpoint immediately.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **depends on:** 0x223, 0x1c7, 0x1cc
|
||||
- **depended on by:** 0x20d, 0x223
|
||||
- **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.
|
||||
Native handler gfx_op_0x20c_present_frame (dispatch ctx[0x26c93+0x20c]) -> gfx_render_frame @0x4820b0. Godot composites continuously. In label_1235a the OR of 0x1c7/0x1cc is nonzero on the read/message-skip branch, which resets the animation service then presents; the port starts and snaps any pending 0x223 transition to its endpoint here. Normal zero-state playback branches to 0x21c, which owns wait/resume. Headless hosts remain non-blocking. Kelebek label u00416200 was VA-drift.
|
||||
|
||||
### 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.
|
||||
@@ -188,6 +195,12 @@ Native handler gfx_op_0x20c_present_frame (dispatch ctx[0x26c93+0x20c]) -> gfx_r
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra handler 0x42a1b0; FUN_0047f2e0(obj op1) + 3x→FUN_00425fb0(2/3/4). label_12649 site 0x00c86 handle=G[0x62457] → G[0x62498/9/a].
|
||||
|
||||
### 0x21d `clone-gfx-object` (clone-gfx-object, argc 2)
|
||||
- **summary:** (source_handle)(destination_handle) - clone the complete retained gfx object record (0xb5 dwords / 0x2d4 bytes). SC0000 uses destination=source+1 as range A's old-frame snapshot immediately before 0x223 crossfades range B's updated source into target handle source+2.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **depends on:** 0x223
|
||||
- **evidence:** Ghidra handler 0x423310 -> gfx_object_clone@0x47e4f0. Worker requires source in ctx+0x408 map, creates destination, copies exactly 0xb5 dwords from source object record, then marks ctx+0xb558 dirty. SC0000 site 0x128fc passes current handle and handle+1 before queueing 0x223 at 0x129e7.
|
||||
|
||||
### 0x21e `set-anim-transform-norm` (set-anim-transform-norm, argc 6)
|
||||
- **summary:** (handle)(delay_ms)(duration_ms)(sx)(sy)(sz) — set the normalized SCALE-matrix channel (100=identity). Target obj+0xac is linearly sampled from current obj+0x6c by gfx_object_apply_transform_channels@0x472f00 on frame-time ctx+0xb550, after delay and for duration, then committed. Shares only start timestamp obj+0x34 with op 0x220; neither Z is opacity.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
@@ -206,6 +219,8 @@ Native handler gfx_op_0x20c_present_frame (dispatch ctx[0x26c93+0x20c]) -> gfx_r
|
||||
### 0x223 `queue-surface-alpha-transition` (queue-surface-alpha-transition, argc 8)
|
||||
- **summary:** (command_key)(target_slot)(range_a_start)(range_a_count)(range_b_start)(range_b_count)(delay_ms)(duration_ms) — queue a type-0 timed alpha transition command in the separate ctx+0x414 command map. This is render-target/surface presentation state, not an object affine matrix. The render frame composites the two handle ranges into target_slot and ramps alpha 0->1 after delay over duration.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **depends on:** 0x20c, 0x21c, 0x1c7, 0x1cc
|
||||
- **depended on by:** 0x20c, 0x21c, 0x21d
|
||||
- **evidence:** Ghidra handler 0x423620 -> gfx_queue_surface_alpha_transition@0x47f440. Record fields: type +0=0, start +4=0, delay +8=arg7, duration +0xc=arg8, slot +0x10=arg2, range A +0x14/+0x1c=args3/4, range B +0x18/+0x20=args5/6. gfx_render_frame@0x47fbc0 initializes start from ctx+0xb550 and consumes type 0 as an alpha ramp. SC0000 executes one shared-helper site at 0x129e7.
|
||||
|
||||
### 0x224 `clear-gfx-command-queue` (clear-gfx-command-queue, argc 0)
|
||||
@@ -286,6 +301,7 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
|
||||
### 0x1c7 `get-message-skip` (get-message-skip, argc 1)
|
||||
- **summary:** (out) - write 1 iff ADV message-skip run-state bit 0x08000000 is set, otherwise 0.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **depended on by:** 0x20c, 0x20d, 0x21c, 0x223
|
||||
- **evidence:** Ghidra handler 0x4272b0 reads ctx+0xa0ce4 bit 0x08000000 and vm_operand_write(1, 1|0). SC0000 label_1235a ORs it with op 0x1cc.
|
||||
|
||||
## marker
|
||||
@@ -988,6 +1004,7 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
|
||||
### 0x20d `u00420E10` (u00420E10, argc 1)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
- **depends on:** 0x20c, 0x1c7, 0x1cc
|
||||
|
||||
### 0x20e `u00416250` (u00416250, argc 0)
|
||||
- **summary:** —
|
||||
@@ -997,10 +1014,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
|
||||
|
||||
### 0x21d `u00421410` (u00421410, argc 2)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0x222 `u004216C0` (u004216C0, argc 2)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
@@ -895,3 +895,47 @@ and composite call order; named useful parameters; saved `/v2`.
|
||||
apart from the pre-existing nullable warning and threaded `SELFTEST OK`; opcode tooling and focused Python
|
||||
tests clean; transform tool compiles; SC0000 coverage **81/129 handled (62.8%)**, 48 GAP ops / 602 GAP
|
||||
instructions; windowed affine capture clean. Final whitespace/diff validation is recorded with the handoff.
|
||||
|
||||
### A2b — ADV foreground transition/click lifecycle ✅ DONE (2026-07-10)
|
||||
|
||||
**Native recovery and control-flow correction.** `0x1c7` is the message-skip run-state query.
|
||||
`0x1cc` reads `ctx+0x6dbd4`, now `adv_read_skip_state`; `adv_refresh_read_skip_state@0x406cd0` plus the
|
||||
text/label/wait handlers maintain it from `message_ReadTextSkip` and the current-PC read-history lookup.
|
||||
It is not surface progress. The earlier reading of `label_1235a`'s branch was reversed: zero OR-state
|
||||
(normal playback) reaches `0x21c` and sets scheduler bit `0x400`; nonzero skip/read state reaches
|
||||
`0x243 + 0x20c` to present the endpoint. Synchronized execution caught and corrected this before validation.
|
||||
|
||||
The executed missing producer for `0x223` range A was `0x21d`: `gfx_object_clone@0x47e4f0` copies the full
|
||||
`0x2d4`-byte retained-object record from the current CG handle to `handle+1`. SC0000 then rebinds the source
|
||||
handle to the new CG and queues `0x223` with old clone range A, new source range B, and target presenter
|
||||
`handle+2`. Op `0x203` mode 2 is the companion transition-source alpha/identity path; its `0xffffffff`
|
||||
is opaque identity, not generic solid-white tint, and negative alpha/RGB preserve current static bytes.
|
||||
|
||||
**Port lifecycle.** `GfxState` owns explicit queue/start/wall-clock progress/natural-or-forced completion.
|
||||
Normal `0x21c` parks only the VM thread until the foreground command completes while Godot continues
|
||||
per-frame compositing. A click during that wait forces only the foreground transition to progress 1, is
|
||||
consumed, and resumes the VM; stable `wait-for-input` remains a separate gate. Tests verify that forcing a
|
||||
transition leaves an independent cyclic retained rotation active. The `0x20c` skip/read path starts and
|
||||
snaps a pending foreground transition to its endpoint without the normal wait.
|
||||
|
||||
**Synchronized window evidence.** At normal speed, the second-to-third CG transition produced seven
|
||||
successive distinct PNG hashes while progress advanced `0.133, 0.297, 0.442, 0.592, 0.742, 0.891, 1.000`;
|
||||
the user independently observed that fade working in the live window. A timed click landed at progress
|
||||
`0.157`, the next frame was the endpoint, and the stable page count did not advance. The accelerated
|
||||
windowed run reached `BGM008 @ 0x1728` on frame 137 with non-white changing pixels (2,469 sampled colors at
|
||||
the sentinel) and continued; its earlier white interval was finite and released before that boundary.
|
||||
This is windowed pixel/object/timeline evidence, not the earlier headless reachability inference.
|
||||
|
||||
The retained dialogue-text surface `0xe678` still has no pixels because native `draw-string 0x204` remains
|
||||
a separate GAP; the Godot `Label` shortcut remains outside this slice. Its create/erase events are explicit
|
||||
in the object timeline and are not being mistaken for foreground CG transition correctness.
|
||||
|
||||
**Ghidra.** Renamed/commented `adv_refresh_read_skip_state` (`0x406cd0`),
|
||||
`op_0x1cc_get_adv_read_skip_state` (`0x427330`), `op_0x21d_clone_gfx_object` (`0x423310`), and
|
||||
`gfx_object_clone` (`0x47e4f0`); added `EngineCtx.adv_read_skip_state`; saved `/v2`.
|
||||
|
||||
**Validation:** engine **102/102**; full sweep unchanged at **284 exit / 13 STEP-LIMIT**; Godot build clean
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -139,6 +139,7 @@ texture ops (no GPU context) — run windowed for real scenes. User args (after
|
||||
- `--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
|
||||
one-shot-plus-cyclic `rot`ation angles. Parent directories are created automatically.
|
||||
- `--transition-click-ms <n>` — diagnostic-only input injector: after a foreground transition has been active for `n` virtual milliseconds, send one click through the real input lifecycle. The click completes/consumes the transition and does not advance a stable page. Use with `--timeline-log`, `--gfx-log`, and windowed `--shot-sequence`; omit for normal play.
|
||||
- `--timeline-log <jsonl>` — diagnostic-only synchronized event stream for a real Godot run. Records every
|
||||
executed script byte offset/opcode, virtual time/frame, VM state changes (`running`, `sleep`, `input-wait`,
|
||||
`halted`), BGM events, and changed visible-object compositor outcomes in one ordered JSONL file. Combine with
|
||||
|
||||
77
engine/Age.Engine.Tests/ForegroundTransitionTests.cs
Normal file
77
engine/Age.Engine.Tests/ForegroundTransitionTests.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Age.Engine.Model;
|
||||
using Age.Engine.Sys4;
|
||||
using Age.Engine.Vm;
|
||||
using Xunit;
|
||||
|
||||
public class ForegroundTransitionTests
|
||||
{
|
||||
private static OpcodeTable T() => OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||
private static Operand G(int address) => new(3, address);
|
||||
private static Operand I(long value) => new(0, value);
|
||||
|
||||
[Fact]
|
||||
public void SurfaceTransition_HasStartDelayProgressNaturalAndForcedCompletion()
|
||||
{
|
||||
var gfx = new GfxState();
|
||||
gfx.SetSurface(4, 0x25, -1);
|
||||
gfx.BindDraw(100, 4, 0, 0, 800, 600, 0, 0);
|
||||
gfx.SetRotationCycle(100, 1000, (0, 0, 1)); // ambient, deliberately independent
|
||||
gfx.QueueSurfaceAlphaTransition(102, 6, 101, 1, 100, 1, 100, 200);
|
||||
|
||||
Assert.Equal(1, gfx.StartForegroundTransitions(1000));
|
||||
Assert.Equal(0.0, gfx.SnapshotForegroundTransitions(1050).Single().Progress);
|
||||
Assert.Equal(0.25, gfx.SnapshotForegroundTransitions(1150).Single().Progress, 3);
|
||||
Assert.False(gfx.HasActiveForegroundTransitions(1300));
|
||||
Assert.True(gfx.TryGet(100)!.RotationEnabled);
|
||||
|
||||
gfx.QueueSurfaceAlphaTransition(202, 7, 201, 1, 200, 1, 0, 500);
|
||||
gfx.StartForegroundTransitions(2000);
|
||||
Assert.True(gfx.HasActiveForegroundTransitions(2100));
|
||||
Assert.Equal(1, gfx.CompleteForegroundTransitions(2100));
|
||||
var forced = gfx.SnapshotForegroundTransitions(2100).Single(t => t.TargetSlot == 7);
|
||||
Assert.True(forced.Forced);
|
||||
Assert.Equal(1.0, forced.Progress);
|
||||
Assert.True(gfx.TryGet(100)!.RotationEnabled); // transition click did not finish ambient animation
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CloneObject_SnapshotsOldSurfaceBindingForTransitionRangeA()
|
||||
{
|
||||
var gfx = new GfxState();
|
||||
gfx.SetSurface(4, 0x25, -1);
|
||||
gfx.BindDraw(100, 4, 0, 0, 800, 600, 0, 0);
|
||||
gfx.SetStaticObjectColorResolved(100, 2, -1, -1);
|
||||
Assert.True(gfx.CloneObject(100, 101));
|
||||
gfx.SetSurface(5, 0x27, -1);
|
||||
gfx.BindDraw(100, 5, 0, 0, 800, 600, 0, 0);
|
||||
|
||||
var snapshot = gfx.SnapshotVisibleObjects();
|
||||
Assert.Equal(0x27, snapshot.Single(x => x.Handle == 100).SurfaceResId);
|
||||
Assert.Equal(0x25, snapshot.Single(x => x.Handle == 101).SurfaceResId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VmDispatchesSkipQueriesQueueAndPresentResumeBoundary()
|
||||
{
|
||||
var table = T();
|
||||
var scene = ScriptAssembler.Assemble(table, "FOREGROUND", new List<(int, Operand[])>
|
||||
{
|
||||
(0x1c7, new[] { G(1) }),
|
||||
(0x1cc, new[] { G(2) }),
|
||||
(0x223, new[] { I(102), I(6), I(101), I(1), I(100), I(1), I(50), I(200) }),
|
||||
(0x21c, System.Array.Empty<Operand>()),
|
||||
(0x2, System.Array.Empty<Operand>()),
|
||||
}, System.Array.Empty<string>());
|
||||
var host = new RecordingHost { MessageSkip = true, AdvReadSkip = true };
|
||||
var vm = new VirtualMachine(scene, table, host);
|
||||
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal(1, vm.Globals[1]);
|
||||
Assert.Equal(1, vm.Globals[2]);
|
||||
Assert.Equal(1, host.TransitionWaits);
|
||||
Assert.Equal(1.0, vm.Gfx.SnapshotForegroundTransitions(100).Single().Progress);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,27 @@ using Xunit;
|
||||
|
||||
public class RenderObjectBlendTests
|
||||
{
|
||||
[Fact]
|
||||
public void NegativeColorOperands_PreserveCurrentStaticColor()
|
||||
{
|
||||
var g = new GfxState();
|
||||
g.SetObjectColor(0x100, GfxState.PackColor(0, 0xffffff));
|
||||
g.SetObjectColorResolved(0x100, -1, -1);
|
||||
Assert.Equal(GfxState.PackColor(0, 0xffffff), g.TryGet(0x100)!.Color);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TransitionColorMode_TreatsWhiteAsIdentityAndAlphaAsOpacity()
|
||||
{
|
||||
var g = new GfxState();
|
||||
g.SetSurface(4, 1, -1);
|
||||
g.BindDraw(0x100, 4, 0, 0, 1, 1, 0, 0);
|
||||
g.SetStaticObjectColorResolved(0x100, 2, -1, -1);
|
||||
var rendered = g.SnapshotVisibleObjects().Single();
|
||||
Assert.Equal(255, rendered.Alpha);
|
||||
Assert.Equal(0, rendered.TintStrength);
|
||||
}
|
||||
|
||||
// Make a visible textured object bound to a slot that has a surface, so it appears in the snapshot.
|
||||
private static GfxState WithVisibleObject(long handle, long resId, long colorKey)
|
||||
{
|
||||
|
||||
@@ -9,12 +9,30 @@ using Age.Engine.Model;
|
||||
internal sealed class RecordingHost : IHost
|
||||
{
|
||||
public int Waits;
|
||||
public int Presents;
|
||||
public int TransitionWaits;
|
||||
public bool MessageSkip;
|
||||
public bool AdvReadSkip;
|
||||
public readonly List<(int Offset, string Text)> Lines = new();
|
||||
public readonly List<long> SleptDurations = new();
|
||||
public void ShowText(int offset, string text) => Lines.Add((offset, text));
|
||||
public void WaitForInput() => Waits++;
|
||||
public void Sleep(long duration) => SleptDurations.Add(duration);
|
||||
public void FrameYield() { }
|
||||
public bool IsMessageSkipActive => MessageSkip;
|
||||
public bool IsAdvReadSkipActive => AdvReadSkip;
|
||||
public void PresentFrame(GfxState gfx)
|
||||
{
|
||||
Presents++;
|
||||
gfx.StartForegroundTransitions(100);
|
||||
gfx.CompleteForegroundTransitions(100);
|
||||
}
|
||||
public void WaitForForegroundTransition(GfxState gfx)
|
||||
{
|
||||
TransitionWaits++;
|
||||
gfx.StartForegroundTransitions(100);
|
||||
gfx.CompleteForegroundTransitions(100);
|
||||
}
|
||||
public void CreateTexture(int slot, int w, int h) { }
|
||||
public void SetTexture(long resId, int slot) { }
|
||||
public void DrawTexture(int slot, int sx, int sy, int w, int h, int dx, int dy) { }
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Age.Engine.Model;
|
||||
|
||||
namespace Age.Engine.Hosting;
|
||||
public interface IHost
|
||||
{
|
||||
@@ -5,6 +7,14 @@ public interface IHost
|
||||
void WaitForInput();
|
||||
void Sleep(long duration);
|
||||
void FrameYield();
|
||||
// Native 0x1c7/0x1cc query two distinct ADV skip channels. Headless and non-interactive
|
||||
// hosts default to normal playback; the Godot host supplies the live interactive values.
|
||||
bool IsMessageSkipActive => false;
|
||||
bool IsAdvReadSkipActive => false;
|
||||
// Normal playback reaches op 0x21c and parks until a queued 0x223 transition completes. The
|
||||
// read/message-skip branch reaches op 0x20c and presents the completed endpoint immediately.
|
||||
void WaitForForegroundTransition(GfxState gfx) { }
|
||||
void PresentFrame(GfxState gfx) { }
|
||||
void CreateTexture(int slot, int width, int height);
|
||||
void SetTexture(long resourceId, int slot);
|
||||
void DrawTexture(int slot, int srcX, int srcY, int width, int height, int dstX, int dstY);
|
||||
|
||||
@@ -14,6 +14,12 @@ public readonly record struct RotationCycleState(bool Enabled, long PeriodMs,
|
||||
double AxisX, double AxisY, double AxisZ,
|
||||
double AngleDegrees = 0);
|
||||
|
||||
/// <summary>Sampled op-0x223 type-0 surface transition. Range A is already present in normal z-order;
|
||||
/// the compositor draws range B over it with <paramref name="Progress"/> to form the native crossfade.</summary>
|
||||
public readonly record struct SurfaceTransitionState(long CommandKey, int TargetSlot,
|
||||
long RangeAStart, int RangeACount, long RangeBStart, int RangeBCount,
|
||||
long DelayMs, long DurationMs, long StartMs, double Progress, bool Forced);
|
||||
|
||||
/// <summary>A renderable view of one visible gfx object — the host composites these in ascending-handle order
|
||||
/// (= the engine's z-order) each frame. Built by <see cref="GfxState.SnapshotVisibleObjects"/>; the surface
|
||||
/// resId/colorkey are resolved from the object's live source slot at snapshot time (see docs/engine-re.md,
|
||||
@@ -25,7 +31,8 @@ public readonly record struct RotationCycleState(bool Enabled, long PeriodMs,
|
||||
public readonly record struct RenderObject(long Handle, long SurfaceResId, long ColorKey,
|
||||
int SrcX, int SrcY, int W, int H, int DstX, int DstY,
|
||||
TransformState Transform, RotationCycleState Rotation,
|
||||
int Alpha, long Tint, int TintStrength, BlendKind Blend);
|
||||
int Alpha, long Tint, int TintStrength, BlendKind Blend,
|
||||
SurfaceTransitionState? SurfaceTransition = null);
|
||||
|
||||
/// <summary>Host-agnostic model of the AGE native gfx command-buffer (reversed in
|
||||
/// docs/engine-re.md, gfx op-contract table). One registry maps an object handle to a GfxObject — the
|
||||
@@ -35,12 +42,20 @@ public readonly record struct RenderObject(long Handle, long SurfaceResId, long
|
||||
/// the query ops read back, which is all the bytecode geometry math needs.</summary>
|
||||
public sealed class GfxState
|
||||
{
|
||||
private sealed class SurfaceTransition
|
||||
{
|
||||
public long CommandKey, RangeAStart, RangeBStart, DelayMs, DurationMs;
|
||||
public int TargetSlot, RangeACount, RangeBCount;
|
||||
public long StartMs = -1;
|
||||
public bool Forced;
|
||||
}
|
||||
public sealed class GfxObject
|
||||
{
|
||||
public (long X, long Y, long Z) V18, V24, V16c;
|
||||
public long Field64, Field68, Field6c;
|
||||
public long Color;
|
||||
public bool HasColor; // true once op 0x202/0x203 set a color/alpha modulation on this object
|
||||
public long StaticColorMode; // op 0x203 operand 2 -> obj+0x30; mode 2 is transition alpha/identity
|
||||
|
||||
// ---- src-rect / spritesheet-cell channel (ops 0x239 static cell, 0x231 animate). Interpolator
|
||||
// SRC-RECT SCROLL channel: period obj+0x230, start obj+0x21c, grid obj+0x238/0x23c. ----
|
||||
@@ -115,6 +130,37 @@ public sealed class GfxState
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Op 0x21d: clone the native 0x2d4-byte retained-object record from source to destination.</summary>
|
||||
public bool CloneObject(long sourceHandle, long destinationHandle)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_objects.TryGetValue(sourceHandle, out var s)) return false;
|
||||
_objects[destinationHandle] = new GfxObject
|
||||
{
|
||||
V18 = s.V18, V24 = s.V24, V16c = s.V16c,
|
||||
Field64 = s.Field64, Field68 = s.Field68, Field6c = s.Field6c,
|
||||
Color = s.Color, HasColor = s.HasColor, StaticColorMode = s.StaticColorMode,
|
||||
SrcGridW = s.SrcGridW, SrcGridH = s.SrcGridH, SrcCell = s.SrcCell,
|
||||
SrcPeriod = s.SrcPeriod, SrcStart = s.SrcStart, SrcAnim = s.SrcAnim,
|
||||
ColorPeriod = s.ColorPeriod, ColorStart = s.ColorStart, ColorTarget = s.ColorTarget,
|
||||
ColorAnim = s.ColorAnim, SourceSlot = s.SourceSlot, SrcRect = s.SrcRect, Visible = s.Visible,
|
||||
ScaleCurrent = s.ScaleCurrent, ScaleTarget = s.ScaleTarget,
|
||||
ScaleDelayMs = s.ScaleDelayMs, ScaleDurationMs = s.ScaleDurationMs, ScaleEnabled = s.ScaleEnabled,
|
||||
TranslationCurrent = s.TranslationCurrent, TranslationTarget = s.TranslationTarget,
|
||||
TranslationDelayMs = s.TranslationDelayMs, TranslationDurationMs = s.TranslationDurationMs,
|
||||
TranslationEnabled = s.TranslationEnabled,
|
||||
RotationCurrent = s.RotationCurrent, RotationTarget = s.RotationTarget,
|
||||
RotationDelayMs = s.RotationDelayMs, RotationDurationMs = s.RotationDurationMs,
|
||||
RotationChannelEnabled = s.RotationChannelEnabled, MatrixStartMs = s.MatrixStartMs,
|
||||
RotationPeriodMs = s.RotationPeriodMs, RotationAxis = s.RotationAxis,
|
||||
RotationEnabled = s.RotationEnabled, RotationStartMs = s.RotationStartMs,
|
||||
};
|
||||
CurrentObject = destinationHandle;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Op 0x1a2: retain the operand's current value in the separate descriptor registry. This does
|
||||
/// not populate the retained-object map used by op 0x215.</summary>
|
||||
public void Register(long handle) { lock (_lock) { _operandRegistry.Add(handle); } }
|
||||
@@ -158,6 +204,7 @@ public sealed class GfxState
|
||||
|
||||
// ---- surfaces (image buffers per slot): ctx+0x52bd4[slot], from create/set-texture ----
|
||||
private readonly Dictionary<int, (long ResId, long ColorKey)> _surfaces = new();
|
||||
private readonly Dictionary<int, SurfaceTransition> _surfaceTransitions = new();
|
||||
public void SetSurface(int slot, long resId, long colorKey) { lock (_lock) { _surfaces[slot] = (resId, colorKey); } }
|
||||
|
||||
/// <summary>Ops 0x202/0x203: record a packed 0xAARRGGBB color/alpha modulation on the object and mark it
|
||||
@@ -167,6 +214,27 @@ public sealed class GfxState
|
||||
lock (_lock) { var o = GetOrCreate(handle); o.Color = packed; o.HasColor = true; }
|
||||
}
|
||||
|
||||
/// <summary>Resolve 0x202/0x203's native negative sentinels against obj+0x60 (the current static
|
||||
/// packed color): negative alpha preserves its alpha byte; negative RGB preserves its RGB bytes.</summary>
|
||||
public void SetObjectColorResolved(long handle, long alpha, long rgb)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var o = GetOrCreate(handle);
|
||||
long current = o.HasColor ? o.Color : 0xffffffff;
|
||||
long resolvedAlpha = alpha < 0 ? (current >> 24) & 0xff : System.Math.Min(alpha, 0xff);
|
||||
long resolvedRgb = rgb < 0 ? current & 0xffffff : rgb & 0xffffff;
|
||||
o.Color = PackColor(resolvedAlpha, resolvedRgb);
|
||||
o.HasColor = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetStaticObjectColorResolved(long handle, long mode, long alpha, long rgb)
|
||||
{
|
||||
SetObjectColorResolved(handle, alpha, rgb);
|
||||
lock (_lock) GetOrCreate(handle).StaticColorMode = mode;
|
||||
}
|
||||
|
||||
/// <summary>Ops 0x239 (static cell, period=0) / 0x231 (animate, period>0): set the spritesheet grid +
|
||||
/// the visible cell. When animated, the interpolator ping-pongs the cell across the grid over the period.</summary>
|
||||
public void SetSrcRect(long handle, long gridW, long gridH, long cell, long period)
|
||||
@@ -191,6 +259,73 @@ public sealed class GfxState
|
||||
}
|
||||
public void ClearSurface(int slot) { lock (_lock) { _surfaces[slot] = (0, 0); } } // create-texture (blank)
|
||||
|
||||
/// <summary>Op 0x223: queue a type-0 timed alpha transition into a target surface slot.</summary>
|
||||
public void QueueSurfaceAlphaTransition(long commandKey, int targetSlot,
|
||||
long rangeAStart, int rangeACount, long rangeBStart, int rangeBCount,
|
||||
long delayMs, long durationMs)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_surfaceTransitions[targetSlot] = new SurfaceTransition
|
||||
{
|
||||
CommandKey = commandKey, TargetSlot = targetSlot,
|
||||
RangeAStart = rangeAStart, RangeACount = System.Math.Max(0, rangeACount),
|
||||
RangeBStart = rangeBStart, RangeBCount = System.Math.Max(0, rangeBCount),
|
||||
DelayMs = System.Math.Max(0, delayMs), DurationMs = System.Math.Max(0, durationMs),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Start every pending foreground transition at the native present boundary.</summary>
|
||||
public int StartForegroundTransitions(long nowMs)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
int started = 0;
|
||||
foreach (var t in _surfaceTransitions.Values)
|
||||
if (t.StartMs < 0) { t.StartMs = nowMs; started++; }
|
||||
return started;
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasActiveForegroundTransitions(long nowMs)
|
||||
{
|
||||
lock (_lock)
|
||||
return _surfaceTransitions.Values.Any(t => TransitionProgress(t, nowMs) < 1.0);
|
||||
}
|
||||
|
||||
/// <summary>Click completion affects only type-0 foreground transitions, never ambient object channels.</summary>
|
||||
public int CompleteForegroundTransitions(long nowMs)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
int completed = 0;
|
||||
foreach (var t in _surfaceTransitions.Values)
|
||||
if (!t.Forced && TransitionProgress(t, nowMs) < 1.0) { t.Forced = true; completed++; }
|
||||
return completed;
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<SurfaceTransitionState> SnapshotForegroundTransitions(long nowMs)
|
||||
{
|
||||
lock (_lock)
|
||||
return _surfaceTransitions.Values.Select(t => SampleTransition(t, nowMs)).ToList();
|
||||
}
|
||||
|
||||
private static double TransitionProgress(SurfaceTransition t, long nowMs)
|
||||
{
|
||||
if (t.Forced) return 1.0;
|
||||
if (t.StartMs < 0) return 0.0;
|
||||
long elapsed = nowMs - t.StartMs - t.DelayMs;
|
||||
if (elapsed <= 0) return 0.0;
|
||||
if (t.DurationMs <= 0) return 1.0;
|
||||
return System.Math.Clamp(elapsed / (double)t.DurationMs, 0.0, 1.0);
|
||||
}
|
||||
|
||||
private static SurfaceTransitionState SampleTransition(SurfaceTransition t, long nowMs)
|
||||
=> new(t.CommandKey, t.TargetSlot, t.RangeAStart, t.RangeACount, t.RangeBStart, t.RangeBCount,
|
||||
t.DelayMs, t.DurationMs, t.StartMs, TransitionProgress(t, nowMs), t.Forced);
|
||||
|
||||
/// <summary>draw-texture bind (gfx_object_bind_draw): object <paramref name="handle"/> draws surface
|
||||
/// <paramref name="slot"/>'s rect at (dstX,dstY) and becomes visible.</summary>
|
||||
public void BindDraw(long handle, int slot, int sx, int sy, int w, int h, int dstX, int dstY)
|
||||
@@ -289,7 +424,17 @@ public sealed class GfxState
|
||||
if (o.HasColor)
|
||||
{
|
||||
var (a, r, g, b) = BlendMath.UnpackArgb(o.Color);
|
||||
strength = a; tint = ((long)r << 16) | ((long)g << 8) | (long)b; blend = BlendKind.Alpha;
|
||||
tint = ((long)r << 16) | ((long)g << 8) | (long)b;
|
||||
if (o.StaticColorMode == 2)
|
||||
{
|
||||
// Native transition-source mode: 0xffffffff is opaque identity modulation,
|
||||
// not a request to replace every texel with white.
|
||||
alpha = a; strength = 0; blend = BlendKind.Alpha;
|
||||
}
|
||||
else
|
||||
{
|
||||
strength = a; blend = BlendKind.Alpha;
|
||||
}
|
||||
}
|
||||
if (o.ColorAnim)
|
||||
{
|
||||
@@ -342,6 +487,8 @@ public sealed class GfxState
|
||||
cycleAngle = ((elapsed % o.RotationPeriodMs) * 360) / o.RotationPeriodMs;
|
||||
}
|
||||
|
||||
SurfaceTransitionState? transition = _surfaceTransitions.TryGetValue(o.SourceSlot, out var st)
|
||||
? SampleTransition(st, nowMs) : null;
|
||||
list.Add(new RenderObject(kv.Key, resId, ck, srcX, srcY, w, h,
|
||||
(int)o.V24.X, (int)o.V24.Y,
|
||||
new TransformState(scale.X, scale.Y, scale.Z,
|
||||
@@ -351,7 +498,7 @@ public sealed class GfxState
|
||||
new RotationCycleState(o.RotationEnabled, o.RotationPeriodMs,
|
||||
o.RotationAxis.X, o.RotationAxis.Y,
|
||||
o.RotationAxis.Z, cycleAngle),
|
||||
alpha, tint, strength, blend));
|
||||
alpha, tint, strength, blend, transition));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@@ -277,6 +277,11 @@ public sealed class VirtualMachine
|
||||
_host.WaitForInput(); return pc + 1;
|
||||
case "sleep": // 0xc8 (duration) — pause the host duration ms; headless hosts no-op (parity). Frame pacing.
|
||||
_host.Sleep(Read(a[0])); return pc + 1;
|
||||
case "get-message-skip": // 0x1c7: Ctrl/message fast-forward run-state bit
|
||||
Write(a[0], _host.IsMessageSkipActive ? 1 : 0); return pc + 1;
|
||||
case "get-adv-read-skip-state": // 0x1cc: per-message read/click skip service state
|
||||
case "get-adv-service-state": // compatibility with pre-recovery generated tables
|
||||
Write(a[0], _host.IsAdvReadSkipActive ? 1 : 0); return pc + 1;
|
||||
case "end-text-line": case "set-font":
|
||||
case "comment": case "display-furigana": case "dev_ukn":
|
||||
return pc + 1;
|
||||
@@ -362,10 +367,12 @@ public sealed class VirtualMachine
|
||||
Gfx.EraseRange(Read(a[0]), Read(a[1])); return pc + 1;
|
||||
case "gfx-elem-release": // 0x1fa (surface slot)
|
||||
Gfx.ClearSurface((int)Read(a[0])); return pc + 1;
|
||||
case "clone-gfx-object": // 0x21d (source handle)(destination handle)
|
||||
Gfx.CloneObject(Read(a[0]), Read(a[1])); return pc + 1;
|
||||
case "gfx-blit-color": // 0x202 (handle)(x)(y)(alpha)(color) — static alpha/tint (anim interp deferred)
|
||||
Gfx.SetObjectColor(Read(a[0]), GfxState.PackColor(Read(a[3]), Read(a[4]))); return pc + 1;
|
||||
Gfx.SetObjectColorResolved(Read(a[0]), Read(a[3]), Read(a[4])); return pc + 1;
|
||||
case "gfx-draw-color": // 0x203 (handle)(v)(alpha)(color) — static alpha/tint
|
||||
Gfx.SetObjectColor(Read(a[0]), GfxState.PackColor(Read(a[2]), Read(a[3]))); return pc + 1;
|
||||
Gfx.SetStaticObjectColorResolved(Read(a[0]), Read(a[1]), Read(a[2]), Read(a[3])); return pc + 1;
|
||||
// ---- sprite transform / animation cluster (docs/engine-re.md "0x21c-0x243 ... ANIMATION") ----
|
||||
case "set-anim-transform-abs": // 0x220 (handle)(delay)(duration)(tx)(ty)(tz)
|
||||
Gfx.SetTranslationChannel(Read(a[0]), Read(a[1]), Read(a[2]),
|
||||
@@ -382,7 +389,13 @@ public sealed class VirtualMachine
|
||||
Gfx.SetAnimClock(Read(a[0])); return pc + 1;
|
||||
case "reset-anim-clock": // 0x243: reset the separate global animation-service clock
|
||||
Gfx.ResetAnimClock(); return pc + 1;
|
||||
case "mark-frame-yield": // 0x21c: host already yields after every completed opcode
|
||||
case "queue-surface-alpha-transition": // 0x223: target surface crossfade over two object ranges
|
||||
Gfx.QueueSurfaceAlphaTransition(Read(a[0]), (int)Read(a[1]), Read(a[2]), (int)Read(a[3]),
|
||||
Read(a[4]), (int)Read(a[5]), Read(a[6]), Read(a[7])); return pc + 1;
|
||||
case "present-frame": // 0x20c: read/message-skip path snaps a queued transition to its endpoint
|
||||
_host.PresentFrame(Gfx); return pc + 1;
|
||||
case "mark-frame-yield": // 0x21c: normal foreground-transition scheduler/resume boundary
|
||||
_host.WaitForForegroundTransition(Gfx); return pc + 1;
|
||||
case "clear-gfx-command-queue": // 0x224: retained compositor does not use this native queue
|
||||
return pc + 1;
|
||||
default:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Age.Engine.Hosting;
|
||||
using Age.Engine.Model;
|
||||
using Age.Engine.Sys4;
|
||||
|
||||
public sealed class GodotAdvHost : IHost
|
||||
@@ -18,7 +19,11 @@ public sealed class GodotAdvHost : IHost
|
||||
private readonly GodotTimelineLog? _timeline;
|
||||
private readonly System.Threading.AutoResetEvent _frameSignal = new(false);
|
||||
private volatile bool _stopping;
|
||||
private GfxState? _foregroundGfx;
|
||||
public volatile bool IsWaiting;
|
||||
public volatile bool IsTransitionWaiting;
|
||||
private long _transitionStartedAtMs = -1;
|
||||
public long TransitionStartedAtMs => System.Threading.Interlocked.Read(ref _transitionStartedAtMs);
|
||||
public readonly List<(int Offset, string Text)> Captured = new();
|
||||
|
||||
public GodotAdvHost(Main main, ResourceMap res, string scene, Age.Engine.Hosting.FrameClock clock,
|
||||
@@ -50,8 +55,64 @@ public sealed class GodotAdvHost : IHost
|
||||
_main.CallDeferred("ClearPage");
|
||||
}
|
||||
|
||||
// called from the main thread (click) or the selftest auto-clicker
|
||||
public void SignalInput() { if (IsWaiting && _gate.CurrentCount == 0) _gate.Release(); }
|
||||
// Called from the main thread (click) or an auto-clicker. A transition click is consumed by
|
||||
// the foreground lifecycle; it never pre-arms or advances the following stable input wait.
|
||||
public void SignalInput()
|
||||
{
|
||||
if (IsTransitionWaiting && _foregroundGfx != null)
|
||||
{
|
||||
int completed = _foregroundGfx.CompleteForegroundTransitions(_clock.NowMs);
|
||||
if (completed > 0)
|
||||
{
|
||||
_timeline?.State("transition-forced-complete", new() { ["count"] = completed });
|
||||
_frameSignal.Set();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (IsWaiting && _gate.CurrentCount == 0) _gate.Release();
|
||||
}
|
||||
|
||||
public void WaitForForegroundTransition(GfxState gfx)
|
||||
{
|
||||
int started = gfx.StartForegroundTransitions(_clock.NowMs);
|
||||
if (started == 0 && !gfx.HasActiveForegroundTransitions(_clock.NowMs)) return;
|
||||
_foregroundGfx = gfx;
|
||||
System.Threading.Interlocked.Exchange(ref _transitionStartedAtMs, _clock.NowMs);
|
||||
IsTransitionWaiting = true;
|
||||
_timeline?.State("transition-start", new() { ["count"] = started });
|
||||
int lastBucket = -1;
|
||||
while (gfx.HasActiveForegroundTransitions(_clock.NowMs) && !_stopping)
|
||||
{
|
||||
var active = gfx.SnapshotForegroundTransitions(_clock.NowMs);
|
||||
int bucket = active.Count == 0 ? 100 : (int)System.Math.Floor(active[0].Progress * 10);
|
||||
if (bucket != lastBucket)
|
||||
{
|
||||
lastBucket = bucket;
|
||||
_timeline?.State("transition-progress", new()
|
||||
{
|
||||
["progress"] = active.Count == 0 ? 1.0 : active[0].Progress,
|
||||
["forced"] = active.Count != 0 && active[0].Forced,
|
||||
});
|
||||
}
|
||||
_frameSignal.WaitOne(50);
|
||||
}
|
||||
IsTransitionWaiting = false;
|
||||
System.Threading.Interlocked.Exchange(ref _transitionStartedAtMs, -1);
|
||||
_foregroundGfx = null;
|
||||
_opPacer.Reset();
|
||||
_timeline?.State("running", new() { ["transition_complete"] = true });
|
||||
}
|
||||
|
||||
public void PresentFrame(GfxState gfx)
|
||||
{
|
||||
int started = gfx.StartForegroundTransitions(_clock.NowMs);
|
||||
int completed = gfx.CompleteForegroundTransitions(_clock.NowMs);
|
||||
if (started > 0 || completed > 0)
|
||||
_timeline?.State("transition-skip-complete", new()
|
||||
{
|
||||
["started"] = started, ["completed"] = completed,
|
||||
});
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
|
||||
@@ -23,7 +23,7 @@ public sealed class GodotTimelineLog : System.IDisposable
|
||||
var dir = Path.GetDirectoryName(path);
|
||||
if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
|
||||
_writer = new StreamWriter(path) { AutoFlush = true };
|
||||
Record("start", new() { ["transition"] = "unmodeled" });
|
||||
Record("start", new() { ["transition"] = "surface-alpha-lifecycle" });
|
||||
}
|
||||
|
||||
public void SetFrame(int frame, long nowMs)
|
||||
|
||||
@@ -98,6 +98,7 @@ public partial class Main : Godot.Control
|
||||
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 speed = 1.0; // --speed <f>: whole-runtime diagnostic speed
|
||||
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
|
||||
for (int i = 0; i < userArgs.Length; i++)
|
||||
{
|
||||
@@ -111,6 +112,7 @@ public partial class Main : Godot.Control
|
||||
if (userArgs[i] == "--frames" && i + 1 < userArgs.Length) int.TryParse(userArgs[i + 1], out _seqFrames);
|
||||
if (userArgs[i] == "--sleep-scale" && i + 1 < userArgs.Length) double.TryParse(userArgs[i + 1], out sleepScale);
|
||||
if (userArgs[i] == "--speed" && i + 1 < userArgs.Length) double.TryParse(userArgs[i + 1], out speed);
|
||||
if (userArgs[i] == "--transition-click-ms" && i + 1 < userArgs.Length) long.TryParse(userArgs[i + 1], out transitionClickMs);
|
||||
if (userArgs[i] == "--trace-histogram" && i + 1 < userArgs.Length) histFile = userArgs[i + 1];
|
||||
if (userArgs[i] == "--seed" && i + 1 < userArgs.Length)
|
||||
{
|
||||
@@ -167,6 +169,17 @@ public partial class Main : Godot.Control
|
||||
// --shot-sequence: auto-advance past every input wait so the paced burst isn't blocked on a click.
|
||||
if (_seqDir != null)
|
||||
_ = Task.Run(async () => { while (!_done) { if (_host.IsWaiting) _host.SignalInput(); await Task.Delay(1); } });
|
||||
if (transitionClickMs >= 0)
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
while (!_done)
|
||||
{
|
||||
if (_host.IsTransitionWaiting && _host.TransitionStartedAtMs >= 0 &&
|
||||
_clock.NowMs - _host.TransitionStartedAtMs >= transitionClickMs)
|
||||
_host.SignalInput();
|
||||
await Task.Delay(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
@@ -253,7 +266,8 @@ public partial class Main : Godot.Control
|
||||
_screen.Fill(new Color(0, 0, 0, 0));
|
||||
System.Collections.Generic.Dictionary<long, string>? decisions = _gfxLogPath != null || _timeline != null ? new() : null;
|
||||
int z = 0;
|
||||
foreach (var v in _vm.Gfx.SnapshotVisibleObjects(_clock.NowMs)) // interpolate at the throttled clock
|
||||
var visible = _vm.Gfx.SnapshotVisibleObjects(_clock.NowMs); // one synchronized sample for objects + ranges
|
||||
foreach (var v in visible) // interpolate at the throttled clock
|
||||
{
|
||||
var t = v.Transform;
|
||||
var affine = Age.Engine.Model.Transform2DMath.Build(t, v.Rotation);
|
||||
@@ -264,7 +278,13 @@ public partial class Main : Godot.Control
|
||||
float opacity = v.Alpha / 255f; // transform Z is never opacity
|
||||
float strength = v.TintStrength / 255f; // tint-blend / fill strength
|
||||
string outcome;
|
||||
if (v.SurfaceResId == 0)
|
||||
if (v.SurfaceTransition is { } transition)
|
||||
{
|
||||
int layers = DrawTransitionRange(visible, transition);
|
||||
outcome = $"TRANSITION slot={transition.TargetSlot} key=0x{transition.CommandKey:x} " +
|
||||
$"progress={transition.Progress:0.000} forced={transition.Forced} layers={layers}";
|
||||
}
|
||||
else if (v.SurfaceResId == 0)
|
||||
{
|
||||
// A colored object with no bound surface = a fade/flash fill (e.g. fade-to-black). Its presence
|
||||
// is the tint STRENGTH (0=absent, 255=solid), scaled by any object opacity. Uncolored surfaceless
|
||||
@@ -305,6 +325,37 @@ public partial class Main : Godot.Control
|
||||
if (decisions != null) LogGfxDecisionChanges(decisions);
|
||||
}
|
||||
|
||||
// Native type-0 surface commands first leave range A in normal z-order, then alpha-composite range B
|
||||
// into the target surface. SC0000 binds that target to handle+2, above both source handles, so drawing
|
||||
// range B here with progress produces old*(1-progress)+new*progress without disturbing ambient channels.
|
||||
private int DrawTransitionRange(IReadOnlyList<RenderObject> visible, SurfaceTransitionState transition)
|
||||
{
|
||||
int drawn = 0;
|
||||
long end = transition.RangeBStart + transition.RangeBCount;
|
||||
foreach (var source in visible)
|
||||
{
|
||||
if (source.Handle < transition.RangeBStart || source.Handle >= end || source.SurfaceTransition != null)
|
||||
continue;
|
||||
var affine = Transform2DMath.Build(source.Transform, source.Rotation).FromLocalOrigin(source.DstX, source.DstY);
|
||||
float opacity = source.Alpha / 255f * (float)transition.Progress;
|
||||
if (source.SurfaceResId == 0)
|
||||
{
|
||||
if (source.Blend == BlendKind.Opaque) continue;
|
||||
int w = source.W > 0 ? source.W : 800, h = source.H > 0 ? source.H : 600;
|
||||
FillAffineQuad(w, h, affine, source.Tint, opacity * source.TintStrength / 255f);
|
||||
}
|
||||
else
|
||||
{
|
||||
var bmp = _host.ResolveResIdTexture(source.SurfaceResId);
|
||||
if (bmp == null) continue;
|
||||
BlitLayer(bmp, source.ColorKey, source.Tint, source.TintStrength / 255f,
|
||||
source.SrcX, source.SrcY, source.W, source.H, affine, opacity);
|
||||
}
|
||||
drawn++;
|
||||
}
|
||||
return drawn;
|
||||
}
|
||||
|
||||
// Diagnostic (--gfx-log): print, per rendered frame, only the objects whose compositor outcome CHANGED
|
||||
// since last frame (added / gone / drawn↔skip / resId change). Quiet until something actually changes, so
|
||||
// the frame where the background drops out — and WHY — stands out. See systematic-debugging of the grey-BG.
|
||||
|
||||
@@ -158,6 +158,11 @@ name = "coroutine_resume_off"
|
||||
type = "int"
|
||||
note = "op 0x7c resume PC offset"
|
||||
[[field]]
|
||||
offset = 0x6dbd4
|
||||
name = "adv_read_skip_state"
|
||||
type = "int"
|
||||
note = "current ADV read/click-skip service state; queried by op 0x1cc, maintained from ReadTextSkip + per-PC read history"
|
||||
[[field]]
|
||||
offset = 0x9b24c
|
||||
name = "dispatch_table"
|
||||
type = "void*"
|
||||
|
||||
@@ -4199,19 +4199,19 @@ observed_types = ["g-int"]
|
||||
|
||||
[[opcode]]
|
||||
op = 0x1cc
|
||||
label = "get-adv-service-state"
|
||||
label = "get-adv-read-skip-state"
|
||||
argc = 1
|
||||
abi_source = "kelebek+decode-validated"
|
||||
|
||||
[opcode.semantics]
|
||||
name = "get-adv-service-state"
|
||||
name = "get-adv-read-skip-state"
|
||||
category = "control"
|
||||
summary = "(out) - copy native ADV service state ctx+0x6dbd4; label_1235a ORs it with message-skip to select its yield branch."
|
||||
summary = "(out) - copy the current ADV read/click-skip service state from ctx+0x6dbd4. label_1235a ORs it with 0x1c7's Ctrl/message-skip bit: zero takes 0x21c's normal transition/yield path; nonzero resets the animation service and presents the completed endpoint through 0x20c."
|
||||
noop_headless = false
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
depends_on = []
|
||||
evidence = "Ghidra handler 0x427330 calls vm_operand_write(1, ctx+0x6dbd4). Exact service-state producer remains outside this opcode."
|
||||
evidence = "Ghidra handler 0x427330 calls vm_operand_write(1, ctx+0x6dbd4). Producer recovered 2026-07-10: adv_update_read_text_skip_state@0x406cd0 and op 0x6e/0x71/0x72 maintain the field from message_ReadTextSkip plus current-PC read-history lookup; adv_interpreter_tick consumes it in click/read-skip control. It is not the 0x223 surface-transition progress flag."
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 1
|
||||
@@ -4832,12 +4832,12 @@ abi_source = "kelebek+decode-validated"
|
||||
[opcode.semantics]
|
||||
name = "gfx-draw-color"
|
||||
category = "draw"
|
||||
summary = "0x203 (handle)(v)(alpha)(color) — gfx cmd-type 9. Handler gfx_op_0x203_draw_color @0x4229a0: worker gfx_op_0x203_worker_set_color @0x47e9b0 sets a STATIC color/alpha (obj+0x60), no anim bit; packs ARGB from alpha(op3)/color(op4). Sibling of 0x202 (one fewer positional arg). C# VM (2026-07-08 blend slice): routes through GfxState.SetObjectColor → compositor applies static alpha/tint (BlendKind.Alpha). See docs/engine-re.md §Blend & transparency."
|
||||
summary = "0x203 (handle)(mode)(alpha)(color) — gfx cmd-type 9. Worker stores mode at obj+0x30 and STATIC packed color at obj+0x60. Negative alpha/RGB preserve current static bytes through FUN_0047f3e0. Mode 2 is the 0x223 transition-source alpha/identity path (0xffffffff = opaque identity, not solid white); mode 0 retains the established tint-strength behavior."
|
||||
noop_headless = false
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
depends_on = []
|
||||
evidence = "Ghidra handler 0x4229a0 (dispatch ctx[0x26c93+0x203]); packs color from operands 3/4, FUN_0047e9b0(op1,op2,packed)."
|
||||
evidence = "Ghidra handler 0x4229a0 (dispatch ctx[0x26c93+0x203]); negative operands read current obj+0x60 via FUN_0047f3e0, then gfx_op_0x203_worker_set_color stores op2 at obj+0x30 and packed ARGB at obj+0x60. SC0000 transition setup uses mode 2 with -1/-1 before 0x21d/0x223; synchronized window pixels prove treating 0xffffffff as generic white tint is wrong."
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 1
|
||||
@@ -5109,12 +5109,12 @@ abi_source = "kelebek+decode-validated"
|
||||
[opcode.semantics]
|
||||
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
|
||||
summary = "Present the composited frame; label_1235a uses this on the read/message-skip branch to expose the completed foreground endpoint immediately."
|
||||
details = "Native handler gfx_op_0x20c_present_frame (dispatch ctx[0x26c93+0x20c]) -> gfx_render_frame @0x4820b0. Godot composites continuously. In label_1235a the OR of 0x1c7/0x1cc is nonzero on the read/message-skip branch, which resets the animation service then presents; the port starts and snaps any pending 0x223 transition to its endpoint here. Normal zero-state playback branches to 0x21c, which owns wait/resume. Headless hosts remain non-blocking. Kelebek label u00416200 was VA-drift."
|
||||
noop_headless = false
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
depends_on = []
|
||||
depends_on = [0x223, 0x1c7, 0x1cc]
|
||||
evidence = "Ghidra: dispatch table FUN_00413860 param_1[0x26e9f]=gfx_op_0x20c_present_frame; 0x26e9f-0x26c93=0x20c. 2026-07-08."
|
||||
|
||||
[[opcode]]
|
||||
@@ -5130,7 +5130,7 @@ summary = ""
|
||||
noop_headless = false
|
||||
source = "kelebek"
|
||||
confidence = "low"
|
||||
depends_on = []
|
||||
depends_on = [0x20c, 0x1c7, 0x1cc]
|
||||
evidence = ""
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
@@ -5468,28 +5468,29 @@ abi_source = "kelebek+decode-validated"
|
||||
[opcode.semantics]
|
||||
name = "mark-frame-yield"
|
||||
category = "control"
|
||||
summary = "Set native run-state bit 0x400. Host-implicit: the port already offers a scheduler yield after every completed opcode."
|
||||
noop_headless = true
|
||||
summary = "Set native run-state bit 0x400; in normal ADV playback this is the queued foreground-transition yield/resume boundary."
|
||||
details = "SC0000 label_1235a reaches this when the OR of 0x1c7 message-skip and 0x1cc read-skip state is zero (normal playback). Native run-state bit 0x400 yields the interpreter while the queued foreground presentation advances. The interactive port starts pending 0x223 commands here, parks only the VM thread while per-frame compositing continues, and resumes after natural or click-forced completion."
|
||||
noop_headless = false
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
depends_on = []
|
||||
evidence = "Ghidra handler 0x417520 sets cmd-type 1 and ORs ctx+0xa0ce4 with 0x400. SC0000 label_1235a reaches it only when op 0x1c7 or 0x1cc is nonzero."
|
||||
depends_on = [0x223, 0x1c7, 0x1cc]
|
||||
evidence = "Ghidra handler 0x417520 sets cmd-type 1 and ORs ctx+0xa0ce4 with 0x400. SC0000 label_1235a's jcc reaches it when the OR of op 0x1c7/0x1cc is zero; synchronized port trace confirmed this is the normal path after 0x223."
|
||||
|
||||
[[opcode]]
|
||||
op = 0x21d
|
||||
label = "u00421410"
|
||||
label = "clone-gfx-object"
|
||||
argc = 2
|
||||
abi_source = "kelebek+decode-validated"
|
||||
|
||||
[opcode.semantics]
|
||||
name = "u00421410"
|
||||
category = "unknown"
|
||||
summary = ""
|
||||
name = "clone-gfx-object"
|
||||
category = "draw"
|
||||
summary = "(source_handle)(destination_handle) - clone the complete retained gfx object record (0xb5 dwords / 0x2d4 bytes). SC0000 uses destination=source+1 as range A's old-frame snapshot immediately before 0x223 crossfades range B's updated source into target handle source+2."
|
||||
noop_headless = false
|
||||
source = "kelebek"
|
||||
confidence = "low"
|
||||
depends_on = []
|
||||
evidence = ""
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
depends_on = [0x223]
|
||||
evidence = "Ghidra handler 0x423310 -> gfx_object_clone@0x47e4f0. Worker requires source in ctx+0x408 map, creates destination, copies exactly 0xb5 dwords from source object record, then marks ctx+0xb558 dirty. SC0000 site 0x128fc passes current handle and handle+1 before queueing 0x223 at 0x129e7."
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 1
|
||||
@@ -5683,7 +5684,7 @@ summary = "(command_key)(target_slot)(range_a_start)(range_a_count)(range_b_star
|
||||
noop_headless = false
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
depends_on = []
|
||||
depends_on = [0x20c, 0x21c, 0x1c7, 0x1cc]
|
||||
evidence = "Ghidra handler 0x423620 -> gfx_queue_surface_alpha_transition@0x47f440. Record fields: type +0=0, start +4=0, delay +8=arg7, duration +0xc=arg8, slot +0x10=arg2, range A +0x14/+0x1c=args3/4, range B +0x18/+0x20=args5/6. gfx_render_frame@0x47fbc0 initializes start from ctx+0xb550 and consumes type 0 as an alpha ramp. SC0000 executes one shared-helper site at 0x129e7."
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
|
||||
Reference in New Issue
Block a user