Implement cyclic scale opcode

This commit is contained in:
gamer147
2026-07-29 00:06:56 -04:00
parent f2f0cac936
commit bcfb3848bc
12 changed files with 179 additions and 22 deletions

View File

@@ -1348,8 +1348,11 @@ stores period `obj+0x224`, builds the target scale matrix at `obj+0x250`, and ra
matrix to the target. The first half-cycle therefore grows/shrinks identity to target and the second returns matrix to the target. The first half-cycle therefore grows/shrinks identity to target and the second returns
target to identity. This matrix lives in the separately anchored cyclic-animation product, independent of target to identity. This matrix lives in the separately anchored cyclic-animation product, independent of
the immediate/current and delayed one-shot scale matrices. Himegari has 31 sites in nine scripts: nineteen the immediate/current and delayed one-shot scale matrices. Himegari has 31 sites in nine scripts: nineteen
in DEBUGADV and twelve across eight ordinary ADV scenes. The port still skips the opcode; its existing in DEBUGADV and twelve across eight ordinary ADV scenes. The port now models this as its own retained
cyclic-channel sampling and affine compositor make this the smallest high-reach next implementation. channel, samples the exact triangular phase on the shared frame clock, and composes
`T(-anchor) * one-shot-scale * one-shot-rotation * translation * cyclic-scale * cyclic-rotation *
T(anchor)`. It remains frame-driven while visible and survives native-style object cloning independently
of the immediate and delayed one-shot scale fields.
**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

@@ -916,6 +916,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_0x233_set_scale_cycle@0x423cf0 divides operands 3..5 by 100 and calls gfx_object_set_scale_cycle@0x47efd0. The worker stores start=0 at obj+0x210, period at +0x224, and the target scale matrix at +0x250. gfx_object_anim_interpolate@0x473ed0 samples a triangular phase 2*min(elapsed%period,period-elapsed%period)/period, linearly blends identity to target, and multiplies the result into the separately anchored cyclic-animation product. Corpus: 31 sites in nine scripts, including eight ordinary ADV scenes. - **evidence:** Ghidra /v2: op_0x233_set_scale_cycle@0x423cf0 divides operands 3..5 by 100 and calls gfx_object_set_scale_cycle@0x47efd0. The worker stores start=0 at obj+0x210, period at +0x224, and the target scale matrix at +0x250. gfx_object_anim_interpolate@0x473ed0 samples a triangular phase 2*min(elapsed%period,period-elapsed%period)/period, linearly blends identity to target, and multiplies the result into the separately anchored cyclic-animation product. Corpus: 31 sites in nine scripts, including eight ordinary ADV scenes.
Port status (2026-07-28): implemented as an independent retained-object scale cycle. The compositor samples the exact triangular phase, keeps the channel frame-driven during waits, and composes it after one-shot translation but before the sibling cyclic rotation.
### 0x234 `anim-start` (anim-start, argc 5) ### 0x234 `anim-start` (anim-start, argc 5)
- **summary:** (handle)(period_ms)(axis_x)(axis_y)(axis_z) — configure cyclic rotation. Worker stores period obj+0x228, start obj+0x214=0, and float axis obj+0x244; each frame uses integer degrees floor(((now-start)%period)*360/period). gfx_object_composite right-multiplies this separately anchored transform after the one-shot scale/rotation/translation product, so cyclic rotation also rotates the translation vector. - **summary:** (handle)(period_ms)(axis_x)(axis_y)(axis_z) — configure cyclic rotation. Worker stores period obj+0x228, start obj+0x214=0, and float axis obj+0x244; each frame uses integer degrees floor(((now-start)%period)*360/period). gfx_object_composite right-multiplies this separately anchored transform after the one-shot scale/rotation/translation product, so cyclic rotation also rotates the translation vector.
- **grounding:** source=investigation, confidence=high - **grounding:** source=investigation, confidence=high

View File

