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 | | `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 | | `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 | | `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 | | `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 | | `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 | | `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 | | `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 | | `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) | | `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 `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, 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 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 a reset: it clears the object's detached-animation control word after binding. Its consumer and complete
the supplied value at `obj+0x2d0`. lifecycle are documented under “Detached finite object animation” below.
**Page-89 surface-lifetime correction (2026-07-11).** Locator `SC0000 P089` resolves to **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 `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 when retained-gfx `owner+0xb55c == 1` (`EngineCtx+0x51b70`) requests forced completion—the target commits
to current, delay/duration clear, to current, delay/duration clear,
target becomes `0xffffffff`, and the one-shot active bit clears when no color/matrix/src-rect sibling remains. 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 The object-local bit at `+0x2d0` suppresses the global force and keeps that finite group from raising the
independently preserve their bytes from current `+0x60`. 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 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 `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 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. 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 **Resolved 2026-07-20:** `0x236` is the movie-to-retained-surface path described below. `0x242` is the
classified as the retained-object `+0x2d0` field setter described above; `0x23d/0x20a/0x20e` remain GAP. 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) ### 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) ## Native walls backlog (targets for this loop)
- ~~**call-script dispatch**~~ — **SOLVED** (above): `call-script <id>` = raw SYS4INI file index. - ~~**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. - **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 - **grounding:** source=kelebek, confidence=low
### 0x242 `set-gfx-field2d0` (set-gfx-field2d0, argc 2) ### 0x242 `set-object-animation-detached` (set-object-animation-detached, 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. - **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 - **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) ### 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 - **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 ## 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, 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. 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.

View File

@@ -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<Operand>());
[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<string>());
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);
}
}

View File

@@ -260,7 +260,7 @@ public class GfxAnimationTests
} }
[Fact] [Fact]
public void ResetAnimClock_DispatchClearsOnlyGlobalServiceClock() public void ResetAnimClock_DispatchClearsGlobalServiceClock()
{ {
var t = T(); var t = T();
var scene = ScriptAssembler.Assemble(t, "CLOCKRESET", new List<(int, Operand[])> var scene = ScriptAssembler.Assemble(t, "CLOCKRESET", new List<(int, Operand[])>

View File

@@ -100,6 +100,9 @@ public sealed class GfxState
public (double X, double Y, double Z, double Angle) RotationTarget; public (double X, double Y, double Z, double Angle) RotationTarget;
public long RotationDelayMs, RotationDurationMs; public long RotationDelayMs, RotationDurationMs;
public bool RotationChannelEnabled; 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 // Shared matrix-channel start timestamp obj+0x34, seeded from retained-gfx owner+0xb550
// (EngineCtx+0x51b64). // (EngineCtx+0x51b64).
public long OneShotStartMs = -1; public long OneShotStartMs = -1;
@@ -170,7 +173,9 @@ public sealed class GfxState
TranslationEnabled = s.TranslationEnabled, TranslationEnabled = s.TranslationEnabled,
RotationCurrent = s.RotationCurrent, RotationTarget = s.RotationTarget, RotationCurrent = s.RotationCurrent, RotationTarget = s.RotationTarget,
RotationDelayMs = s.RotationDelayMs, RotationDurationMs = s.RotationDurationMs, 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, RotationPeriodMs = s.RotationPeriodMs, RotationAxis = s.RotationAxis,
RotationEnabled = s.RotationEnabled, RotationStartMs = s.RotationStartMs, RotationEnabled = s.RotationEnabled, RotationStartMs = s.RotationStartMs,
}; };
@@ -395,10 +400,58 @@ public sealed class GfxState
lock (_lock) lock (_lock)
return _surfaceTransitions.Values.Any(t => TransitionProgress(t, nowMs) < 1.0) || return _surfaceTransitions.Values.Any(t => TransitionProgress(t, nowMs) < 1.0) ||
_objects.Values.Any(o => o.Visible && _objects.Values.Any(o => o.Visible &&
(o.OneShotAnimationControlFlags & 1) == 0 &&
(o.OneShotColorEnabled || o.ScaleEnabled || (o.OneShotColorEnabled || o.ScaleEnabled ||
o.RotationChannelEnabled || o.TranslationEnabled)); o.RotationChannelEnabled || o.TranslationEnabled));
} }
/// <summary>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.</summary>
public void SetOneShotAnimationControl(long handle, long flags)
{
lock (_lock) GetOrCreate(handle).OneShotAnimationControlFlags = flags;
}
/// <summary>Part of op 0x243: immediately commit every unprotected finite one-shot channel. Objects
/// marked by op 0x242 bit 0 keep sampling asynchronously.</summary>
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;
}
}
/// <summary>Whether sampling the retained scene at a later frame can change its pixels without another /// <summary>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 /// 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.</summary> /// the interpreter is parked at an input wait. Static waits themselves are deliberately not animation.</summary>
@@ -555,10 +608,16 @@ public sealed class GfxState
lock (_lock) { AnimClockDurationTicks = durationTicks; AnimClockGeneration++; } lock (_lock) { AnimClockDurationTicks = durationTicks; AnimClockGeneration++; }
} }
/// <summary>Op 0x243: reset the separate global animation-service clock.</summary> /// <summary>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.</summary>
public void ResetAnimClock() public void ResetAnimClock()
{ {
lock (_lock) { AnimClockDurationTicks = 0; AnimClockGeneration++; } lock (_lock)
{
ForceCompleteOneShotChannels();
AnimClockDurationTicks = 0;
AnimClockGeneration++;
}
} }
/// <summary>Back-compat: snapshot with no animation clock (nowMs = 0) — deterministic, for headless /// <summary>Back-compat: snapshot with no animation clock (nowMs = 0) — deterministic, for headless
@@ -579,6 +638,8 @@ public sealed class GfxState
{ {
var o = kv.Value; var o = kv.Value;
if (!o.Visible) continue; 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); 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 // ---- 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. // 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 < 0)
o.OneShotStartMs = nowMs; o.OneShotStartMs = nowMs;
var scale = SampleMatrixChannel(ref o.ScaleCurrent, o.ScaleTarget, o.ScaleDelayMs, var scale = SampleMatrixChannel(ref o.ScaleCurrent, o.ScaleTarget, o.ScaleDelayMs,
@@ -656,7 +717,10 @@ public sealed class GfxState
o.TranslationDelayMs, o.TranslationDurationMs, o.TranslationDelayMs, o.TranslationDurationMs,
o.OneShotStartMs, ref o.TranslationEnabled, nowMs); o.OneShotStartMs, ref o.TranslationEnabled, nowMs);
if (!o.OneShotColorEnabled && !o.ScaleEnabled && !o.RotationChannelEnabled && !o.TranslationEnabled) if (!o.OneShotColorEnabled && !o.ScaleEnabled && !o.RotationChannelEnabled && !o.TranslationEnabled)
{
if (hadOneShot) o.OneShotAnimationControlFlags &= ~1L;
o.OneShotStartMs = -1; o.OneShotStartMs = -1;
}
double cycleAngle = 0; double cycleAngle = 0;
if (o.RotationEnabled && o.RotationPeriodMs > 0) if (o.RotationEnabled && o.RotationPeriodMs > 0)

View File

@@ -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; 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) case "set-anim-clock": // 0x238 (duration) — global, non-blocking (host advances it per-frame)
Gfx.SetAnimClock(Read(a[0])); return pc + 1; 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; Gfx.ResetAnimClock(); return pc + 1;
case "queue-surface-alpha-transition": // 0x223: target surface crossfade over two object ranges 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]), Gfx.QueueSurfaceAlphaTransition(Read(a[0]), (int)Read(a[1]), Read(a[2]), (int)Read(a[3]),

