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

@@ -14,6 +14,9 @@ public readonly record struct RotationCycleState(bool Enabled, long PeriodMs,
double AxisX, double AxisY, double AxisZ,
double AngleDegrees = 0);
public readonly record struct ScaleCycleState(bool Enabled, long PeriodMs,
double ScaleX, double ScaleY, double ScaleZ);
[System.Flags]
public enum GfxPresentationReason
{
@@ -82,7 +85,8 @@ public readonly record struct RenderObject(long Handle, long SurfaceResId, long
SurfaceTransitionState? SurfaceTransition = null,
ColorTransitionState? ColorTransition = 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>
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 bool RotationEnabled;
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) ----
@@ -487,6 +497,8 @@ public sealed class GfxState
OneShotStartMs = s.OneShotStartMs,
RotationPeriodMs = s.RotationPeriodMs, RotationAxis = s.RotationAxis,
RotationEnabled = s.RotationEnabled, RotationStartMs = s.RotationStartMs,
ScaleCyclePeriodMs = s.ScaleCyclePeriodMs, ScaleCycleTarget = s.ScaleCycleTarget,
ScaleCycleEnabled = s.ScaleCycleEnabled, ScaleCycleStartMs = s.ScaleCycleStartMs,
};
private readonly object _lock = new();
@@ -851,6 +863,7 @@ public sealed class GfxState
o.TranslationEnabled ||
(o.SrcAnim && o.SrcPeriod > 0) ||
(o.ColorAnim && o.ColorPeriod > 0) ||
(o.ScaleCycleEnabled && o.ScaleCyclePeriodMs > 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>
public void SetAnimClock(long durationTicks)
{
@@ -1138,6 +1164,7 @@ public sealed class GfxState
(o.OneShotColorEnabled || o.ScaleEnabled || o.RotationChannelEnabled ||
o.TranslationEnabled ||
(o.ColorAnim && o.ColorPeriod > 0) ||
(o.ScaleCycleEnabled && o.ScaleCyclePeriodMs > 0) ||
(o.RotationEnabled && o.RotationPeriodMs > 0))))
reasons |= GfxPresentationReason.ContinuousChannel;
@@ -1328,6 +1355,15 @@ public sealed class GfxState
}
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.RotationStartMs < 0) o.RotationStartMs = nowMs;
@@ -1345,6 +1381,7 @@ public sealed class GfxState
o.TranslationEnabled ||
(o.SrcAnim && o.SrcPeriod > 0) ||
(o.ColorAnim && o.ColorPeriod > 0) ||
(o.ScaleCycleEnabled && o.ScaleCyclePeriodMs > 0) ||
(o.RotationEnabled && o.RotationPeriodMs > 0) ||
transition is { Progress: < 1.0 } ||
(objectRangeTransform != null && rangeTimeVarying);
@@ -1358,7 +1395,9 @@ public sealed class GfxState
o.RotationAxis.X, o.RotationAxis.Y,
o.RotationAxis.Z, cycleAngle),
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;
}
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
/// 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)

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
/// scale, one-shot axis-angle rotation, translation, then separately anchored cyclic rotation. The adjacent
/// anchor translations cancel, yielding T(-a)*S*R1*T*Rcycle*T(+a). Z is projected away only afterward.</summary>
/// one-shot scale/axis-angle rotation/translation, then separately anchored cyclic scale and cyclic rotation.
/// 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 Affine2D Build(TransformState t, RotationCycleState cycle = default)
public static Affine2D Build(TransformState t, RotationCycleState cycle = default,
ScaleCycleState scaleCycle = default)
{
Matrix3D m = Identity();
m = Mul(m, Translation(-t.AnchorX, -t.AnchorY, -t.AnchorZ));
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, 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));
m = Mul(m, Translation(t.AnchorX, t.AnchorY, t.AnchorZ));
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,
RotationCycleState cycle = default)
=> Build(transform, cycle).Apply(x, y);
RotationCycleState cycle = default,
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
// 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
? flags | 2
: flags & ~2;
flags = value.RotationEnabled || value.SrcAnim || value.ColorAnim
flags = value.ScaleCycleEnabled || value.RotationEnabled || value.SrcAnim || value.ColorAnim
? flags | 4
: flags & ~4;
WriteInt(raw, 0, flags);
@@ -111,8 +111,10 @@ internal static class NativeGfxPersistenceCodec
WriteFloat(raw, 0x204, value.RotationCurrent.Angle);
WriteFloat(raw, 0x208, value.RotationTarget.Angle);
WriteInt(raw, 0x20c, EncodeNativeStart(value.ColorStart));
WriteInt(raw, 0x210, EncodeNativeStart(value.ScaleCycleStartMs));
WriteInt(raw, 0x214, EncodeNativeStart(value.RotationStartMs));
WriteInt(raw, 0x220, unchecked((int)value.ColorPeriod));
WriteInt(raw, 0x224, unchecked((int)value.ScaleCyclePeriodMs));
WriteInt(raw, 0x228, unchecked((int)value.RotationPeriodMs));
WriteInt(raw, 0x230, unchecked((int)value.SrcPeriod));
WriteInt(raw, 0x234, unchecked((int)value.SrcCell));
@@ -121,6 +123,7 @@ internal static class NativeGfxPersistenceCodec
if (value.ColorAnim)
WriteInt(raw, 0x240, unchecked((int)value.ColorTarget));
WriteVector(raw, 0x244, value.RotationAxis);
WriteScaleMatrix(raw, 0x250, value.ScaleCycleTarget);
WriteInt(raw, 0x2d0, unchecked((int)value.OneShotAnimationControlFlags));
return raw;
}
@@ -167,8 +170,10 @@ internal static class NativeGfxPersistenceCodec
ReadFloat(raw, 0x1f8), ReadFloat(raw, 0x1fc), ReadFloat(raw, 0x200),
ReadFloat(raw, 0x208)),
ColorStart = DecodeNativeStart(ReadInt(raw, 0x20c)),
ScaleCycleStartMs = DecodeNativeStart(ReadInt(raw, 0x210)),
RotationStartMs = DecodeNativeStart(ReadInt(raw, 0x214)),
ColorPeriod = ReadInt(raw, 0x220),
ScaleCyclePeriodMs = ReadInt(raw, 0x224),
RotationPeriodMs = ReadInt(raw, 0x228),
SrcPeriod = ReadInt(raw, 0x230),
SrcCell = ReadInt(raw, 0x234),
@@ -176,7 +181,9 @@ internal static class NativeGfxPersistenceCodec
SrcColumns = Math.Max(1, ReadInt(raw, 0x23c)),
ColorTarget = unchecked((uint)ReadInt(raw, 0x240)),
RotationAxis = ReadLongVector(raw, 0x244),
ScaleCycleTarget = ReadScale(raw, 0x250),
OneShotAnimationControlFlags = unchecked((uint)ReadInt(raw, 0x2d0)),
ScaleCycleEnabled = (flags & 4) != 0 && ReadInt(raw, 0x224) > 0,
RotationEnabled = (flags & 4) != 0 && ReadInt(raw, 0x228) > 0,
SrcAnim = (flags & 4) != 0 && ReadInt(raw, 0x238) > 1,
ColorAnim = (flags & 4) != 0 && ReadInt(raw, 0x220) > 0,

View File

@@ -28,7 +28,8 @@ public static class RetainedSurfaceRasterizer
TransformState transform = item.Transform;
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)
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;
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;
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
{
if (Gfx.TryQueryTranslationTarget(Read(a[1]), out var v))