Implement detached object animation control

This commit is contained in:
gamer147
2026-07-20 14:53:34 -04:00
parent d740c27b4a
commit 6aede1e525
10 changed files with 231 additions and 29 deletions

View File

@@ -25,11 +25,12 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch-
| `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 |
| `0x51b6c` | `gfx_dirty_a` | `int` | retained_gfx_manager+0xb558 redraw-dirty flag; active finite object channels set it even when detached by 0x242; 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 |
| `0x51b74` | `gfx_dirty_b` | `int` | retained_gfx_manager+0xb560 blocking-presentation dirty flag; active finite object channels set it unless obj+0x2d0 bit 0 detaches them |
| `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 |
| `0x51b80` | `gfx_animation_service_flags` | `uint` | retained_gfx_manager+0xb56c service flags; bit 1 suppresses op 0x243 force-complete/clock-reset request |
| `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) |

View File

@@ -439,8 +439,8 @@ The preceding query-gated cleanup had skipped this created-but-unbound object be
`SourceSlot=-1`. Native `gfx_object_init_default` zeroes `obj+4`, so op `0x215` returns slot 0, the cleanup
erases the object, and the later bind recreates identity state. Matching that default leaves EV050EA centered,
unrotated, and opaque in the synchronized page-58 compositor trace. The adjacent op `0x242(handle,0)` is not
a reset: `op_0x242_set_object_field2d0@0x4249d0` calls `gfx_object_set_field2d0@0x47f1a0`, which only writes
the supplied value at `obj+0x2d0`.
a reset: it clears the object's detached-animation control word after binding. Its consumer and complete
lifecycle are documented under “Detached finite object animation” below.
**Page-89 surface-lifetime correction (2026-07-11).** Locator `SC0000 P089` resolves to
`wait@0x89e9`; the loader at `0x8976..0x898a` resolves resource `0x6a` to `BG004D.AGF`. The background
@@ -865,8 +865,9 @@ integer, bytewise linear interpolation to target `+0x64` for duration `+0x4c`. A
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`.
The object-local bit at `+0x2d0` suppresses the global force and keeps that finite group from raising the
blocking-presentation dirty flag; it remains redraw-active and therefore animates asynchronously. Negative
alpha/RGB target operands independently preserve their bytes from current `+0x60`.
The port now carries current and target separately and samples them from the unified `FrameClock`; an op
`0x203` static write after `0x202` therefore becomes the ramp's current value rather than overwriting its
@@ -975,8 +976,9 @@ negative operands in `SetColorAnimResolved`, samples packed ARGB before blend se
the existing mode-specific path. Exact AE001H, mode-0 RGB-modulation, and mode-1 alpha regressions cover the
contract; the white pulse is removed without suppressing the scripted channel.
**Resolved 2026-07-11:** `0x236` is the movie-to-retained-surface path described below. `0x242` is now
classified as the retained-object `+0x2d0` field setter described above; `0x23d/0x20a/0x20e` remain GAP.
**Resolved 2026-07-20:** `0x236` is the movie-to-retained-surface path described below. `0x242` is the
detached finite-animation control described below; `0x23d`, `0x20a`, and `0x20e` are also implemented in
their later lifecycle/presentation slices.
### Movie-to-surface opcode `0x236` (2026-07-11)
@@ -1899,6 +1901,40 @@ backbuffer. Fresh offscreen surfaces commonly use `0x20d` then `0x20e` before dr
---
### Detached finite object animation -- opcodes `0x242` / `0x243` (2026-07-20)
`op_0x242_set_object_animation_detached@0x4249d0` fetches `(handle, flags)` and calls
`gfx_object_set_animation_control@0x47f1a0`, which get-or-creates the retained object and replaces its
32-bit control word at `obj+0x2d0`. Only bit 0 has a located consumer. It does not reset geometry, color,
or matrix state.
`gfx_object_apply_transform_channels@0x472f00` gives bit 0 two connected effects on the finite one-shot
group (packed color, scale, rotation, translation, and timed source rectangle):
- Manager `+0xb55c` value 1 normally forces every finite channel to its endpoint. Bit 0 masks that request
for this object.
- An unfinished ordinary object raises manager `+0xb560`, keeping blocking presentation active. A bit-0
object does not raise that flag, but still raises redraw-dirty `+0xb558`, so it continues animating while
script execution proceeds.
When no finite channel remains, the consumer clears bit 0 and the shared start timestamp. The control is
therefore a per-animation detachment flag, not a persistent object mode. Object initialization sets the
word to zero, and native clone copies it with the rest of the complete object record.
`op_0x243_force_complete_and_reset_anim_clock@0x4182d0` supplies the paired global operation. When service
flags manager `+0xb56c` bit 1 is clear, it sets force-complete `+0xb55c` (`EngineCtx+0x51b70`) to 1 and
zeros the separate animation-service elapsed/duration fields at `+0xb564/+0xb568`. The next composition
commits every ordinary finite group; detached objects ignore the request. The port performs that observable
commit directly when executing `0x243`, excludes detached groups from its blocking wait predicate, and
continues including them in its visual recomposition predicate until natural completion.
Corpus evidence is unusually sharp: all 303 calls pass an immediate flag, with zero used 302 times and one
used once. SC0000's two CG-loader sites (`0x12723`, `0x13310`) write zero. `BTL.BIN@0x2b4d` writes one on an
animated battle object after its texture/movie or sprite-cell setup, matching the nonblocking background
animation contract. No boot seed, script-offset branch, or persistence state is involved.
---
## Native walls backlog (targets for this loop)
- ~~**call-script dispatch**~~ — **SOLVED** (above): `call-script <id>` = raw SYS4INI file index.