@@ -805,13 +805,16 @@ fallthrough inventory from 25 to 23 distinct opcodes. Focused regressions distin
starts and prove immediate rotation does not arm the delayed `0x21f` channel. The opcode registry and starts and prove immediate rotation does not arm the delayed `0x21f` channel. The opcode registry and
generated references are current, and the native `/v2` image is annotated and saved. generated references are current, and the native `/v2` image is annotated and saved.
**Next-opcode reconnaissance:** `0x233` is the recommended next implementation. Native proves a **Cyclic scale `0x233` implemented (2026-07-28).** The retained model now owns the native channel
self-contained cyclic scale channel: identity→target over the first half-period and target→identity over independently of immediate and delayed one-shot scale. It samples the exact triangular
the second. Its 31 calls cover eight ordinary ADV scenes plus DEBUGADV, and the retained model already has identity-to-target-to-identity phase, remains frame-driven during visible waits, survives object cloning,
the sibling color/rotation cycles and affine sampling machinery. It therefore has broader runtime reach and and enters the separately anchored cyclic product after one-shot translation and before cyclic rotation.
lower ownership risk than the next alternative. The VM handler closes all 31 skipped sites across DEBUGADV and eight ordinary ADV scenes, reducing the
effectful fallthrough inventory from 23 to 22 distinct opcodes. Focused tests cover dispatch, percentages,
odd/even period timing, continuous-presentation classification, and matrix order; the full engine suite,
warning-free Godot build, and Himegari-targeted threaded selftest pass (468 engine tests).
After `0x233`, implement CONFIG's mixer ABI as one four-opcode tranche rather than piecemeal: **NEXT:** implement CONFIG's mixer ABI as one four-opcode tranche rather than piecemeal:
`0xc5` gets master/music/SFX/voice/movie volume, `0xc6` sets and applies it, `0x1ba` enables/disables the `0xc5` gets master/music/SFX/voice/movie volume, `0xc6` sets and applies it, `0x1ba` enables/disables the
four non-master routes, and `0xc7` queries route state. Those 37 CONFIG sites require shared persistent four non-master routes, and `0xc7` queries route state. Those 37 CONFIG sites require shared persistent
settings plus live Godot-bus application; implementing only a setter or getter would leave the menu settings plus live Godot-bus application; implementing only a setter or getter would leave the menu

View File

@@ -129,6 +129,11 @@ public class GfxAnimationTests
rotation.SetRotationCycle(7, 1000, (0, 0, 1)); rotation.SetRotationCycle(7, 1000, (0, 0, 1));
Assert.True(rotation.HasActiveVisualPresentation(1000)); Assert.True(rotation.HasActiveVisualPresentation(1000));
Assert.True(rotation.SnapshotVisibleObjects(1000).Single().TimeVarying); Assert.True(rotation.SnapshotVisibleObjects(1000).Single().TimeVarying);
var scale = VisibleObject();
scale.SetScaleCycle(7, 1000, (150, 75, 100));
Assert.True(scale.HasActiveVisualPresentation(1000));
Assert.True(scale.SnapshotVisibleObjects(1000).Single().TimeVarying);
} }
[Fact] [Fact]
@@ -367,6 +372,27 @@ public class GfxAnimationTests
Assert.Equal(1, vm.Gfx.AnimClockGeneration); Assert.Equal(1, vm.Gfx.AnimClockGeneration);
} }
[Fact]
public void Op0x233_ConfiguresAnIndependentCyclicScaleChannel()
{
var t = T();
var scene = ScriptAssembler.Assemble(t, "SCALE_CYCLE", new List<(int, Operand[])>
{
MovGI(1, 0x1000), MovGI(2, 1200), MovGI(3, 80), MovGI(4, 120), MovGI(5, 100),
(0x233, new[] { G(1), G(2), G(3), G(4), G(5) }),
Exit(),
}, System.Array.Empty<string>());
var vm = new VirtualMachine(scene, t, new RecordingHost());
vm.Run();
var o = vm.Gfx.TryGet(0x1000)!;
Assert.Equal(1200, o.ScaleCyclePeriodMs);
Assert.Equal((0.8, 1.2, 1.0), o.ScaleCycleTarget);
Assert.True(o.ScaleCycleEnabled);
Assert.Equal((1.0, 1.0, 1.0), o.ScaleCurrent);
}
[Fact] [Fact]
public void CurrentTranslationSetter_ReplacesTheLiveMatrixImmediately() public void CurrentTranslationSetter_ReplacesTheLiveMatrixImmediately()
{ {
@@ -456,6 +482,50 @@ public class GfxAnimationTests
Assert.Equal(255, done.Alpha); // scale-Z=3, translation-Z=99, rotation-axis-Z=5: still opaque Assert.Equal(255, done.Alpha); // scale-Z=3, translation-Z=99, rotation-axis-Z=5: still opaque
} }
[Fact]
public void SnapshotSamplesScaleCycleWithNativeTriangularPhase()
{
var g = new GfxState();
g.SetSurface(1, 5, -1);
g.BindDraw(7, 1, 0, 0, 64, 64, 0, 0);
g.SetScaleCycle(7, 1200, (200, 50, 100));
var start = g.SnapshotVisibleObjects(1000).Single().ScaleCycle;
var quarter = g.SnapshotVisibleObjects(1300).Single().ScaleCycle;
var midpoint = g.SnapshotVisibleObjects(1600).Single().ScaleCycle;
var threeQuarter = g.SnapshotVisibleObjects(1900).Single().ScaleCycle;
var wrapped = g.SnapshotVisibleObjects(2200).Single().ScaleCycle;
Assert.Equal((1.0, 1.0, 1.0), (start.ScaleX, start.ScaleY, start.ScaleZ));
Assert.Equal((1.5, 0.75, 1.0), (quarter.ScaleX, quarter.ScaleY, quarter.ScaleZ));
Assert.Equal((2.0, 0.5, 1.0), (midpoint.ScaleX, midpoint.ScaleY, midpoint.ScaleZ));
Assert.Equal((1.5, 0.75, 1.0), (threeQuarter.ScaleX, threeQuarter.ScaleY, threeQuarter.ScaleZ));
Assert.Equal((1.0, 1.0, 1.0), (wrapped.ScaleX, wrapped.ScaleY, wrapped.ScaleZ));
g.SetScaleCycle(7, 5, (200, 100, 100));
_ = g.SnapshotVisibleObjects(3000);
var oddPeriod = g.SnapshotVisibleObjects(3002).Single().ScaleCycle;
Assert.Equal(1.8, oddPeriod.ScaleX, 10);
}
[Fact]
public void ScaleCycleClonePreservesTargetAndSharedPhase()
{
var g = new GfxState();
g.SetSurface(1, 5, -1);
g.BindDraw(7, 1, 0, 0, 64, 64, 0, 0);
g.SetScaleCycle(7, 1000, (160, 80, 100));
_ = g.SnapshotVisibleObjects(2000);
Assert.True(g.CloneObject(7, 8));
var samples = g.SnapshotVisibleObjects(2250);
var source = samples.Single(x => x.Handle == 7).ScaleCycle;
var clone = samples.Single(x => x.Handle == 8).ScaleCycle;
Assert.Equal(source, clone);
Assert.Equal((1.3, 0.9, 1.0), (clone.ScaleX, clone.ScaleY, clone.ScaleZ));
}
[Fact] [Fact]
public void Transform2D_UsesNativeAnchoredRowVectorOrder_AndDirectProjection() public void Transform2D_UsesNativeAnchoredRowVectorOrder_AndDirectProjection()
{ {
@@ -491,4 +561,17 @@ public class GfxAnimationTests
Assert.Equal(100.0, p.X, 10); Assert.Equal(100.0, p.X, 10);
Assert.Equal(100.0, p.Y, 10); Assert.Equal(100.0, p.Y, 10);
} }
[Fact]
public void Transform2D_CyclicScaleOccursAfterTranslationAndBeforeCyclicRotation()
{
var t = new TransformState(2, 1, 1, 10, 0, 0, 100, 50, 0);
var scale = new ScaleCycleState(true, 1000, 3, 1, 1);
var rotation = new RotationCycleState(true, 1000, 0, 0, 1, 90);
var p = Transform2DMath.Apply(120, 50, t, rotation, scale);
Assert.Equal(100.0, p.X, 10);
Assert.Equal(200.0, p.Y, 10);
}
} }

