Implement retained graphics lifecycle ops
This commit is contained in:
@@ -6,7 +6,6 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch-
|
||||
|
||||
| offset | name | type | note |
|
||||
|---|---|---|---|
|
||||
| `0x408` | `gfx_obj_registry` | `int` | retained gfx-object map (std::map handle->object); geometry/draw get-or-create, 0x215 returns obj+4 source slot, 0x1f7 erases |
|
||||
| `0x40c` | `sys4ini_count` | `int` | SYS4INI record count |
|
||||
| `0x410` | `archive_name_table` | `void*` | archive-name table base (arc_id*0x100 indexes it) |
|
||||
| `0x414` | `sys4ini_records` | `void*` | SYS4INI 80-byte record base {name[64],arc_id,file_number,offset,size}; record = base + id*0x50 |
|
||||
@@ -15,15 +14,22 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch-
|
||||
| `0x3028` | `alt_pack_table` | `int` | call-script high-byte alternate pack table (unused by corpus) |
|
||||
| `0x4d7c` | `shared_profile_state` | `void*` | embedded shared SAVE.DAT state object; owns profile integer/settings tables and container timing metadata |
|
||||
| `0x5190` | `shared_profile_int_table` | `int` | open-addressing 12-byte string-key to 32-bit value table; op 0x1a2 stores, 0x1a3 loads, shared SAVE.DAT serializes it |
|
||||
| `0xb558` | `gfx_dirty_a` | `int` | gfx dirty flag (anim set raises) |
|
||||
| `0xb560` | `gfx_dirty_b` | `int` | gfx dirty flag |
|
||||
| `0x14d54` | `gfx_obj_ptr_table` | `void*` | per-object pointer table (ops 0x212/0x213 write obj+0x64/0x68/0x6c) |
|
||||
| `0x14ea0` | `text_line_spacing` | `int` | extra pixel leading between text lines; defaults to 6, op 0x8b writes it |
|
||||
| `0x14f45` | `script_frame_index` | `int` | call-script frame index (0x1e-dword frames) |
|
||||
| `0x46188` | `renderer_device_state` | `int` | embedded D3D9 device/render state; op 0x20e passes this base to the target/depth clear worker |
|
||||
| `0x46598` | `d3d9_device` | `void*` | IDirect3DDevice9 pointer at renderer_device_state+0x410; op 0x20e invokes vtable Clear +0xac |
|
||||
| `0x46614` | `retained_gfx_manager` | `int` | embedded retained graphics manager passed by gfx opcode handlers; worker offsets are relative to this base |
|
||||
| `0x46a1c` | `gfx_obj_registry` | `int` | retained_gfx_manager+0x408 std::map handle->object; 0x1f6 clears, geometry/draw get-or-create, 0x215 queries, 0x1f7 erases |
|
||||
| `0x46d14` | `query_table_46d14` | `void*` | stride-0x14 table read by op 0x216 |
|
||||
| `0x51b64` | `frame_timer` | `int` | frame timer (present updates 0x51b64/0x51b68) |
|
||||
| `0x51b78` | `anim_clock_elapsed` | `int` | global anim clock elapsed (op 0x238 zeroes) |
|
||||
| `0x51b7c` | `anim_clock_duration` | `int` | global anim clock total duration (op 0x238 sets) |
|
||||
| `0x50ba4` | `gfx_render_target_table` | `void*` | retained_gfx_manager+0xa590 array of per-slot D3D texture wrappers selected by op 0x20d |
|
||||
| `0x51b44` | `gfx_current_render_target` | `int` | retained_gfx_manager+0xb530 selected surface slot; -1 denotes device backbuffer |
|
||||
| `0x51b64` | `frame_timer` | `int` | frame timer at retained_gfx_manager+0xb550; present updates 0x51b64/0x51b68 and object animation samples it |
|
||||
| `0x51b6c` | `gfx_dirty_a` | `int` | retained_gfx_manager+0xb558 dirty flag; op 0x1f6 clears it |
|
||||
| `0x51b70` | `gfx_force_complete` | `int` | retained_gfx_manager+0xb55c requests forced completion of one-shot channels; op 0x1f6 clears it |
|
||||
| `0x51b74` | `gfx_dirty_b` | `int` | retained_gfx_manager+0xb560 dirty flag |
|
||||
| `0x51b78` | `anim_clock_elapsed` | `int` | global anim clock elapsed at retained_gfx_manager+0xb564; op 0x238 and object reset 0x1f6 zero it |
|
||||
| `0x51b7c` | `anim_clock_duration` | `int` | global anim clock duration at retained_gfx_manager+0xb568; op 0x238 sets and object reset 0x1f6 zeroes it |
|
||||
| `0x52bd4` | `surfaces` | `void*` | surface array base [~1000 slots]; create/set-texture (0x1f8/0x1f9) allocate |
|
||||
| `0x53d14` | `cur_ctx_index` | `uint` | current script-context index (curCtx); indexes 0x78-byte coroutine/frame records |
|
||||
| `0x53d28` | `frame_codebase` | `void*` | current frame codebase (PC = codebase + off*4) |
|
||||
|
||||
@@ -498,7 +498,8 @@ independent matrix channels, not two encodings of one vec3 property.
|
||||
at `obj+0x208`, and the target axis-angle matrix at `obj+0x12c`. Current axis/angle are
|
||||
`obj+0x1ec..0x1f4/+0x204`, with current matrix `obj+0xec`.
|
||||
- `gfx_object_apply_transform_channels` (`0x472f00`) supplies the timing contract. All three channels use
|
||||
shared start timestamp `obj+0x34` and global frame-time `ctx+0xb550`, but have independent delay/duration:
|
||||
shared start timestamp `obj+0x34` and retained-gfx frame-time `owner+0xb550`
|
||||
(`EngineCtx+0x51b64`), but have independent delay/duration:
|
||||
scale `obj+0x3c/+0x50`, rotation `obj+0x40/+0x54`, translation `obj+0x44/+0x58`. Each holds current through the
|
||||
delay, linearly interpolates current→target for its duration, then commits the target and clears its own timing.
|
||||
Neither third component is opacity.
|
||||
@@ -556,7 +557,8 @@ both annotated) and grepping the SC0000 opening settles the animation model and
|
||||
real and verifiable on screen.
|
||||
|
||||
**Corrected host model:** `0x21e` scale and `0x220` translation run directly from frame-time
|
||||
`ctx+0xb550` using their own delay/duration; they do not use op `0x238` as their duration.
|
||||
`retained-gfx owner+0xb550` (`EngineCtx+0x51b64`) using their own delay/duration; they do not use op
|
||||
`0x238` as their duration.
|
||||
Op `0x234` is the independent rotation cycle above. Op `0x238` still configures the separate
|
||||
`ctx+0x51b78/+0x51b7c` animation service used by its own family.
|
||||
|
||||
@@ -742,7 +744,8 @@ per-frame bytecode. Reversed + annotated in Ghidra:
|
||||
- **Retained objects carry cyclic rotation state:** flag value `4` at `obj+0`, start timestamp
|
||||
`obj+0x214`, period `obj+0x228`, and axis `obj+0x244/248/24c`.
|
||||
- **`gfx_object_set_rotation_cycle`** (`0x47f060`, worker for legacy op **`0x234`**)
|
||||
configures that channel. `gfx_object_anim_interpolate` consumes it from frame-time `ctx+0xb550`
|
||||
configures that channel. `gfx_object_anim_interpolate` consumes it from retained-gfx frame-time
|
||||
`owner+0xb550` (`EngineCtx+0x51b64`)
|
||||
as a repeating 0..360-degree axis rotation. Op **`0x1fd`**
|
||||
(`gfx_op_0x1fd_set_vec_scaled@0x422650` → `gfx_object_set_scale_current@0x47e6b0`) is an immediate
|
||||
current-scale setter: it divides integer X/Y/Z percentages by 100, marks scale state at `obj+0x68`, and
|
||||
@@ -772,7 +775,8 @@ and snapshotted textures at draw time; symptoms: alternating grey, glow over bac
|
||||
set-texture) resolves `resId` via the SYS4INI resolver (`FUN_0044f390`) and loads the file into the slot's
|
||||
surface **with a colorkey/chromakey** (op arg 3 — never modelled before), also releasing the old surface.
|
||||
A surface persists at its slot until the next set-texture overwrites it.
|
||||
- **Objects** — the `ctx+0x408` registry, keyed by handle (a `std::map`). `gfx_op_0x1fb_draw_bind` (`0x422510`,
|
||||
- **Objects** — the retained-gfx `owner+0x408` registry (`EngineCtx+0x46a1c`), keyed by handle (a
|
||||
`std::map`). `gfx_op_0x1fb_draw_bind` (`0x422510`,
|
||||
op `0x1fb` draw-texture) → `gfx_object_bind_draw` (`0x47e870`): sets the object's **source slot** (`obj+4`),
|
||||
**source rect** (`obj+8..0x14` = left,top,right,bottom), **position** (`obj+0x24/28/2c` = V24), and the
|
||||
**visible** flag (bit 0). The object references its surface **by slot index, live** (re-resolved each frame),
|
||||
@@ -807,7 +811,8 @@ Ghidra functions renamed + plate-commented, saved).
|
||||
is loaded/cached.
|
||||
- **`0x202` (`gfx_op_0x202_worker_set_color_anim` `0x47ea00`)**: sets an **animated** color/alpha target
|
||||
`obj+0x64 = packedARGB`, the color-anim active bit, and resets shared start `obj+0x34=0`. Operands 2/3
|
||||
are delay/duration at `obj+0x38/+0x4c`; sampling uses frame clock `ctx+0xb550`, not op `0x238`.
|
||||
are delay/duration at `obj+0x38/+0x4c`; sampling uses retained-gfx frame clock `owner+0xb550`
|
||||
(`EngineCtx+0x51b64`), not op `0x238`.
|
||||
- **`0x203` (`gfx_op_0x203_worker_set_color` `0x47e9b0`)**: sets a **static** color/alpha `obj+0x60`, no anim
|
||||
bit. Immediate per-object modulation.
|
||||
- **Blit** (`gfx_object_blit_d3d9` `0x4774c0`): selects a **blend mode** (`local_2c`: 0 opaque, 1 alpha
|
||||
@@ -841,10 +846,12 @@ visible glow on the ordinary interactive path. This glow is a retained scaled/ro
|
||||
fade animates so smooth ramping could join slice A. Findings (Ghidra `gfx_object_anim_interpolate`
|
||||
`0x473ed0`, annotated+saved): the bit-2 anim family (op `0x234`) interpolates **5 independent sub-channels**
|
||||
(color `obj+0x240`/period `obj+0x220`, two matrices, rotation `0x168`=360°, src-rect scroll), each on the
|
||||
**global frame clock `ctx+0xb550`** (advanced per present, NOT the op-`0x238` clock `ctx+0x51b7c`), and each
|
||||
**global frame clock `retained-gfx owner+0xb550` (`EngineCtx+0x51b64`)** (advanced per present, NOT the
|
||||
op-`0x238` clock `ctx+0x51b7c`), and each
|
||||
**ping-pongs** (triangle wave, folded at period/2) — i.e. these are *oscillating/pulsing* effects, not
|
||||
one-shot fades. The `0x202`/`0x203` color (`obj+0x60` static / `obj+0x64` animated, sets
|
||||
`ctx+0xb558`/`0xb560`) is a **separate channel whose blit consumer was not located** in this pass. So a
|
||||
`owner+0xb558`/`0xb560` = `EngineCtx+0x51b6c`/`0x51b74`) is a **separate channel whose blit consumer was
|
||||
not located** in this pass. So a
|
||||
one-shot fade's exact source→target→easing is still unresolved and would take a dedicated dig (find the
|
||||
`obj+0x64` consumer + the color→`obj+0x240` path + the clock advance). ⇒ **smooth color-anim interpolation
|
||||
stays deferred**; slice A ships the static end-state (which reaches the correct final alpha/tint and fixes
|
||||
@@ -852,9 +859,11 @@ the stuck-opaque bug), with interpolation as a scoped follow-up.
|
||||
|
||||
**Resolution (2026-07-10 — supersedes the deferral above).** The missing consumer was the bit-1 branch in
|
||||
`gfx_object_apply_transform_channels` (`0x472f00`), before its matrix channels. It seeds shared start
|
||||
`obj+0x34` from `ctx+0xb550`; holds current packed ARGB `obj+0x60` through delay `+0x38`; then performs an
|
||||
`obj+0x34` from retained-gfx `owner+0xb550` (`EngineCtx+0x51b64`); holds current packed ARGB `obj+0x60`
|
||||
through delay `+0x38`; then performs an
|
||||
integer, bytewise linear interpolation to target `+0x64` for duration `+0x4c`. At natural completion—or
|
||||
when `ctx+0xb55c == 1` requests forced completion—the target commits to current, delay/duration clear,
|
||||
when retained-gfx `owner+0xb55c == 1` (`EngineCtx+0x51b70`) requests forced completion—the target commits
|
||||
to current, delay/duration clear,
|
||||
target becomes `0xffffffff`, and the one-shot active bit clears when no color/matrix/src-rect sibling remains.
|
||||
The object-local override bit at `+0x2d0` suppresses the global force. Negative alpha/RGB target operands
|
||||
independently preserve their bytes from current `+0x60`.
|
||||
@@ -1845,6 +1854,51 @@ the saved `/v2` image.
|
||||
|
||||
---
|
||||
|
||||
### Retained graphics lifecycle and render targets -- opcodes `0x1f6` / `0x23d` / `0x20d` / `0x20e` (2026-07-20)
|
||||
|
||||
The initially suspected `0x1f6` / `0x20e` pair is actually two independent lifecycle pairs. Opcode
|
||||
`0x1f6` clears retained object records, while `0x23d` releases transient surface resources. Separately,
|
||||
`0x20d` selects a Direct3D render target and `0x20e` clears the selected target to black. Their frequent
|
||||
adjacency comes from scene setup and teardown, not from one shared service.
|
||||
|
||||
`op_0x1f6_clear_retained_gfx_objects@0x417430` passes the embedded retained-gfx manager at
|
||||
`EngineCtx+0x46614` to `retained_gfx_objects_clear@0x47cab0`. The worker destroys the complete
|
||||
handle-to-object map at manager `+0x408` (`EngineCtx+0x46a1c`), resets its sentinel/count and transient
|
||||
dirty/force-completion/animation-clock state, but leaves the surface table and queued surface commands
|
||||
intact. The port consequently clears `GfxState` objects without releasing their source slots, allowing a
|
||||
later bind to recreate an object from an existing surface.
|
||||
|
||||
`op_0x23d_release_transient_surfaces@0x4175c0` loops over slots 42 through 999. For each slot it stops and
|
||||
releases any movie-to-texture object at `EngineCtx+0x52bd4[slot]`, then calls
|
||||
`retained_gfx_release_surface@0x474e40`. Slots 0 through 41 are deliberately preserved. The native release
|
||||
worker can also retain a slot protected by its per-slot ownership guard; the port has no corresponding
|
||||
external owner and releases the complete transient range. In the corpus, 146 of 149 `0x23d` calls directly
|
||||
follow `0x1f6`, forming the full object-plus-resource reset.
|
||||
|
||||
`op_0x20d_select_render_target@0x422e10` forwards its operand to
|
||||
`retained_gfx_select_render_target@0x479660`. Values below 1000 resolve a retained texture wrapper, acquire
|
||||
texture level zero, and call `IDirect3DDevice9::SetRenderTarget(0, surface)`. Values at or above 1000 acquire
|
||||
backbuffer zero instead and record current target `-1` at manager `+0xb530` (`EngineCtx+0x51b44`). Opcode
|
||||
`0x20e` then calls `d3d_clear_render_target_black@0x471460`, which invokes `IDirect3DDevice9::Clear` with no
|
||||
rectangles, `D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER`, color zero, depth 1.0, and stencil zero. The port tracks
|
||||
the selected target and forwards the clear to the host; the retained compositor reconstructs its main
|
||||
backbuffer from black at publication boundaries, while offscreen clears also discard modeled text pixels.
|
||||
|
||||
This trace also corrects an important base-pointer assumption in earlier graphics notes. Offsets `+0x408`
|
||||
and `+0xb550` are relative to the retained-gfx manager at `EngineCtx+0x46614`, not to `EngineCtx` itself.
|
||||
Their absolute locations are therefore `EngineCtx+0x46a1c` (object registry) and `EngineCtx+0x51b64`
|
||||
(frame timer). The same correction moves the dirty/force-completion fields to `EngineCtx+0x51b6c` through
|
||||
`+0x51b74`. This changes native field provenance and the Ghidra structure, but not the already implemented
|
||||
host-side object/animation behavior, which was based on worker semantics and runtime traces rather than
|
||||
directly reading those native addresses.
|
||||
|
||||
Corpus totals are 293 `0x1f6` calls in 146 scripts, 149 `0x23d` calls in 146 scripts, 113 `0x20d` calls in
|
||||
24 scripts, and 345 `0x20e` calls in 167 scripts. In SC0000, the full reset appears as `0x1f6` then `0x23d`
|
||||
at `0x4ed`; other paths use `0x1f6` then `0x20e` to discard retained objects and clear the already selected
|
||||
backbuffer. Fresh offscreen surfaces commonly use `0x20d` then `0x20e` before drawing.
|
||||
|
||||
---
|
||||
|
||||
## Native walls backlog (targets for this loop)
|
||||
|
||||
- ~~**call-script dispatch**~~ — **SOLVED** (above): `call-script <id>` = raw SYS4INI file index.
|
||||
|
||||
@@ -329,6 +329,13 @@ SC0000 label_1235a reaches this when 0x1c7/0x1cc are zero. Native run-state bit
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0x131_handler@0x4295e0 calls the settings getter with `message:MesWinAlpha` and writes the result. HISTORY.BIN and the shared ADV redraw path compute (16-value)<<4 for the control-strip alpha.
|
||||
|
||||
### 0x1f6 `clear-retained-gfx-objects` (clear-retained-gfx-objects, argc 0)
|
||||
- **summary:** Clear the complete retained gfx-object registry while preserving allocated surface resources. Subsequent object queries return absent until draw/geometry operations recreate records.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0x1f6_clear_retained_gfx_objects@0x417430 passes ctx+0x46614 to retained_gfx_objects_clear@0x47cab0. The worker destroys every node in owner+0x408, resets its sentinel/count, and clears owner+0xb558/+0xb55c/+0xb564/+0xb568 plus bit 0x10000 state. DATA1: 293 calls in 146 scripts; 146 continue to 0x23d and 143 to 0x20e.
|
||||
|
||||
The handler clears the map embedded at retained-gfx owner+0x408, resets its count and transient dirty/force flags, but does not release the 1000 surface slots or clear the separate queued-surface-command list. Corpus call sites pair it either with 0x23d for a full transient-resource reset or with 0x20e for a backbuffer clear.
|
||||
|
||||
### 0x1f7 `gfx-elem-erase` (gfx-elem-erase, argc 2)
|
||||
- **summary:** 0x1f7 (handle)(count) — erase retained gfx objects. Handler 0x422270 calls gfx_object_erase_range@0x47d8b0 for [handle,handle+count) when count>1, else gfx_object_erase@0x47d850. This removes entries from the same object map queried by op 0x215, so erased objects stop compositing. SC0000 uses it before op 0x1fa releases the returned surface slot.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
@@ -365,9 +372,9 @@ SC0000 label_1235a reaches this when 0x1c7/0x1cc are zero. Native run-state bit
|
||||
- **evidence:** Ghidra /v2: handler gfx_op_0x1ff_set_geom3@0x4227b0 converts operands 2..4 to float and calls gfx_object_set_translation_current@0x47e800. The worker gets/creates the object, marks obj+0x168, and calls matrix4_make_translation on obj+0x16c. SC0000 uses (0,0,0) before animating BG001A. Annotated and saved 2026-07-11.
|
||||
|
||||
### 0x202 `gfx-blit-color` (gfx-blit-color, argc 5)
|
||||
- **summary:** 0x202 (handle)(delay_ms)(duration_ms)(alpha)(color) — arm the one-shot packed-ARGB channel. Worker gfx_op_0x202_worker_set_color_anim @0x47ea00 resets shared start obj+0x34, writes delay +0x38, duration +0x4c, and target +0x64. gfx_object_apply_transform_channels @0x472f00 linearly interpolates each byte from current +0x60 on frame clock ctx+0xb550, commits the target, clears timing, writes target -1, and clears the one-shot active bit when all sibling channels finish. Negative alpha/RGB independently preserve current bytes. Implemented in GfxState with synchronized current/target timeline evidence; draw-string 0x204/0x7a remains separate.
|
||||
- **summary:** 0x202 (handle)(delay_ms)(duration_ms)(alpha)(color) — arm the one-shot packed-ARGB channel. Worker gfx_op_0x202_worker_set_color_anim @0x47ea00 resets shared start obj+0x34, writes delay +0x38, duration +0x4c, and target +0x64. gfx_object_apply_transform_channels @0x472f00 linearly interpolates each byte from current +0x60 on retained-gfx owner+0xb550 (EngineCtx+0x51b64), commits the target, clears timing, writes target -1, and clears the one-shot active bit when all sibling channels finish. Negative alpha/RGB independently preserve current bytes. Implemented in GfxState with synchronized current/target timeline evidence; draw-string 0x204/0x7a remains separate.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra handler 0x4228d0 packs operands 4/5 and calls worker 0x47ea00(handle,delay,duration,packed). Consumer 0x472f00: shared start +0x34; color delay/duration +0x38/+0x4c; current/target +0x60/+0x64; frame clock ctx+0xb550; bytewise integer LERP; natural or ctx+0xb55c forced completion. /v2 annotated and saved 2026-07-10.
|
||||
- **evidence:** Ghidra handler 0x4228d0 packs operands 4/5 and calls worker 0x47ea00(handle,delay,duration,packed). Consumer 0x472f00: shared start +0x34; color delay/duration +0x38/+0x4c; current/target +0x60/+0x64; frame clock retained-gfx owner+0xb550 (EngineCtx+0x51b64); bytewise integer LERP; natural or owner+0xb55c (EngineCtx+0x51b70) forced completion. /v2 annotated and saved 2026-07-10.
|
||||
|
||||
### 0x203 `gfx-draw-color` (gfx-draw-color, argc 4)
|
||||
- **summary:** 0x203 (handle)(mode)(alpha)(color) — worker stores the D3D blend selector at obj+0x30 and STATIC packed color at obj+0x60; the handler's ctx+0x53d88 write is the generic 9-dword instruction length. Negative alpha/RGB preserve current static bytes. Mode 0 is the opaque textured path: preserved 0xffffffff is identity (the alpha byte is not tint strength); mode 1 is SRCALPHA/INVSRCALPHA with ARGB alpha opacity and multiplicative RGB modulation; mode 2 is the 0x223 transition-source identity path. Surfaceless mode-0 fill consumption remains a distinct case.
|
||||
@@ -393,6 +400,21 @@ SC0000 label_1235a reaches this when 0x1c7/0x1cc are zero. Native run-state bit
|
||||
|
||||
Native handler gfx_op_0x20c_present_frame -> gfx_render_frame @0x4820b0. This is an explicit retained-state publication boundary, not a continuously visible object-store mutation. The read/message-skip branch resets the animation service then presents; the port publishes and snaps pending 0x223 state here. Normal playback branches to 0x21c, which owns repeated render/wait/resume. Headless hosts remain non-blocking.
|
||||
|
||||
### 0x20d `select-render-target` (select-render-target, argc 1)
|
||||
- **summary:** Select an offscreen surface slot as Direct3D render target 0, or restore the device backbuffer when the operand is at least 1000.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **depends on:** 0x20c, 0x1c7, 0x1cc
|
||||
- **evidence:** Ghidra /v2: op_0x20d_select_render_target@0x422e10 passes operand 1 and retained-gfx owner ctx+0x46614 to retained_gfx_select_render_target@0x479660. Its D3D calls resolve texture level 0 or backbuffer 0, then invoke device vtable +0x94 SetRenderTarget and store selected slot at owner+0xb530. DATA1: 113 calls in 24 scripts.
|
||||
|
||||
For slots below 1000 the native worker obtains that surface's level-0 D3D texture surface and calls IDirect3DDevice9::SetRenderTarget(0,...). The >=1000 path obtains backbuffer 0 and records current target -1. Himegari commonly follows a freshly created blank surface selection with 0x20e before drawing into it.
|
||||
|
||||
### 0x20e `clear-render-target` (clear-render-target, argc 0)
|
||||
- **summary:** Clear the currently selected render target to black and reset its depth buffer to 1.0.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0x20e_clear_render_target@0x4174f0 passes renderer ctx+0x46188 to d3d_clear_render_target_black@0x471460. The worker calls device pointer +0x410, vtable +0xac with (0,NULL,3,0,1.0f,0). DATA1: 345 calls in 167 scripts; 57 directly follow 0x20d and 143 directly follow 0x1f6.
|
||||
|
||||
This is a target-pixel operation, not retained-object teardown. It invokes IDirect3DDevice9::Clear with zero rectangles, flags D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER (3), color 0, depth 1.0, and stencil 0. The port tracks 0x20d target ownership and forwards the clear to the host; its retained compositor already rebuilds the backbuffer from black.
|
||||
|
||||
### 0x212 `set-gfx-field64` (set-gfx-field64, argc 2)
|
||||
- **summary:** 0x212 (obj_idx)(val) — handler gfx_op_0x212_set_field64 @0x4230c0: obj=[ctx+0x14d54 + obj_idx*4]; if obj: *(obj+0x64)=val. The generic instruction length is 5 dwords. See docs/engine-re.md gfx op-contract table.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
@@ -437,12 +459,12 @@ Native handler gfx_op_0x20c_present_frame -> gfx_render_frame @0x4820b0. This is
|
||||
- **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.
|
||||
- **evidence:** Ghidra handler 0x423310 -> gfx_object_clone@0x47e4f0. Worker requires source in retained-gfx owner+0x408 (EngineCtx+0x46a1c), creates destination, copies exactly 0xb5 dwords from source object record, then marks owner+0xb558 (EngineCtx+0x51b6c) 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.
|
||||
- **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 retained-gfx frame time owner+0xb550 (EngineCtx+0x51b64), 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
|
||||
- **evidence:** Ghidra 0x47eaa0 calls matrix builder 0x48af1d for target obj+0xac. Consumer 0x472f00 uses delay obj+0x3c, duration obj+0x50, current obj+0x6c, target obj+0xac, shared start obj+0x34, and frame-time ctx+0xb550.
|
||||
- **evidence:** Ghidra 0x47eaa0 calls matrix builder 0x48af1d for target obj+0xac. Consumer 0x472f00 uses delay obj+0x3c, duration obj+0x50, current obj+0x6c, target obj+0xac, shared start obj+0x34, and retained-gfx frame time owner+0xb550 (EngineCtx+0x51b64).
|
||||
|
||||
### 0x21f `set-anim-rotation-axis-angle` (set-anim-rotation-axis-angle, argc 7)
|
||||
- **summary:** (handle)(delay_ms)(duration_ms)(axis_x)(axis_y)(axis_z)(angle_deg) — set the delayed one-shot axis-angle rotation channel. Handler converts axis/angle integers to floats; worker stores target axis obj+0x1f8 and angle obj+0x208 and builds target matrix obj+0x12c. gfx_object_apply_transform_channels samples current axis/angle linearly on shared start obj+0x34 and composes T(-anchor)*scale*rotation*translation*T(anchor).
|
||||
@@ -450,9 +472,9 @@ Native handler gfx_op_0x20c_present_frame -> gfx_render_frame @0x4820b0. This is
|
||||
- **evidence:** Ghidra handler 0x423410 -> gfx_object_set_rotation_channel@0x47eb70; consumer gfx_object_apply_transform_channels@0x472f00 uses delay +0x40, duration +0x54, current axis +0x1ec/angle +0x204, target axis +0x1f8/angle +0x208, current matrix +0xec and target +0x12c. Native SC0000 handle 0xcb8e sample at 11/390 of axis (0,0,1), 30deg matches matrix [0.9055,0.0134;-0.0134,0.9055] and translation (74.1449,47.3127).
|
||||
|
||||
### 0x220 `set-anim-transform-abs` (set-anim-transform-abs, argc 6)
|
||||
- **summary:** (handle)(delay_ms)(duration_ms)(tx)(ty)(tz) — set the absolute TRANSLATION-matrix channel. Target obj+0x1ac is linearly sampled from current obj+0x16c by gfx_object_apply_transform_channels@0x472f00 on frame-time ctx+0xb550, after delay and for duration, then committed. Independent of op 0x21e scale; neither Z is opacity.
|
||||
- **summary:** (handle)(delay_ms)(duration_ms)(tx)(ty)(tz) — set the absolute TRANSLATION-matrix channel. Target obj+0x1ac is linearly sampled from current obj+0x16c by gfx_object_apply_transform_channels@0x472f00 on retained-gfx frame time owner+0xb550 (EngineCtx+0x51b64), after delay and for duration, then committed. Independent of op 0x21e scale; neither Z is opacity.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra 0x47ecc0 calls matrix builder 0x48afb1 for target obj+0x1ac. Consumer 0x472f00 uses delay obj+0x44, duration obj+0x58, current obj+0x16c, target obj+0x1ac, shared start obj+0x34, and frame-time ctx+0xb550.
|
||||
- **evidence:** Ghidra 0x47ecc0 calls matrix builder 0x48afb1 for target obj+0x1ac. Consumer 0x472f00 uses delay obj+0x44, duration obj+0x58, current obj+0x16c, target obj+0x1ac, shared start obj+0x34, and retained-gfx frame time owner+0xb550 (EngineCtx+0x51b64).
|
||||
|
||||
### 0x222 `present-gfx-object-range` (present-gfx-object-range, argc 2)
|
||||
- **summary:** (first_handle)(count) - flush/present retained graphics objects in the selected handle range and clear their pending update flags.
|
||||
@@ -464,7 +486,7 @@ Native handler gfx_op_0x20c_present_frame -> gfx_render_frame @0x4820b0. This is
|
||||
- **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.
|
||||
- **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 retained-gfx owner+0xb550 (EngineCtx+0x51b64) 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)
|
||||
- **summary:** Clear the native gfx command queue rooted at ctx+0x418. Host-implicit because the port composites retained state directly.
|
||||
@@ -497,7 +519,7 @@ Native handler gfx_op_0x20c_present_frame -> gfx_render_frame @0x4820b0. This is
|
||||
### 0x234 `anim-start` (anim-start, argc 5)
|
||||
- **summary:** (handle)(period_ms)(axis_x)(axis_y)(axis_z) — configure cyclic rotation. Worker stores period obj+0x228, start obj+0x214=0, and float axis obj+0x244; each frame uses integer degrees floor(((now-start)%period)*360/period). gfx_object_composite right-multiplies this separately anchored transform after the one-shot scale/rotation/translation product, so cyclic rotation also rotates the translation vector.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra handler 0x423da0 converts axis ints to floats -> worker 0x47f060. gfx_object_anim_interpolate@0x473ed0 consumes obj+0x228/+0x214/+0x244 on ctx+0xb550 and matrix4_make_axis_angle@0x48b215. gfx_object_composite@0x47f650 calls one-shot transform first, cyclic animation second.
|
||||
- **evidence:** Ghidra handler 0x423da0 converts axis ints to floats -> worker 0x47f060. gfx_object_anim_interpolate@0x473ed0 consumes obj+0x228/+0x214/+0x244 on retained-gfx owner+0xb550 (EngineCtx+0x51b64) and matrix4_make_axis_angle@0x48b215. gfx_object_composite@0x47f650 calls one-shot transform first, cyclic animation second.
|
||||
|
||||
### 0x236 `play-movie-to-surface` (play-movie-to-surface, argc 4)
|
||||
- **summary:** (resource_id)(surface_slot)(movie_flags)(sync_mask) - synchronously resolve/open an archive movie and construct its DirectShow graph, then start asynchronous frame delivery into the retained destination surface. The opcode itself is non-blocking: the VM advances to the next instruction. SC0000's native site evaluates (0x33, 0, 2, 0) at 0x13c8 and resumes at bytecode 0x13d1; its later 0x21c service boundary yields until movie EOF before cleanup.
|
||||
@@ -515,6 +537,13 @@ The handler requires an existing destination texture, allocates/reuses a 0x478-b
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Native /v2 worker and gfx_object_apply_transform_channels decompile. The consumer advances target_frame cells over duration after delay, preserves the existing source-rect dimensions, and commits the endpoint.
|
||||
|
||||
### 0x23d `release-transient-surfaces` (release-transient-surfaces, argc 0)
|
||||
- **summary:** Stop movie bindings and release transient gfx surface slots 42 through 999 inclusive, preserving system-owned slots 0 through 41.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0x23d_release_transient_surfaces@0x4175c0 loops EDI=42 while EDI<1000, stops/releases ctx+0x52bd4[EDI] movie objects, and calls retained_gfx_release_surface@0x474e40 on owner ctx+0x46614. DATA1: 149 calls in 146 scripts; 146 directly follow 0x1f6.
|
||||
|
||||
For each fixed slot in [42,1000), the handler stops/releases the movie-to-texture object at ctx+0x52bd4[slot], then invokes the ordinary retained-gfx surface-release worker. Protected/externally owned slots may be retained by the worker's per-slot guard. This is the resource half of the common 0x1f6/0x23d full-reset sequence.
|
||||
|
||||
### 0x23f `u00422930` (u00422930, argc 2)
|
||||
- **summary:** 0x23f query-object (out)(handle): return object status (FUN_0042a520; -1 if none). C# VM: 0 if the object exists else -1. See docs/engine-re.md §SC0000 anim cluster.
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
@@ -1094,10 +1123,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=med
|
||||
|
||||
### 0x1f6 `u00416170` (u00416170, argc 0)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0x1fe `u004206C0` (u004206C0, argc 5)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
@@ -1110,15 +1135,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
|
||||
|
||||
### 0x20d `u00420E10` (u00420E10, argc 1)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
- **depends on:** 0x20c, 0x1c7, 0x1cc
|
||||
|
||||
### 0x20e `u00416250` (u00416250, argc 0)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0x20f `u00420E40` (u00420E40, argc 3)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
@@ -1155,10 +1171,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
|
||||
|
||||
### 0x23d `u004162F0` (u004162F0, argc 0)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0x241 `u00422B80` (u00422B80, argc 5)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
@@ -954,10 +954,12 @@ The plain check reports only the generated reference's CRLF on its newly added r
|
||||
**Native contract.** `gfx_op_0x202_worker_set_color_anim@0x47ea00` writes target packed ARGB at
|
||||
`obj+0x64`, delay/duration at `+0x38/+0x4c`, and resets the one-shot family's shared start `+0x34`.
|
||||
The missing consumer is the bit-1 path in `gfx_object_apply_transform_channels@0x472f00`: it seeds start
|
||||
from frame time `ctx+0xb550`, performs an integer bytewise current-`+0x60` to target-`+0x64` LERP, then
|
||||
from frame time `retained-gfx owner+0xb550` (`EngineCtx+0x51b64`), performs an integer bytewise
|
||||
current-`+0x60` to target-`+0x64` LERP, then
|
||||
commits target, clears timing, writes target `-1`, and clears the active bit once all sibling one-shot
|
||||
channels finish. This is ordinary presentation-clock coupling, not op `0x238`'s separate service clock.
|
||||
`ctx+0xb55c == 1` forces completion unless the object-local `+0x2d0` override bit is set. `/v2` comments
|
||||
`retained-gfx owner+0xb55c == 1` (`EngineCtx+0x51b70`) forces completion unless the object-local
|
||||
`+0x2d0` override bit is set. `/v2` comments
|
||||
were corrected and saved.
|
||||
|
||||
**Port and endpoint diagnosis.** `GfxState` now retains current/target/delay/duration separately, shares
|
||||
@@ -2041,3 +2043,36 @@ largest repeated non-persistence SC0000 cluster.
|
||||
|
||||
Validation: all 206 engine tests pass; opcode and EngineCtx tests/lints, vm0 RECOVER, and `git diff --check`
|
||||
are clean.
|
||||
|
||||
### Slice A2b retained-graphics lifecycle implemented (2026-07-20)
|
||||
|
||||
Native RE split the apparent `0x1f6` / `0x20e` pair into two separate services. `0x1f6` clears the retained
|
||||
handle-to-object registry without releasing surface resources, and the commonly adjacent `0x23d` stops
|
||||
movie bindings and releases transient surface slots 42 through 999. Independently, `0x20d(slot)` selects
|
||||
an offscreen Direct3D render target (or the backbuffer for values at least 1000), and `0x20e` clears the
|
||||
selected color target to black plus its depth buffer to 1.0.
|
||||
|
||||
The VM now models all four operations generically. `GfxState` owns the selected target and distinct
|
||||
object/surface lifecycle; the host receives target clears and fixed-range surface release. Godot removes
|
||||
modeled offscreen text pixels on clear, stops movies and clears slot-owned resources on the bulk release,
|
||||
and continues to rebuild the main retained frame from black. Focused regressions prove object clear
|
||||
preserves surfaces for rebinding, target selection reaches both an offscreen slot and the backbuffer, and
|
||||
the bulk release preserves system-owned low slots while dropping slot 42.
|
||||
|
||||
The trace corrected an older EngineCtx-base assumption: object registry `+0x408`, frame timer `+0xb550`,
|
||||
and nearby animation flags are relative to the embedded retained-gfx manager at `EngineCtx+0x46614`.
|
||||
Their actual absolute addresses are `EngineCtx+0x46a1c` and `EngineCtx+0x51b64` onward. Existing visible
|
||||
graphics and animation implementations remain valid because they model the recovered worker behavior and
|
||||
observed timing rather than reading native process addresses.
|
||||
|
||||
The canonical opcode and EngineCtx sources are regenerated. `/v2` names/comments the four handlers and
|
||||
five supporting workers, and the corrected structure is reapplied before saving. SC0000 is now **121/129
|
||||
distinct opcodes handled (93.8%)** and **16,037/16,257 instructions handled or safe-noop (98.6%)**; 206 of
|
||||
the 220 remaining effectful-gap instructions are the deliberately deferred `0x1a2` shared-profile writes.
|
||||
|
||||
**Next:** investigate/implement the two-call `0x242` retained-object field setter. It is already classified
|
||||
as an object `+0x2d0` write and is the smallest coherent non-persistence gap; keep `0x1a2`/`0x1a3`, `0x1ad`,
|
||||
and `0x1cb` behind the future profile/save ownership decision.
|
||||
|
||||
Validation: all 209 engine tests pass; opcode and EngineCtx tests/lints, vm0 RECOVER, the Godot build,
|
||||
threaded self-test, and `git diff --check` are clean.
|
||||
|
||||
Reference in New Issue
Block a user