Implement cyclic animation reset opcode

This commit is contained in:
gamer147
2026-07-29 10:52:21 -04:00
parent 2777da435b
commit 4af0290bbb
7 changed files with 128 additions and 5 deletions

View File

@@ -1371,7 +1371,10 @@ active flag bit 2, and zeroes five start/period pairs:
It does not change the object's current/base transforms. Five DEBUGADV calls reset an effect object before It does not change the object's current/base transforms. Five DEBUGADV calls reset an effect object before
demonstrating a new looping asset; FIELD's sole ordinary call resets the temporary moving-unit handle demonstrating a new looping asset; FIELD's sole ordinary call resets the temporary moving-unit handle
between its one-shot translation setup and `0x239` source-cell animation. This is a bounded retained-state between its one-shot translation setup and `0x239` source-cell animation. This is a bounded retained-state
operation and is the recommended follow-up after the black-fade pair below. operation. The port now exposes the same get-or-create reset on `GfxState`: it disables cyclic color,
scale, rotation, and source-rectangle sampling; clears all ten native timing dwords in the preserved
numbered-save record, including the unmodeled second cyclic matrix; and leaves base/current transforms,
cyclic targets, and finite one-shot channels intact.
**Follow-up resolution (2026-07-10):** `0x21f` is the one-shot axis-angle channel and is implemented with **Follow-up resolution (2026-07-10):** `0x21f` is the one-shot axis-angle channel and is implemented with
affine rasterization. `0x223` is **not affine**: `gfx_queue_surface_alpha_transition` (`0x47f440`) inserts affine rasterization. `0x223` is **not affine**: `gfx_queue_surface_alpha_transition` (`0x47f440`) inserts

View File

@@ -942,6 +942,8 @@ Implemented through IHost.PlayModalMovieToSurface. Its operand uses the same nat
- **grounding:** source=investigation, confidence=high - **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x230_reset_gfx_cyclic_animations@0x423ba0 fetches the handle and calls gfx_object_reset_cyclic_animation_channels@0x47ee60. The worker clears object flag bit 2 and zeroes starts obj+0x20c/+0x210/+0x214/+0x218/+0x21c plus periods obj+0x220/+0x224/+0x228/+0x22c/+0x230, covering looping color, scale, rotation, the second cyclic matrix, and source-rectangle channels. Corpus: five DEBUGADV effect demonstrations plus one FIELD movement setup before 0x239. - **evidence:** Ghidra /v2: op_0x230_reset_gfx_cyclic_animations@0x423ba0 fetches the handle and calls gfx_object_reset_cyclic_animation_channels@0x47ee60. The worker clears object flag bit 2 and zeroes starts obj+0x20c/+0x210/+0x214/+0x218/+0x21c plus periods obj+0x220/+0x224/+0x228/+0x22c/+0x230, covering looping color, scale, rotation, the second cyclic matrix, and source-rectangle channels. Corpus: five DEBUGADV effect demonstrations plus one FIELD movement setup before 0x239.
Port status (2026-07-29): implemented as one retained-state reset covering cyclic color, scale, rotation, secondary-matrix, and source-rectangle timing. It disables the four modeled looping channels, clears the complete native raw timing block for drop-in persistence, and preserves base/current transforms, cyclic targets, and one-shot channels.
### 0x231 `animate-gfx-srcrect-loop` (u00421EA0, argc 4) ### 0x231 `animate-gfx-srcrect-loop` (u00421EA0, argc 4)
- **summary:** (handle)(frame_period_ms)(frame_count)(column_count) — loop row-major through the spritesheet. Every sample preserves draw-texture's source-rectangle width/height; frame=floor((shared_frame_time-object_start)/frame_period)%frame_count, src offset=(frame%columns*width, frame/columns*height). All objects use the retained manager's shared current/previous millisecond timestamps but retain their own start and period. The native consumer raises redraw dirty only when the current and previous samples select different cells. Worker gfx_worker_anim_srcrect @0x47eec0; consumer gfx_object_anim_interpolate @0x473ed0. - **summary:** (handle)(frame_period_ms)(frame_count)(column_count) — loop row-major through the spritesheet. Every sample preserves draw-texture's source-rectangle width/height; frame=floor((shared_frame_time-object_start)/frame_period)%frame_count, src offset=(frame%columns*width, frame/columns*height). All objects use the retained manager's shared current/previous millisecond timestamps but retain their own start and period. The native consumer raises redraw dirty only when the current and previous samples select different cells. Worker gfx_worker_anim_srcrect @0x47eec0; consumer gfx_object_anim_interpolate @0x473ed0.
- **grounding:** source=investigation, confidence=high - **grounding:** source=investigation, confidence=high

View File