View File

@@ -14,6 +14,9 @@ public readonly record struct RotationCycleState(bool Enabled, long PeriodMs,
double AxisX, double AxisY, double AxisZ, double AxisX, double AxisY, double AxisZ,
double AngleDegrees = 0); double AngleDegrees = 0);
public readonly record struct ScaleCycleState(bool Enabled, long PeriodMs,
double ScaleX, double ScaleY, double ScaleZ);
[System.Flags] [System.Flags]
public enum GfxPresentationReason public enum GfxPresentationReason
{ {
@@ -82,7 +85,8 @@ public readonly record struct RenderObject(long Handle, long SurfaceResId, long
SurfaceTransitionState? SurfaceTransition = null, SurfaceTransitionState? SurfaceTransition = null,
ColorTransitionState? ColorTransition = null, ColorTransitionState? ColorTransition = null,
Affine2D? RangeTransform = null, Affine2D? RangeTransform = null,
bool TimeVarying = false); bool TimeVarying = false,
ScaleCycleState ScaleCycle = default);
/// <summary>The retained handle interval selected by an op-0x222 backbuffer publication.</summary> /// <summary>The retained handle interval selected by an op-0x222 backbuffer publication.</summary>
public readonly record struct GfxHandleRange(long First, long Count) public readonly record struct GfxHandleRange(long First, long Count)
@@ -172,6 +176,12 @@ public sealed class GfxState
public (long X, long Y, long Z) RotationAxis; public (long X, long Y, long Z) RotationAxis;
public bool RotationEnabled; public bool RotationEnabled;
public long RotationStartMs = -1; public long RotationStartMs = -1;
// Op 0x233 is a separate cyclic scale channel (period obj+0x224, target matrix obj+0x250).
public long ScaleCyclePeriodMs;
public (double X, double Y, double Z) ScaleCycleTarget = (1, 1, 1);
public bool ScaleCycleEnabled;
public long ScaleCycleStartMs = -1;
} }
// ---- geometry/draw object store (V18/V24/draw bind, the compositor's input) ---- // ---- geometry/draw object store (V18/V24/draw bind, the compositor's input) ----
@@ -487,6 +497,8 @@ public sealed class GfxState
OneShotStartMs = s.OneShotStartMs, 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,
ScaleCyclePeriodMs = s.ScaleCyclePeriodMs, ScaleCycleTarget = s.ScaleCycleTarget,
ScaleCycleEnabled = s.ScaleCycleEnabled, ScaleCycleStartMs = s.ScaleCycleStartMs,
}; };
private readonly object _lock = new(); private readonly object _lock = new();
@@ -851,6 +863,7 @@ public sealed class GfxState
o.TranslationEnabled || o.TranslationEnabled ||
(o.SrcAnim && o.SrcPeriod > 0) || (o.SrcAnim && o.SrcPeriod > 0) ||
(o.ColorAnim && o.ColorPeriod > 0) || (o.ColorAnim && o.ColorPeriod > 0) ||
(o.ScaleCycleEnabled && o.ScaleCyclePeriodMs > 0) ||
(o.RotationEnabled && o.RotationPeriodMs > 0))); (o.RotationEnabled && o.RotationPeriodMs > 0)));
} }
@@ -1071,6 +1084,19 @@ public sealed class GfxState
} }
} }
/// <summary>Op 0x233: cyclic identity-to-target scale, sampled with a triangular ping-pong phase.</summary>
public void SetScaleCycle(long handle, long periodMs, (long X, long Y, long Z) percent)
{
lock (_lock)
{
var o = GetOrCreate(handle);
o.ScaleCyclePeriodMs = periodMs;
o.ScaleCycleTarget = (percent.X / 100.0, percent.Y / 100.0, percent.Z / 100.0);
o.ScaleCycleEnabled = periodMs > 0;
o.ScaleCycleStartMs = -1;
}
}
/// <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)
{ {
@@ -1138,6 +1164,7 @@ public sealed class GfxState
(o.OneShotColorEnabled || o.ScaleEnabled || o.RotationChannelEnabled || (o.OneShotColorEnabled || o.ScaleEnabled || o.RotationChannelEnabled ||
o.TranslationEnabled || o.TranslationEnabled ||
(o.ColorAnim && o.ColorPeriod > 0) || (o.ColorAnim && o.ColorPeriod > 0) ||
(o.ScaleCycleEnabled && o.ScaleCyclePeriodMs > 0) ||
(o.RotationEnabled && o.RotationPeriodMs > 0)))) (o.RotationEnabled && o.RotationPeriodMs > 0))))
reasons |= GfxPresentationReason.ContinuousChannel; reasons |= GfxPresentationReason.ContinuousChannel;
@@ -1328,6 +1355,15 @@ public sealed class GfxState
} }
double cycleAngle = 0; double cycleAngle = 0;
double cycleScaleX = 1, cycleScaleY = 1, cycleScaleZ = 1;
if (o.ScaleCycleEnabled && o.ScaleCyclePeriodMs > 0)
{
if (o.ScaleCycleStartMs < 0) o.ScaleCycleStartMs = nowMs;
double weight = ScaleCycleWeight(nowMs, o.ScaleCycleStartMs, o.ScaleCyclePeriodMs);
cycleScaleX += (o.ScaleCycleTarget.X - 1) * weight;
cycleScaleY += (o.ScaleCycleTarget.Y - 1) * weight;
cycleScaleZ += (o.ScaleCycleTarget.Z - 1) * weight;
}
if (o.RotationEnabled && o.RotationPeriodMs > 0) if (o.RotationEnabled && o.RotationPeriodMs > 0)
{ {
if (o.RotationStartMs < 0) o.RotationStartMs = nowMs; if (o.RotationStartMs < 0) o.RotationStartMs = nowMs;
@@ -1345,6 +1381,7 @@ public sealed class GfxState
o.TranslationEnabled || o.TranslationEnabled ||
(o.SrcAnim && o.SrcPeriod > 0) || (o.SrcAnim && o.SrcPeriod > 0) ||
(o.ColorAnim && o.ColorPeriod > 0) || (o.ColorAnim && o.ColorPeriod > 0) ||
(o.ScaleCycleEnabled && o.ScaleCyclePeriodMs > 0) ||
(o.RotationEnabled && o.RotationPeriodMs > 0) || (o.RotationEnabled && o.RotationPeriodMs > 0) ||
transition is { Progress: < 1.0 } || transition is { Progress: < 1.0 } ||
(objectRangeTransform != null && rangeTimeVarying); (objectRangeTransform != null && rangeTimeVarying);
@@ -1358,7 +1395,9 @@ public sealed class GfxState
o.RotationAxis.X, o.RotationAxis.Y, o.RotationAxis.X, o.RotationAxis.Y,
o.RotationAxis.Z, cycleAngle), o.RotationAxis.Z, cycleAngle),
alpha, tint, strength, blend, multiplyTint, transition, alpha, tint, strength, blend, multiplyTint, transition,
colorTransition, objectRangeTransform, timeVarying)); colorTransition, objectRangeTransform, timeVarying,
new ScaleCycleState(o.ScaleCycleEnabled, o.ScaleCyclePeriodMs,
cycleScaleX, cycleScaleY, cycleScaleZ)));
} }
} }
} }
@@ -1458,6 +1497,14 @@ public sealed class GfxState
return (double)BlendMath.PingPong(now, start, period) / half; return (double)BlendMath.PingPong(now, start, period) / half;
} }
private static double ScaleCycleWeight(long now, long start, long period)
{
if (period <= 0) return 0;
long elapsed = System.Math.Max(0, now - start);
long position = elapsed % period;
return 2.0 * System.Math.Min(position, period - position) / period;
}
/// <summary>Pack (alpha, rgb) → 0xAARRGGBB, matching op 0x202/0x203's handler bit-manipulation for the /// <summary>Pack (alpha, rgb) → 0xAARRGGBB, matching op 0x202/0x203's handler bit-manipulation for the
/// common (non-negative-sentinel) case. The alpha&lt;0 / color&lt;0 native-fetch path is deferred.</summary> /// common (non-negative-sentinel) case. The alpha&lt;0 / color&lt;0 native-fetch path is deferred.</summary>
public static long PackColor(long alpha, long color) public static long PackColor(long alpha, long color)

