Implement cyclic scale opcode
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<0 / color<0 native-fetch path is deferred.</summary>
|
/// common (non-negative-sentinel) case. The alpha<0 / color<0 native-fetch path is deferred.</summary>
|
||||||
public static long PackColor(long alpha, long color)
|
public static long PackColor(long alpha, long color)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user