Implement detached object animation control
This commit is contained in:
@@ -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) |
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
61
engine/Age.Engine.Tests/GfxAnimationDetachTests.cs
Normal file
61
engine/Age.Engine.Tests/GfxAnimationDetachTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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[])>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
/// <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
|
||||
/// 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>
|
||||
@@ -555,10 +608,16 @@ public sealed class GfxState
|
||||
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()
|
||||
{
|
||||
lock (_lock) { AnimClockDurationTicks = 0; AnimClockGeneration++; }
|
||||
lock (_lock)
|
||||
{
|
||||
ForceCompleteOneShotChannels();
|
||||
AnimClockDurationTicks = 0;
|
||||
AnimClockGeneration++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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;
|
||||
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)
|
||||
|
||||
@@ -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]),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user