From d1af8e3b07a8840c3b1ecbda861eac032ba46a44 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Mon, 20 Jul 2026 14:53:34 -0400 Subject: [PATCH] Implement detached object animation control --- docs/engine-ctx-reference.md | 5 +- docs/engine-re.md | 48 +++++++++++-- docs/opcode-reference.md | 14 ++-- docs/phase-a-slice-plan.md | 28 ++++++++ .../GfxAnimationDetachTests.cs | 61 ++++++++++++++++ engine/Age.Engine.Tests/GfxAnimationTests.cs | 2 +- engine/Age.Engine/Model/GfxState.cs | 72 +++++++++++++++++-- engine/Age.Engine/Vm/VirtualMachine.cs | 4 +- vm-map/engine-ctx.toml | 9 ++- vm-map/opcodes.toml | 17 ++--- 10 files changed, 231 insertions(+), 29 deletions(-) create mode 100644 engine/Age.Engine.Tests/GfxAnimationDetachTests.cs diff --git a/docs/engine-ctx-reference.md b/docs/engine-ctx-reference.md index 9503fad..e2f3bc6 100644 --- a/docs/engine-ctx-reference.md +++ b/docs/engine-ctx-reference.md @@ -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) | diff --git a/docs/engine-re.md b/docs/engine-re.md index 9b1a8ca..1690214 100644 --- a/docs/engine-re.md +++ b/docs/engine-re.md @@ -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 ` = raw SYS4INI file index. diff --git a/docs/opcode-reference.md b/docs/opcode-reference.md index a536fd4..b7a8027 100644 --- a/docs/opcode-reference.md +++ b/docs/opcode-reference.md @@ -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 diff --git a/docs/phase-a-slice-plan.md b/docs/phase-a-slice-plan.md index f6f8fc9..b5dd7e7 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -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. diff --git a/engine/Age.Engine.Tests/GfxAnimationDetachTests.cs b/engine/Age.Engine.Tests/GfxAnimationDetachTests.cs new file mode 100644 index 0000000..bc3b6bb --- /dev/null +++ b/engine/Age.Engine.Tests/GfxAnimationDetachTests.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using Age.Engine.Model; +using Age.Engine.Sys4; +using Age.Engine.Vm; +using Xunit; + +namespace Age.Engine.Tests; + +public class GfxAnimationDetachTests +{ + 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); + private static (int, Operand[]) Mov(int address, long value) => (0x55, new[] { G(address), I(value) }); + private static (int, Operand[]) Exit() => (0x2, System.Array.Empty()); + + [Fact] + public void Opcode242SetsControlWordOnGetOrCreateObjectAndCloneCopiesIt() + { + var table = T(); + var scene = ScriptAssembler.Assemble(table, "ANIM-DETACH", new List<(int, Operand[])> + { + Mov(1, 0x100), Mov(2, 0x101), + (0x242, new[] { G(1), I(1) }), + (0x21d, new[] { G(1), G(2) }), + Exit(), + }, System.Array.Empty()); + + var vm = new VirtualMachine(scene, table, new RecordingHost()); + vm.Run(); + + Assert.Equal(1, vm.Gfx.TryGet(0x100)!.OneShotAnimationControlFlags); + Assert.Equal(1, vm.Gfx.TryGet(0x101)!.OneShotAnimationControlFlags); + } + + [Fact] + public void DetachedOneShotSurvivesGlobalCompletionWithoutBlockingAndClearsFlagNaturally() + { + var gfx = new GfxState(); + gfx.SetSurface(1, 0x20, -1); + gfx.BindDraw(0x100, 1, 0, 0, 100, 100, 0, 0); + gfx.BindDraw(0x101, 1, 0, 0, 100, 100, 0, 0); + gfx.SetAnimatedObjectColorResolved(0x100, 0, 1000, 0, 0xffffff); + gfx.SetAnimatedObjectColorResolved(0x101, 0, 1000, 0, 0xffffff); + gfx.SetOneShotAnimationControl(0x100, 1); + + gfx.ResetAnimClock(); + + Assert.True(gfx.TryGet(0x100)!.OneShotColorEnabled); + Assert.False(gfx.TryGet(0x101)!.OneShotColorEnabled); + Assert.False(gfx.HasActiveTimedPresentation(1000)); + Assert.True(gfx.HasActiveVisualPresentation(1000)); + + gfx.SnapshotVisibleObjects(1000); + Assert.True(gfx.SnapshotVisibleObjects(1500).Single(x => x.Handle == 0x100).ColorTransition!.Value.Active); + gfx.SnapshotVisibleObjects(2000); + + Assert.False(gfx.TryGet(0x100)!.OneShotColorEnabled); + Assert.Equal(0, gfx.TryGet(0x100)!.OneShotAnimationControlFlags & 1); + } +} diff --git a/engine/Age.Engine.Tests/GfxAnimationTests.cs b/engine/Age.Engine.Tests/GfxAnimationTests.cs index f9a4397..0ee38a5 100644 --- a/engine/Age.Engine.Tests/GfxAnimationTests.cs +++ b/engine/Age.Engine.Tests/GfxAnimationTests.cs @@ -260,7 +260,7 @@ public class GfxAnimationTests } [Fact] - public void ResetAnimClock_DispatchClearsOnlyGlobalServiceClock() + public void ResetAnimClock_DispatchClearsGlobalServiceClock() { var t = T(); var scene = ScriptAssembler.Assemble(t, "CLOCKRESET", new List<(int, Operand[])> diff --git a/engine/Age.Engine/Model/GfxState.cs b/engine/Age.Engine/Model/GfxState.cs index 608d473..316e11a 100644 --- a/engine/Age.Engine/Model/GfxState.cs +++ b/engine/Age.Engine/Model/GfxState.cs @@ -100,6 +100,9 @@ public sealed class GfxState public (double X, double Y, double Z, double Angle) RotationTarget; public long RotationDelayMs, RotationDurationMs; public bool RotationChannelEnabled; + // Op 0x242 writes obj+0x2d0. Bit 0 detaches the finite one-shot group from the blocking-dirty + // service and protects it from op 0x243's global force-completion request until natural completion. + public long OneShotAnimationControlFlags; // Shared matrix-channel start timestamp obj+0x34, seeded from retained-gfx owner+0xb550 // (EngineCtx+0x51b64). public long OneShotStartMs = -1; @@ -170,7 +173,9 @@ public sealed class GfxState TranslationEnabled = s.TranslationEnabled, RotationCurrent = s.RotationCurrent, RotationTarget = s.RotationTarget, RotationDelayMs = s.RotationDelayMs, RotationDurationMs = s.RotationDurationMs, - RotationChannelEnabled = s.RotationChannelEnabled, OneShotStartMs = s.OneShotStartMs, + RotationChannelEnabled = s.RotationChannelEnabled, + OneShotAnimationControlFlags = s.OneShotAnimationControlFlags, + OneShotStartMs = s.OneShotStartMs, RotationPeriodMs = s.RotationPeriodMs, RotationAxis = s.RotationAxis, RotationEnabled = s.RotationEnabled, RotationStartMs = s.RotationStartMs, }; @@ -395,10 +400,58 @@ public sealed class GfxState lock (_lock) return _surfaceTransitions.Values.Any(t => TransitionProgress(t, nowMs) < 1.0) || _objects.Values.Any(o => o.Visible && + (o.OneShotAnimationControlFlags & 1) == 0 && (o.OneShotColorEnabled || o.ScaleEnabled || o.RotationChannelEnabled || o.TranslationEnabled)); } + /// Op 0x242: replace the retained object's animation-control word. Native bit 0 makes its + /// finite one-shot channels nonblocking and immune to op 0x243 forced completion. + public void SetOneShotAnimationControl(long handle, long flags) + { + lock (_lock) GetOrCreate(handle).OneShotAnimationControlFlags = flags; + } + + /// Part of op 0x243: immediately commit every unprotected finite one-shot channel. Objects + /// marked by op 0x242 bit 0 keep sampling asynchronously. + private void ForceCompleteOneShotChannels() + { + foreach (var o in _objects.Values) + { + if ((o.OneShotAnimationControlFlags & 1) != 0) continue; + if (o.OneShotColorEnabled) + { + o.Color = o.OneShotColorTarget & 0xffffffff; + o.OneShotColorTarget = -1; + o.ColorDelayMs = 0; + o.ColorDurationMs = 0; + o.OneShotColorEnabled = false; + } + if (o.ScaleEnabled) + { + o.ScaleCurrent = o.ScaleTarget; + o.ScaleDelayMs = 0; + o.ScaleDurationMs = 0; + o.ScaleEnabled = false; + } + if (o.RotationChannelEnabled) + { + o.RotationCurrent = o.RotationTarget; + o.RotationDelayMs = 0; + o.RotationDurationMs = 0; + o.RotationChannelEnabled = false; + } + if (o.TranslationEnabled) + { + o.TranslationCurrent = o.TranslationTarget; + o.TranslationDelayMs = 0; + o.TranslationDurationMs = 0; + o.TranslationEnabled = false; + } + o.OneShotStartMs = -1; + } + } + /// Whether sampling the retained scene at a later frame can change its pixels without another /// VM mutation. Includes finite presentation work plus the ambient channels that may remain active while /// the interpreter is parked at an input wait. Static waits themselves are deliberately not animation. @@ -555,10 +608,16 @@ public sealed class GfxState lock (_lock) { AnimClockDurationTicks = durationTicks; AnimClockGeneration++; } } - /// Op 0x243: reset the separate global animation-service clock. + /// Op 0x243: force ordinary finite channels to their endpoints and reset the separate + /// global animation-service clock. Op-0x242-detached objects ignore the completion request. public void ResetAnimClock() { - lock (_lock) { AnimClockDurationTicks = 0; AnimClockGeneration++; } + lock (_lock) + { + ForceCompleteOneShotChannels(); + AnimClockDurationTicks = 0; + AnimClockGeneration++; + } } /// Back-compat: snapshot with no animation clock (nowMs = 0) — deterministic, for headless @@ -579,6 +638,8 @@ public sealed class GfxState { var o = kv.Value; if (!o.Visible) continue; + bool hadOneShot = o.OneShotColorEnabled || o.ScaleEnabled || + o.RotationChannelEnabled || o.TranslationEnabled; var (resId, ck) = _surfaces.TryGetValue(o.SourceSlot, out var s) ? s : (0L, 0L); // ---- packed color: a static mode 0 treats alpha as tint/fill strength. Once op 0x202 has @@ -644,7 +705,7 @@ public sealed class GfxState } // One-shot matrix channels: hold current through delay, then linearly sample current -> target. - if ((o.OneShotColorEnabled || o.ScaleEnabled || o.RotationChannelEnabled || o.TranslationEnabled) + if (hadOneShot && o.OneShotStartMs < 0) o.OneShotStartMs = nowMs; var scale = SampleMatrixChannel(ref o.ScaleCurrent, o.ScaleTarget, o.ScaleDelayMs, @@ -656,7 +717,10 @@ public sealed class GfxState o.TranslationDelayMs, o.TranslationDurationMs, o.OneShotStartMs, ref o.TranslationEnabled, nowMs); if (!o.OneShotColorEnabled && !o.ScaleEnabled && !o.RotationChannelEnabled && !o.TranslationEnabled) + { + if (hadOneShot) o.OneShotAnimationControlFlags &= ~1L; o.OneShotStartMs = -1; + } double cycleAngle = 0; if (o.RotationEnabled && o.RotationPeriodMs > 0) diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs index 4321650..f67d072 100644 --- a/engine/Age.Engine/Vm/VirtualMachine.cs +++ b/engine/Age.Engine/Vm/VirtualMachine.cs @@ -1102,7 +1102,9 @@ public sealed class VirtualMachine Gfx.SetRotationCycle(Read(a[0]), Read(a[1]), (Read(a[2]), Read(a[3]), Read(a[4]))); return pc + 1; case "set-anim-clock": // 0x238 (duration) — global, non-blocking (host advances it per-frame) Gfx.SetAnimClock(Read(a[0])); return pc + 1; - case "reset-anim-clock": // 0x243: reset the separate global animation-service clock + case "set-object-animation-detached": // 0x242 (handle)(flags): bit 0 is nonblocking/force-proof + Gfx.SetOneShotAnimationControl(Read(a[0]), Read(a[1])); return pc + 1; + case "reset-anim-clock": // 0x243: force unprotected one-shots and reset the global service clock Gfx.ResetAnimClock(); return pc + 1; 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]), diff --git a/vm-map/engine-ctx.toml b/vm-map/engine-ctx.toml index ba123aa..1711891 100644 --- a/vm-map/engine-ctx.toml +++ b/vm-map/engine-ctx.toml @@ -116,7 +116,7 @@ note = "retained_gfx_manager+0xb530 selected surface slot; -1 denotes device bac offset = 0x51b6c name = "gfx_dirty_a" type = "int" -note = "retained_gfx_manager+0xb558 dirty flag; op 0x1f6 clears it" +note = "retained_gfx_manager+0xb558 redraw-dirty flag; active finite object channels set it even when detached by 0x242; op 0x1f6 clears it" [[field]] offset = 0x51b70 name = "gfx_force_complete" @@ -126,7 +126,12 @@ note = "retained_gfx_manager+0xb55c requests forced completion of one-shot chann offset = 0x51b74 name = "gfx_dirty_b" type = "int" -note = "retained_gfx_manager+0xb560 dirty flag" +note = "retained_gfx_manager+0xb560 blocking-presentation dirty flag; active finite object channels set it unless obj+0x2d0 bit 0 detaches them" +[[field]] +offset = 0x51b80 +name = "gfx_animation_service_flags" +type = "uint" +note = "retained_gfx_manager+0xb56c service flags; bit 1 suppresses op 0x243 force-complete/clock-reset request" [[field]] offset = 0x52bd4 name = "surfaces" diff --git a/vm-map/opcodes.toml b/vm-map/opcodes.toml index c8b6ce1..f16d77b 100644 --- a/vm-map/opcodes.toml +++ b/vm-map/opcodes.toml @@ -6440,19 +6440,20 @@ observed_types = ["l-int"] [[opcode]] op = 0x242 -label = "set-gfx-field2d0" +label = "set-object-animation-detached" argc = 2 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "set-gfx-field2d0" +name = "set-object-animation-detached" category = "draw" -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." +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." +details = "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." noop_headless = false source = "investigation" confidence = "high" depends_on = [] -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." +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." [[opcode.semantics.args]] i = 1 @@ -6461,7 +6462,7 @@ observed_types = ["l-int", "l-ptr"] [[opcode.semantics.args]] i = 2 -role = "field2d0_value" +role = "animation control flags; bit 0 = detached/nonblocking" observed_types = ["imm"] [[opcode]] @@ -6473,12 +6474,12 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "reset-anim-clock" category = "draw" -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." noop_headless = false source = "investigation" confidence = "high" -depends_on = [] -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." [[opcode]] op = 0x248