View File

@@ -48,25 +48,29 @@ public readonly record struct Affine2D(double XX, double XY, double YX, double Y
} }
/// <summary>Exact 2D projection of AGE's row-vector retained-object matrix. Native call order is anchored /// <summary>Exact 2D projection of AGE's row-vector retained-object matrix. Native call order is anchored
/// scale, one-shot axis-angle rotation, translation, then separately anchored cyclic rotation. The adjacent /// one-shot scale/axis-angle rotation/translation, then separately anchored cyclic scale and cyclic rotation.
/// anchor translations cancel, yielding T(-a)*S*R1*T*Rcycle*T(+a). Z is projected away only afterward.</summary> /// The adjacent anchor translations cancel, yielding T(-a)*S*R1*T*Scycle*Rcycle*T(+a). Z is projected away
/// only afterward.</summary>
public static class Transform2DMath public static class Transform2DMath
{ {
public static Affine2D Build(TransformState t, RotationCycleState cycle = default) public static Affine2D Build(TransformState t, RotationCycleState cycle = default,
ScaleCycleState scaleCycle = default)
{ {
Matrix3D m = Identity(); Matrix3D m = Identity();
m = Mul(m, Translation(-t.AnchorX, -t.AnchorY, -t.AnchorZ)); m = Mul(m, Translation(-t.AnchorX, -t.AnchorY, -t.AnchorZ));
m = Mul(m, Scale(t.ScaleX, t.ScaleY, t.ScaleZ)); m = Mul(m, Scale(t.ScaleX, t.ScaleY, t.ScaleZ));
m = Mul(m, AxisAngle(t.RotationAxisX, t.RotationAxisY, t.RotationAxisZ, t.RotationAngleDegrees)); m = Mul(m, AxisAngle(t.RotationAxisX, t.RotationAxisY, t.RotationAxisZ, t.RotationAngleDegrees));
m = Mul(m, Translation(t.TranslateX, t.TranslateY, t.TranslateZ)); m = Mul(m, Translation(t.TranslateX, t.TranslateY, t.TranslateZ));
if (scaleCycle.Enabled) m = Mul(m, Scale(scaleCycle.ScaleX, scaleCycle.ScaleY, scaleCycle.ScaleZ));
if (cycle.Enabled) m = Mul(m, AxisAngle(cycle.AxisX, cycle.AxisY, cycle.AxisZ, cycle.AngleDegrees)); if (cycle.Enabled) m = Mul(m, AxisAngle(cycle.AxisX, cycle.AxisY, cycle.AxisZ, cycle.AngleDegrees));
m = Mul(m, Translation(t.AnchorX, t.AnchorY, t.AnchorZ)); m = Mul(m, Translation(t.AnchorX, t.AnchorY, t.AnchorZ));
return new(m.M11, m.M12, m.M21, m.M22, m.TX, m.TY); return new(m.M11, m.M12, m.M21, m.M22, m.TX, m.TY);
} }
public static (double X, double Y) Apply(double x, double y, TransformState transform, public static (double X, double Y) Apply(double x, double y, TransformState transform,
RotationCycleState cycle = default) RotationCycleState cycle = default,
=> Build(transform, cycle).Apply(x, y); ScaleCycleState scaleCycle = default)
=> Build(transform, cycle, scaleCycle).Apply(x, y);
// AGE composes affine 4x4 row-vector matrices, whose last column is always (0,0,0,1). Carry only // AGE composes affine 4x4 row-vector matrices, whose last column is always (0,0,0,1). Carry only
// the 3x3 linear part and translation row as a value type: the old double[16] implementation allocated // the 3x3 linear part and translation row as a value type: the old double[16] implementation allocated

View File

@@ -73,7 +73,7 @@ internal static class NativeGfxPersistenceCodec
|| value.RotationChannelEnabled || value.TranslationEnabled || value.RotationChannelEnabled || value.TranslationEnabled
? flags | 2 ? flags | 2
: flags & ~2; : flags & ~2;
flags = value.RotationEnabled || value.SrcAnim || value.ColorAnim flags = value.ScaleCycleEnabled || value.RotationEnabled || value.SrcAnim || value.ColorAnim
? flags | 4 ? flags | 4
: flags & ~4; : flags & ~4;
WriteInt(raw, 0, flags); WriteInt(raw, 0, flags);
@@ -111,8 +111,10 @@ internal static class NativeGfxPersistenceCodec
WriteFloat(raw, 0x204, value.RotationCurrent.Angle); WriteFloat(raw, 0x204, value.RotationCurrent.Angle);
WriteFloat(raw, 0x208, value.RotationTarget.Angle); WriteFloat(raw, 0x208, value.RotationTarget.Angle);
WriteInt(raw, 0x20c, EncodeNativeStart(value.ColorStart)); WriteInt(raw, 0x20c, EncodeNativeStart(value.ColorStart));
WriteInt(raw, 0x210, EncodeNativeStart(value.ScaleCycleStartMs));
WriteInt(raw, 0x214, EncodeNativeStart(value.RotationStartMs)); WriteInt(raw, 0x214, EncodeNativeStart(value.RotationStartMs));
WriteInt(raw, 0x220, unchecked((int)value.ColorPeriod)); WriteInt(raw, 0x220, unchecked((int)value.ColorPeriod));
WriteInt(raw, 0x224, unchecked((int)value.ScaleCyclePeriodMs));
WriteInt(raw, 0x228, unchecked((int)value.RotationPeriodMs)); WriteInt(raw, 0x228, unchecked((int)value.RotationPeriodMs));
WriteInt(raw, 0x230, unchecked((int)value.SrcPeriod)); WriteInt(raw, 0x230, unchecked((int)value.SrcPeriod));
WriteInt(raw, 0x234, unchecked((int)value.SrcCell)); WriteInt(raw, 0x234, unchecked((int)value.SrcCell));
@@ -121,6 +123,7 @@ internal static class NativeGfxPersistenceCodec
if (value.ColorAnim) if (value.ColorAnim)
WriteInt(raw, 0x240, unchecked((int)value.ColorTarget)); WriteInt(raw, 0x240, unchecked((int)value.ColorTarget));
WriteVector(raw, 0x244, value.RotationAxis); WriteVector(raw, 0x244, value.RotationAxis);
WriteScaleMatrix(raw, 0x250, value.ScaleCycleTarget);
WriteInt(raw, 0x2d0, unchecked((int)value.OneShotAnimationControlFlags)); WriteInt(raw, 0x2d0, unchecked((int)value.OneShotAnimationControlFlags));
return raw; return raw;
} }
@@ -167,8 +170,10 @@ internal static class NativeGfxPersistenceCodec
ReadFloat(raw, 0x1f8), ReadFloat(raw, 0x1fc), ReadFloat(raw, 0x200), ReadFloat(raw, 0x1f8), ReadFloat(raw, 0x1fc), ReadFloat(raw, 0x200),
ReadFloat(raw, 0x208)), ReadFloat(raw, 0x208)),
ColorStart = DecodeNativeStart(ReadInt(raw, 0x20c)), ColorStart = DecodeNativeStart(ReadInt(raw, 0x20c)),
ScaleCycleStartMs = DecodeNativeStart(ReadInt(raw, 0x210)),
RotationStartMs = DecodeNativeStart(ReadInt(raw, 0x214)), RotationStartMs = DecodeNativeStart(ReadInt(raw, 0x214)),
ColorPeriod = ReadInt(raw, 0x220), ColorPeriod = ReadInt(raw, 0x220),
ScaleCyclePeriodMs = ReadInt(raw, 0x224),
RotationPeriodMs = ReadInt(raw, 0x228), RotationPeriodMs = ReadInt(raw, 0x228),
SrcPeriod = ReadInt(raw, 0x230), SrcPeriod = ReadInt(raw, 0x230),
SrcCell = ReadInt(raw, 0x234), SrcCell = ReadInt(raw, 0x234),
@@ -176,7 +181,9 @@ internal static class NativeGfxPersistenceCodec
SrcColumns = Math.Max(1, ReadInt(raw, 0x23c)), SrcColumns = Math.Max(1, ReadInt(raw, 0x23c)),
ColorTarget = unchecked((uint)ReadInt(raw, 0x240)), ColorTarget = unchecked((uint)ReadInt(raw, 0x240)),
RotationAxis = ReadLongVector(raw, 0x244), RotationAxis = ReadLongVector(raw, 0x244),
ScaleCycleTarget = ReadScale(raw, 0x250),
OneShotAnimationControlFlags = unchecked((uint)ReadInt(raw, 0x2d0)), OneShotAnimationControlFlags = unchecked((uint)ReadInt(raw, 0x2d0)),
ScaleCycleEnabled = (flags & 4) != 0 && ReadInt(raw, 0x224) > 0,
RotationEnabled = (flags & 4) != 0 && ReadInt(raw, 0x228) > 0, RotationEnabled = (flags & 4) != 0 && ReadInt(raw, 0x228) > 0,
SrcAnim = (flags & 4) != 0 && ReadInt(raw, 0x238) > 1, SrcAnim = (flags & 4) != 0 && ReadInt(raw, 0x238) > 1,
ColorAnim = (flags & 4) != 0 && ReadInt(raw, 0x220) > 0, ColorAnim = (flags & 4) != 0 && ReadInt(raw, 0x220) > 0,