View File

@@ -548,15 +548,19 @@ For each fixed slot in [42,1000), the handler stops/releases the movie-to-textur
- **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
### 0x242 `set-gfx-field2d0` (set-gfx-field2d0, argc 2)
- **summary:** 0x242 (handle)(value) — get-or-create the retained gfx object and write value to obj+0x2d0; the generic instruction length is 5 dwords. SC0000's common CG loader passes zero after draw binding. This field does not reset transform or color channels; its downstream purpose remains unknown.
### 0x242 `set-object-animation-detached` (set-object-animation-detached, argc 2)
- **summary:** Replace the retained object's animation-control word at obj+0x2d0. Bit 0 detaches finite one-shot channels from blocking presentation and protects them from 0x243 forced completion until they finish naturally.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x242_set_object_field2d0@0x4249d0 fetches operands 2 and 1 and calls gfx_object_set_field2d0@0x47f1a0; the worker calls gfx_object_get_or_create then stores operand 2 at returned object+0x2d0.
- **depended on by:** 0x243
- **evidence:** Ghidra /v2: op_0x242_set_object_animation_detached@0x4249d0 calls gfx_object_set_animation_control@0x47f1a0. Consumer gfx_object_apply_transform_channels@0x472f00 tests obj+0x2d0 bit 0 to suppress manager+0xb55c forced completion and manager+0xb560 blocking-dirty publication, while still setting manager+0xb558 redraw dirty; it clears bit 0 when the finite group completes. DATA1: 303 calls; values zero x302, one x1.
The setter get-or-creates the object and writes the complete operand. During retained composition, bit 0 makes the one-shot color/scale/rotation/translation/source-rectangle group ignore the manager's force-complete request and suppresses the blocking-dirty flag while leaving the redraw-dirty flag active. When no finite channel remains, native clears bit 0 and the shared start time. SC0000's two common CG loaders explicitly write zero. Across DATA1, 302 calls write zero and the sole nonzero call is BTL.BIN's animated battle-object setup, which writes one.
### 0x243 `reset-anim-clock` (reset-anim-clock, argc 0)
- **summary:** Reset the native global animation-service elapsed and duration fields to zero when service flag bit 1 is clear.
- **summary:** When animation-service flag bit 1 is clear, request forced completion of ordinary finite one-shot object channels and reset the global animation-service elapsed/duration fields. Objects detached by 0x242 bit 0 ignore the force request.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra handler 0x4182d0: if !(ctx+0x51b80 & 2), set ctx+0x51b70=1 and zero ctx+0x51b78/+0x51b7c. Normal SC0000 label_1235a calls it before present-frame.
- **depends on:** 0x242
- **evidence:** Ghidra handler 0x4182d0: if !(ctx+0x51b80 & 2), set retained-gfx owner+0xb55c (EngineCtx+0x51b70)=1 and zero owner+0xb564/+0xb568. gfx_object_apply_transform_channels treats force value 1 as immediate completion unless obj+0x2d0 bit 0 is set. SC0000 label_1235a calls it before present-frame.
## input

View File

@@ -2076,3 +2076,31 @@ 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.
### Slice A2b detached finite object animation implemented (2026-07-20)
The `obj+0x2d0` consumer resolves `0x242(handle,flags)` as a finite-animation detachment control. Bit 0
makes that object's one-shot color/scale/rotation/translation/source-rectangle group ignore `0x243`'s
global force-complete request and prevents it from holding native blocking-presentation dirty state, while
leaving redraw dirty so it continues animating asynchronously. Native clears bit 0 after the finite group
finishes. SC0000's two CG loaders pass zero; 302 of 303 corpus calls do the same. The only value-one use is
an animated battle object in `BTL.BIN`.
`GfxState` now retains and clones the control word, excludes detached channels from blocking waits without
excluding them from visual recomposition, protects them from `0x243`, and clears bit 0 on natural completion.
The `0x243` model is correspondingly completed: it commits every unprotected finite channel as well as
resetting the separate global animation clock. Focused regressions cover get-or-create/clone behavior and
the protected-versus-ordinary completion split.
The canonical opcode source is refined and regenerated. EngineCtx adds the animation-service flags at
`0x51b80` and refines the two dirty-field roles; the expanded 76-field structure is reapplied to `/v2`.
The `0x242` handler/worker, `0x243`, and the shared channel consumer are named/commented and saved. SC0000
is now **122/129 distinct opcodes handled (94.6%)** and **16,039/16,257 instructions handled or safe-noop
(98.7%)**. Its remaining seven gaps total 218 instructions, 206 of them the deferred shared-profile write.
**Next:** investigate the four adjacent one-call unknowns `0x19b`, `0x19c`, `0x1ca`, and `0xae` together
before choosing an implementation. Their low frequency makes a shared native-service relationship more
important than any one opcode's count; persistence-bound `0x1a2`, `0x1ad`, and `0x1cb` remain deferred.
Validation: all 211 engine tests pass; opcode and EngineCtx tests/lints, vm0 RECOVER, the zero-warning
Godot build, threaded self-test, and `git diff --check` are clean.