View File

@@ -116,7 +116,7 @@ note = "retained_gfx_manager+0xb530 selected surface slot; -1 denotes device bac
offset = 0x51b6c offset = 0x51b6c
name = "gfx_dirty_a" name = "gfx_dirty_a"
type = "int" 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]] [[field]]
offset = 0x51b70 offset = 0x51b70
name = "gfx_force_complete" name = "gfx_force_complete"
@@ -126,7 +126,12 @@ note = "retained_gfx_manager+0xb55c requests forced completion of one-shot chann
offset = 0x51b74 offset = 0x51b74
name = "gfx_dirty_b" name = "gfx_dirty_b"
type = "int" 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]] [[field]]
offset = 0x52bd4 offset = 0x52bd4
name = "surfaces" name = "surfaces"

View File

@@ -6440,19 +6440,20 @@ observed_types = ["l-int"]
[[opcode]] [[opcode]]
op = 0x242 op = 0x242
label = "set-gfx-field2d0" label = "set-object-animation-detached"
argc = 2 argc = 2
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "set-gfx-field2d0" name = "set-object-animation-detached"
category = "draw" 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 noop_headless = false
source = "investigation" source = "investigation"
confidence = "high" confidence = "high"
depends_on = [] 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]] [[opcode.semantics.args]]
i = 1 i = 1
@@ -6461,7 +6462,7 @@ observed_types = ["l-int", "l-ptr"]
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 2 i = 2
role = "field2d0_value" role = "animation control flags; bit 0 = detached/nonblocking"
observed_types = ["imm"] observed_types = ["imm"]
[[opcode]] [[opcode]]
@@ -6473,12 +6474,12 @@ abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "reset-anim-clock" name = "reset-anim-clock"
category = "draw" 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 noop_headless = false
source = "investigation" source = "investigation"
confidence = "high" confidence = "high"
depends_on = [] depends_on = [0x242]
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." 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]] [[opcode]]
op = 0x248 op = 0x248