Implement cyclic scale opcode
This commit is contained in:
@@ -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<0 / color<0 native-fetch path is deferred.</summary>
|
||||
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
|
||||
/// 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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user