@@ -849,11 +849,15 @@ conversion and exact terminal-frame publication, and closes all 11 sites. The re
is 16 distinct opcodes / 28 instructions. All 477 engine tests, opcode build/lint, the zero-warning Godot is 16 distinct opcodes / 28 instructions. All 477 engine tests, opcode build/lint, the zero-warning Godot
build, and the Himegari-targeted threaded selftest pass. build, and the Himegari-targeted threaded selftest pass.
The follow-up `0x230(handle)` is decoded: it clears the retained object's cyclic active flag and five **Cyclic reset implemented (2026-07-29):** `0x230(handle)` now gets or creates the retained object,
looping-channel start/period pairs without changing current/base transforms. Its six calls are five disables the four looping channels represented by the compositor, and clears the complete native
DEBUGADV effect demonstrations plus one FIELD movement setup. start/period block—including the preserved raw state for the currently unmodeled second cyclic matrix.
Base/current transforms, cyclic targets, and finite one-shot channels remain intact. This closes its six
sites (five DEBUGADV demonstrations and one FIELD movement setup), reducing the remaining effectful
inventory to 15 distinct opcodes / 22 instructions. All 479 engine tests, opcode build/lint, the
zero-warning Godot build, and the Himegari-targeted threaded selftest pass.
**NEXT:** implement `0x230`. **NEXT:** investigate `0x1b2`, the widest remaining unknown at three sites.
## Later Phase B breadth ## Later Phase B breadth

View File