View File

@@ -28,7 +28,8 @@ public static class RetainedSurfaceRasterizer
TransformState transform = item.Transform; TransformState transform = item.Transform;
Affine2D localToDestination = Affine2D localToDestination =
Transform2DMath.Build(transform, item.Rotation).FromLocalOrigin(item.DstX, item.DstY); Transform2DMath.Build(transform, item.Rotation, item.ScaleCycle)
.FromLocalOrigin(item.DstX, item.DstY);
if (item.RangeTransform is { } rangeTransform) if (item.RangeTransform is { } rangeTransform)
localToDestination = localToDestination.Then(rangeTransform); localToDestination = localToDestination.Then(rangeTransform);

View File

@@ -2517,6 +2517,9 @@ public sealed class VirtualMachine
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
Gfx.SetColorAnimResolved(Read(a[0]), Read(a[1]), Read(a[2]), Read(a[3])); return pc + 1; Gfx.SetColorAnimResolved(Read(a[0]), Read(a[1]), Read(a[2]), Read(a[3])); return pc + 1;
case "set-scale-cycle": // 0x233 (handle)(period ms)(target scale x/y/z percent)
Gfx.SetScaleCycle(Read(a[0]), Read(a[1]), (Read(a[2]), Read(a[3]), Read(a[4])));
return pc + 1;
case "u00421940": // 0x228: (succ)(handle)(outX)(outY)(outZ) <- target translation matrix case "u00421940": // 0x228: (succ)(handle)(outX)(outY)(outZ) <- target translation matrix
{ {
if (Gfx.TryQueryTranslationTarget(Read(a[1]), out var v)) if (Gfx.TryQueryTranslationTarget(Read(a[1]), out var v))

View File

@@ -1438,7 +1438,8 @@ public sealed class GodotAdvHost : IHost
if (source == null) continue; if (source == null) continue;
Affine2D localToTarget = Affine2D localToTarget =
Transform2DMath.Build(item.Transform, item.Rotation).FromLocalOrigin(item.DstX, item.DstY); Transform2DMath.Build(item.Transform, item.Rotation, item.ScaleCycle)
.FromLocalOrigin(item.DstX, item.DstY);
if (item.RangeTransform is { } rangeTransform) if (item.RangeTransform is { } rangeTransform)
localToTarget = localToTarget.Then(rangeTransform); localToTarget = localToTarget.Then(rangeTransform);
foreach (SurfaceTextDraw draw in source) foreach (SurfaceTextDraw draw in source)

View File

@@ -1009,7 +1009,8 @@ public partial class Main : Godot.Control
foreach (var v in _visibleSnapshot) foreach (var v in _visibleSnapshot)
{ {
_perf?.RecordObject(v.TimeVarying); _perf?.RecordObject(v.TimeVarying);
var affine = Transform2DMath.Build(v.Transform, v.Rotation).FromLocalOrigin(v.DstX, v.DstY); var affine = Transform2DMath.Build(v.Transform, v.Rotation, v.ScaleCycle)
.FromLocalOrigin(v.DstX, v.DstY);
if (v.RangeTransform is { } rangeTransform) affine = affine.Then(rangeTransform); if (v.RangeTransform is { } rangeTransform) affine = affine.Then(rangeTransform);
float opacity = v.Alpha / 255f; float opacity = v.Alpha / 255f;
var rawObject = _vm.Gfx.TryGet(v.Handle); var rawObject = _vm.Gfx.TryGet(v.Handle);
@@ -1102,7 +1103,7 @@ public partial class Main : Godot.Control
if (source.Handle < transition.RangeBStart || source.Handle >= end || source.SurfaceTransition != null) if (source.Handle < transition.RangeBStart || source.Handle >= end || source.SurfaceTransition != null)
continue; continue;
_perf?.RecordObject(source.TimeVarying); _perf?.RecordObject(source.TimeVarying);
var affine = Transform2DMath.Build(source.Transform, source.Rotation) var affine = Transform2DMath.Build(source.Transform, source.Rotation, source.ScaleCycle)
.FromLocalOrigin(source.DstX, source.DstY); .FromLocalOrigin(source.DstX, source.DstY);
if (source.RangeTransform is { } rangeTransform) affine = affine.Then(rangeTransform); if (source.RangeTransform is { } rangeTransform) affine = affine.Then(rangeTransform);
float opacity = source.Alpha / 255f * (float)transition.Progress; float opacity = source.Alpha / 255f * (float)transition.Progress;
@@ -1226,7 +1227,7 @@ public partial class Main : Godot.Control
{ {
_perf?.RecordObject(v.TimeVarying); _perf?.RecordObject(v.TimeVarying);
var t = v.Transform; var t = v.Transform;
var affine = Age.Engine.Model.Transform2DMath.Build(t, v.Rotation); var affine = Age.Engine.Model.Transform2DMath.Build(t, v.Rotation, v.ScaleCycle);
var localToDest = affine.FromLocalOrigin(v.DstX, v.DstY); var localToDest = affine.FromLocalOrigin(v.DstX, v.DstY);
if (v.RangeTransform is { } rangeTransform) if (v.RangeTransform is { } rangeTransform)
localToDest = localToDest.Then(rangeTransform); localToDest = localToDest.Then(rangeTransform);
@@ -1602,7 +1603,8 @@ public partial class Main : Godot.Control
if (source.Handle < transition.RangeBStart || source.Handle >= end || source.SurfaceTransition != null) if (source.Handle < transition.RangeBStart || source.Handle >= end || source.SurfaceTransition != null)
continue; continue;
_perf?.RecordObject(source.TimeVarying); _perf?.RecordObject(source.TimeVarying);
var affine = Transform2DMath.Build(source.Transform, source.Rotation).FromLocalOrigin(source.DstX, source.DstY); var affine = Transform2DMath.Build(source.Transform, source.Rotation, source.ScaleCycle)
.FromLocalOrigin(source.DstX, source.DstY);
if (source.RangeTransform is { } rangeTransform) if (source.RangeTransform is { } rangeTransform)
affine = affine.Then(rangeTransform); affine = affine.Then(rangeTransform);
float opacity = source.Alpha / 255f * (float)transition.Progress; float opacity = source.Alpha / 255f * (float)transition.Progress;

View File

@@ -6169,6 +6169,7 @@ source = "investigation"
confidence = "high" confidence = "high"
depends_on = [] depends_on = []
evidence = "Ghidra /v2: op_0x233_set_scale_cycle@0x423cf0 divides operands 3..5 by 100 and calls gfx_object_set_scale_cycle@0x47efd0. The worker stores start=0 at obj+0x210, period at +0x224, and the target scale matrix at +0x250. gfx_object_anim_interpolate@0x473ed0 samples a triangular phase 2*min(elapsed%period,period-elapsed%period)/period, linearly blends identity to target, and multiplies the result into the separately anchored cyclic-animation product. Corpus: 31 sites in nine scripts, including eight ordinary ADV scenes." evidence = "Ghidra /v2: op_0x233_set_scale_cycle@0x423cf0 divides operands 3..5 by 100 and calls gfx_object_set_scale_cycle@0x47efd0. The worker stores start=0 at obj+0x210, period at +0x224, and the target scale matrix at +0x250. gfx_object_anim_interpolate@0x473ed0 samples a triangular phase 2*min(elapsed%period,period-elapsed%period)/period, linearly blends identity to target, and multiplies the result into the separately anchored cyclic-animation product. Corpus: 31 sites in nine scripts, including eight ordinary ADV scenes."
details = "Port status (2026-07-28): implemented as an independent retained-object scale cycle. The compositor samples the exact triangular phase, keeps the channel frame-driven during waits, and composes it after one-shot translation but before the sibling cyclic rotation."
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 1 i = 1