diff --git a/docs/engine-re.md b/docs/engine-re.md index 4972075..33dece3 100644 --- a/docs/engine-re.md +++ b/docs/engine-re.md @@ -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 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 -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 affine rasterization. `0x223` is **not affine**: `gfx_queue_surface_alpha_transition` (`0x47f440`) inserts diff --git a/docs/opcode-reference.md b/docs/opcode-reference.md index f618017..429a1a9 100644 --- a/docs/opcode-reference.md +++ b/docs/opcode-reference.md @@ -942,6 +942,8 @@ Implemented through IHost.PlayModalMovieToSurface. Its operand uses the same nat - **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. +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) - **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 diff --git a/docs/phase-b-framework.md b/docs/phase-b-framework.md index bd4884c..da523ac 100644 --- a/docs/phase-b-framework.md +++ b/docs/phase-b-framework.md @@ -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 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 -looping-channel start/period pairs without changing current/base transforms. Its six calls are five -DEBUGADV effect demonstrations plus one FIELD movement setup. +**Cyclic reset implemented (2026-07-29):** `0x230(handle)` now gets or creates the retained object, +disables the four looping channels represented by the compositor, and clears the complete native +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 diff --git a/engine/Age.Engine.Tests/GfxAnimationTests.cs b/engine/Age.Engine.Tests/GfxAnimationTests.cs index c59f1b5..3b9efeb 100644 --- a/engine/Age.Engine.Tests/GfxAnimationTests.cs +++ b/engine/Age.Engine.Tests/GfxAnimationTests.cs @@ -393,6 +393,85 @@ public class GfxAnimationTests 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()); + 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] public void CurrentTranslationSetter_ReplacesTheLiveMatrixImmediately() { diff --git a/engine/Age.Engine/Model/GfxState.cs b/engine/Age.Engine/Model/GfxState.cs index ece03fe..b1d43c9 100644 --- a/engine/Age.Engine/Model/GfxState.cs +++ b/engine/Age.Engine/Model/GfxState.cs @@ -1097,6 +1097,37 @@ public sealed class GfxState } } + /// 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. + 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); + } + } + } + /// Op 0x238: set its separate global animation-service duration and reset marker. public void SetAnimClock(long durationTicks) { diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs index a05006e..7b6a0f2 100644 --- a/engine/Age.Engine/Vm/VirtualMachine.cs +++ b/engine/Age.Engine/Vm/VirtualMachine.cs @@ -2556,6 +2556,9 @@ public sealed class VirtualMachine return pc + 1; 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; + 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) 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 diff --git a/vm-map/opcodes.toml b/vm-map/opcodes.toml index 0857959..5e77e34 100644 --- a/vm-map/opcodes.toml +++ b/vm-map/opcodes.toml @@ -6076,6 +6076,7 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "reset-gfx-cyclic-animations" 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." noop_headless = false source = "investigation"