@@ -393,6 +393,85 @@ public class GfxAnimationTests
Assert.Equal((1.0, 1.0, 1.0), o.ScaleCurrent); Assert.Equal((1.0, 1.0, 1.0), o.ScaleCurrent);
} }
[Fact]
public void ResetCyclicAnimationChannels_StopsEveryLoopAndPreservesBaseAndOneShotState()
{
var g = new GfxState();
g.SetSurface(1, 5, -1);
g.BindDraw(7, 1, 0, 0, 64, 64, 12, 34);
var o = g.TryGet(7)!;
o.V18 = (10, 20, 30);
o.V24 = (12, 34, 56);
o.V16c = (7, 8, 9);
o.ScaleCurrent = (1.25, 0.75, 2);
o.TranslationCurrent = (4, 5, 6);
o.RotationCurrent = (0, 0, 1, 15);
o.ScaleEnabled = true;
o.ScaleDurationMs = 200;
o.OneShotStartMs = 123;
o.NativePersistenceRecord = Enumerable.Repeat((byte)0x7f, 0x2d4).ToArray();
g.SetColorAnim(7, 400, GfxState.PackColor(0x80, 0xff0000));
g.SetScaleCycle(7, 600, (150, 75, 100));
g.SetRotationCycle(7, 800, (0, 0, 1));
g.SetSrcRect(7, frameCount: 8, columns: 4, cell: 3, period: 100);
g.ResetCyclicAnimationChannels(7);
Assert.False(o.ColorAnim);
Assert.False(o.ScaleCycleEnabled);
Assert.False(o.RotationEnabled);
Assert.False(o.SrcAnim);
Assert.Equal((0L, 0L, 0L, 0L), (o.ColorPeriod, o.ScaleCyclePeriodMs, o.RotationPeriodMs, o.SrcPeriod));
Assert.Equal((-1L, -1L, -1L, -1L), (o.ColorStart, o.ScaleCycleStartMs, o.RotationStartMs, o.SrcStart));
Assert.Equal((10L, 20L, 30L), o.V18);
Assert.Equal((12L, 34L, 56L), o.V24);
Assert.Equal((7L, 8L, 9L), o.V16c);
Assert.Equal((1.25, 0.75, 2.0), o.ScaleCurrent);
Assert.Equal((4.0, 5.0, 6.0), o.TranslationCurrent);
Assert.Equal((0.0, 0.0, 1.0, 15.0), o.RotationCurrent);
Assert.True(o.ScaleEnabled);
Assert.Equal(200, o.ScaleDurationMs);
Assert.Equal(123, o.OneShotStartMs);
Assert.Equal((1.5, 0.75, 1.0), o.ScaleCycleTarget);
Assert.Equal((0L, 0L, 1L), o.RotationAxis);
Assert.Equal((8L, 4L, 3L), (o.SrcFrameCount, o.SrcColumns, o.SrcCell));
Assert.Equal(0, o.NativePersistenceRecord[0] & 4);
Assert.All(o.NativePersistenceRecord.Skip(0x20c).Take(0x28), value => Assert.Equal(0, value));
}
[Fact]
public void Op0x230_DispatchGetsOrCreatesObjectAndRemovesContinuousPresentation()
{
var t = T();
var scene = ScriptAssembler.Assemble(t, "RESET_CYCLES", new List<(int, Operand[])>
{
MovGI(1, 0x1000),
(0x230, new[] { G(1) }),
Exit(),
}, System.Array.Empty<string>());
var vm = new VirtualMachine(scene, t, new RecordingHost());
vm.Gfx.SetSurface(1, 5, -1);
vm.Gfx.BindDraw(0x1000, 1, 0, 0, 64, 64, 0, 0);
vm.Gfx.SetColorAnim(0x1000, 400, GfxState.PackColor(0x80, 0xff0000));
vm.Gfx.SetScaleCycle(0x1000, 600, (150, 75, 100));
vm.Gfx.SetRotationCycle(0x1000, 800, (0, 0, 1));
vm.Gfx.SetSrcRect(0x1000, frameCount: 8, columns: 4, cell: 0, period: 100);
Assert.True(vm.Gfx.HasActiveVisualPresentation(1000));
vm.Run();
Assert.NotNull(vm.Gfx.TryGet(0x1000));
Assert.False(vm.Gfx.HasActiveVisualPresentation(1000));
Assert.Equal(
GfxPresentationReason.RetainedMutation,
vm.Gfx.ConsumePresentationReasons(1000));
var createOnly = new VirtualMachine(scene, t, new RecordingHost());
createOnly.Run();
Assert.NotNull(createOnly.Gfx.TryGet(0x1000));
}
[Fact] [Fact]
public void CurrentTranslationSetter_ReplacesTheLiveMatrixImmediately() public void CurrentTranslationSetter_ReplacesTheLiveMatrixImmediately()
{ {

View File

@@ -1097,6 +1097,37 @@ public sealed class GfxState
} }
} }
/// <summary>Op 0x230: stop every cyclic object channel without changing its base/current state,
/// one-shot channels, or retained cyclic targets. Native clears flag bit 2 and the five start/period
/// pairs at obj+0x20c..0x230, including one secondary matrix channel not otherwise modeled here.</summary>
public void ResetCyclicAnimationChannels(long handle)
{
lock (_lock)
{
var o = GetOrCreate(handle);
o.ColorStart = -1;
o.ScaleCycleStartMs = -1;
o.RotationStartMs = -1;
o.SrcStart = -1;
o.ColorPeriod = 0;
o.ScaleCyclePeriodMs = 0;
o.RotationPeriodMs = 0;
o.SrcPeriod = 0;
o.ColorAnim = false;
o.ScaleCycleEnabled = false;
o.RotationEnabled = false;
o.SrcAnim = false;
// Preserve drop-in numbered-save compatibility for the unmodeled secondary cyclic matrix
// as well as the modeled channels. The ten native timing dwords form one contiguous range.
if (o.NativePersistenceRecord is { Length: >= 0x234 } raw)
{
raw[0] &= 0xfb;
System.Array.Clear(raw, 0x20c, 0x28);
}
}
}
/// <summary>Op 0x238: set its separate global animation-service duration and reset marker.</summary> /// <summary>Op 0x238: set its separate global animation-service duration and reset marker.</summary>
public void SetAnimClock(long durationTicks) public void SetAnimClock(long durationTicks)
{ {

View File

@@ -2556,6 +2556,9 @@ public sealed class VirtualMachine
return pc + 1; return pc + 1;
case "u004223C0": // 0x239 spritesheet cell: (handle)(delay)(duration)(frame count)(columns)(cell) case "u004223C0": // 0x239 spritesheet cell: (handle)(delay)(duration)(frame count)(columns)(cell)
Gfx.SetSrcRect(Read(a[0]), Read(a[3]), Read(a[4]), Read(a[5]), 0); return pc + 1; Gfx.SetSrcRect(Read(a[0]), Read(a[3]), Read(a[4]), Read(a[5]), 0); return pc + 1;
case "reset-gfx-cyclic-animations": // 0x230: stop all five retained looping channels
case "u00421E70":
Gfx.ResetCyclicAnimationChannels(Read(a[0])); return pc + 1;
case "u00421EA0": // 0x231 looping spritesheet: (handle)(ms per frame)(frame count)(columns) case "u00421EA0": // 0x231 looping spritesheet: (handle)(ms per frame)(frame count)(columns)
Gfx.SetSrcRect(Read(a[0]), Read(a[2]), Read(a[3]), 0, Read(a[1])); return pc + 1; Gfx.SetSrcRect(Read(a[0]), Read(a[2]), Read(a[3]), 0, Read(a[1])); return pc + 1;
case "u00421EF0": // 0x232 cyclic packed ARGB; negative alpha/RGB preserve static obj color case "u00421EF0": // 0x232 cyclic packed ARGB; negative alpha/RGB preserve static obj color

View File

@@ -6076,6 +6076,7 @@ abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "reset-gfx-cyclic-animations" name = "reset-gfx-cyclic-animations"
category = "draw" category = "draw"
details = "Port status (2026-07-29): implemented as one retained-state reset covering cyclic color, scale, rotation, secondary-matrix, and source-rectangle timing. It disables the four modeled looping channels, clears the complete native raw timing block for drop-in persistence, and preserves base/current transforms, cyclic targets, and one-shot channels."
summary = "(handle) — get or create a retained gfx object, clear its cyclic-animation active flag, and zero all five looping-channel start/period pairs without changing current/base transforms." summary = "(handle) — get or create a retained gfx object, clear its cyclic-animation active flag, and zero all five looping-channel start/period pairs without changing current/base transforms."
noop_headless = false noop_headless = false
source = "investigation" source = "investigation"