Profile and optimize retained rendering
This commit is contained in:
@@ -14,6 +14,15 @@ public readonly record struct RotationCycleState(bool Enabled, long PeriodMs,
|
||||
double AxisX, double AxisY, double AxisZ,
|
||||
double AngleDegrees = 0);
|
||||
|
||||
[System.Flags]
|
||||
public enum GfxPresentationReason
|
||||
{
|
||||
None = 0,
|
||||
RetainedMutation = 1,
|
||||
ContinuousChannel = 2,
|
||||
DiscreteSourceCell = 4,
|
||||
}
|
||||
|
||||
/// <summary>Sampled op-0x223 type-0 surface transition. Range A is already present in normal z-order;
|
||||
/// the compositor draws range B over it with <paramref name="Progress"/> to form the native crossfade.</summary>
|
||||
public readonly record struct SurfaceTransitionState(long CommandKey, int TargetSlot,
|
||||
@@ -62,7 +71,8 @@ public readonly record struct RenderObject(long Handle, long SurfaceResId, long
|
||||
bool MultiplyTint,
|
||||
SurfaceTransitionState? SurfaceTransition = null,
|
||||
ColorTransitionState? ColorTransition = null,
|
||||
Affine2D? RangeTransform = null);
|
||||
Affine2D? RangeTransform = null,
|
||||
bool TimeVarying = false);
|
||||
|
||||
/// <summary>Host-agnostic model of the AGE native gfx command-buffer (reversed in
|
||||
/// docs/engine-re.md, gfx op-contract table). One registry maps an object handle to a GfxObject — the
|
||||
@@ -142,6 +152,9 @@ public sealed class GfxState
|
||||
// Populated lazily by the geometry SET ops and draw-texture. Op 0x215 queries this same native map and
|
||||
// returns the object's live source slot (obj+4), or -1 when the handle has not been drawn/bound yet.
|
||||
private readonly Dictionary<long, GfxObject> _objects = new();
|
||||
// Native composition is handle-ascending z order. Mutations maintain this small index so snapshots do
|
||||
// not rebuild/sort a dictionary-sized LINQ buffer, while hot handle lookup remains O(1).
|
||||
private readonly List<long> _orderedObjectHandles = new();
|
||||
private readonly NumericGlyphStyle[] _numericGlyphStyles = new NumericGlyphStyle[11];
|
||||
|
||||
// Ops 0x229-0x22e address one embedded gfx-object record outside the ordinary object map. Its sampled
|
||||
@@ -164,6 +177,12 @@ public sealed class GfxState
|
||||
public long AnimationServiceFlags { get; private set; }
|
||||
public uint PreviousFrameTimeMilliseconds { get; private set; }
|
||||
public uint CurrentFrameTimeMilliseconds { get; private set; }
|
||||
private long _previousFrameTimeMs;
|
||||
private long _currentFrameTimeMs;
|
||||
private long _mutationGeneration;
|
||||
private long _publishedMutationGeneration;
|
||||
|
||||
private void MarkRetainedMutation() => _mutationGeneration++;
|
||||
|
||||
/// <summary>Live geometry objects and the surface slot they draw from — for the CLI gfx oracle.</summary>
|
||||
public IEnumerable<(long Handle, int Slot)> Objects
|
||||
@@ -178,8 +197,14 @@ public sealed class GfxState
|
||||
// (Monitor) so the callers that already hold it are fine.
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_objects.TryGetValue(handle, out var o)) { o = new GfxObject(); _objects[handle] = o; }
|
||||
if (!_objects.TryGetValue(handle, out var o))
|
||||
{
|
||||
o = new GfxObject();
|
||||
_objects[handle] = o;
|
||||
InsertOrderedHandle(handle);
|
||||
}
|
||||
CurrentObject = handle;
|
||||
MarkRetainedMutation();
|
||||
return o;
|
||||
}
|
||||
}
|
||||
@@ -189,6 +214,31 @@ public sealed class GfxState
|
||||
lock (_lock) DefaultObjectSlot = slot;
|
||||
}
|
||||
|
||||
public void SetObjectAnchor(long handle, (long X, long Y, long Z) anchor)
|
||||
{
|
||||
lock (_lock) GetOrCreate(handle).V18 = anchor;
|
||||
}
|
||||
|
||||
public void SetObjectPosition(long handle, (long X, long Y, long Z) position)
|
||||
{
|
||||
lock (_lock) GetOrCreate(handle).V24 = position;
|
||||
}
|
||||
|
||||
public void SetObjectField64(long handle, long value)
|
||||
{
|
||||
lock (_lock) GetOrCreate(handle).Field64 = value;
|
||||
}
|
||||
|
||||
public void SetObjectFields68And6c(long handle, long value68, long value6c)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var o = GetOrCreate(handle);
|
||||
o.Field68 = value68;
|
||||
o.Field6c = value6c;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Op 0x229: reset the embedded range transform, select [first, first+count), and set its
|
||||
/// anchor/pivot. This does not create or mutate an ordinary retained object.</summary>
|
||||
public void SetRangeTransform(long first, long count, (long X, long Y, long Z) anchor)
|
||||
@@ -198,6 +248,7 @@ public sealed class GfxState
|
||||
_rangeTransformFirst = first;
|
||||
_rangeTransformCount = System.Math.Max(0, count);
|
||||
_rangeTransform = new GfxObject { V18 = anchor };
|
||||
MarkRetainedMutation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,13 +256,20 @@ public sealed class GfxState
|
||||
public void SetRangeScaleCurrent((long X, long Y, long Z) percent)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_rangeTransform.ScaleCurrent = (percent.X / 100.0, percent.Y / 100.0, percent.Z / 100.0);
|
||||
MarkRetainedMutation();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Op 0x22c: immediately replace the embedded range transform's current translation.</summary>
|
||||
public void SetRangeTranslationCurrent((long X, long Y, long Z) translation)
|
||||
{
|
||||
lock (_lock) _rangeTransform.TranslationCurrent = translation;
|
||||
lock (_lock)
|
||||
{
|
||||
_rangeTransform.TranslationCurrent = translation;
|
||||
MarkRetainedMutation();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Op 0x22d: arm the range transform's delayed one-shot scale target.</summary>
|
||||
@@ -224,6 +282,7 @@ public sealed class GfxState
|
||||
_rangeTransform.ScaleTarget = (percent.X / 100.0, percent.Y / 100.0, percent.Z / 100.0);
|
||||
_rangeTransform.ScaleEnabled = durationMs > 0;
|
||||
_rangeTransform.OneShotStartMs = -1;
|
||||
MarkRetainedMutation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,6 +292,7 @@ public sealed class GfxState
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_objects.TryGetValue(sourceHandle, out var s)) return false;
|
||||
bool destinationIsNew = !_objects.ContainsKey(destinationHandle);
|
||||
_objects[destinationHandle] = new GfxObject
|
||||
{
|
||||
V18 = s.V18, V24 = s.V24, V16c = s.V16c,
|
||||
@@ -258,7 +318,9 @@ public sealed class GfxState
|
||||
RotationPeriodMs = s.RotationPeriodMs, RotationAxis = s.RotationAxis,
|
||||
RotationEnabled = s.RotationEnabled, RotationStartMs = s.RotationStartMs,
|
||||
};
|
||||
if (destinationIsNew) InsertOrderedHandle(destinationHandle);
|
||||
CurrentObject = destinationHandle;
|
||||
MarkRetainedMutation();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -280,7 +342,10 @@ public sealed class GfxState
|
||||
{
|
||||
lock (_lock)
|
||||
if (_objects.TryGetValue(handle, out var obj) && obj.SourceSlot == fromSlot)
|
||||
{
|
||||
obj.SourceSlot = toSlot;
|
||||
MarkRetainedMutation();
|
||||
}
|
||||
}
|
||||
public long QueryField(long idx) => _fieldTable.TryGetValue(idx, out var v) ? v : 0;
|
||||
|
||||
@@ -288,7 +353,12 @@ public sealed class GfxState
|
||||
{
|
||||
lock (_lock) // re-entrant: EraseRange already holds _lock
|
||||
{
|
||||
_objects.Remove(handle);
|
||||
if (_objects.Remove(handle))
|
||||
{
|
||||
int index = _orderedObjectHandles.BinarySearch(handle);
|
||||
if (index >= 0) _orderedObjectHandles.RemoveAt(index);
|
||||
MarkRetainedMutation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,7 +382,9 @@ public sealed class GfxState
|
||||
lock (_lock)
|
||||
{
|
||||
_objects.Clear();
|
||||
_orderedObjectHandles.Clear();
|
||||
CurrentObject = 0;
|
||||
MarkRetainedMutation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,6 +396,7 @@ public sealed class GfxState
|
||||
lock (_lock)
|
||||
{
|
||||
_objects.Clear();
|
||||
_orderedObjectHandles.Clear();
|
||||
_fieldTable.Clear();
|
||||
_surfaces.Clear();
|
||||
_createdSurfaces.Clear();
|
||||
@@ -339,6 +412,9 @@ public sealed class GfxState
|
||||
AnimationServiceFlags = 0;
|
||||
PreviousFrameTimeMilliseconds = 0;
|
||||
CurrentFrameTimeMilliseconds = 0;
|
||||
_previousFrameTimeMs = 0;
|
||||
_currentFrameTimeMs = 0;
|
||||
MarkRetainedMutation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,6 +436,7 @@ public sealed class GfxState
|
||||
_surfaces[slot] = (resId, colorKey);
|
||||
_createdSurfaces.Remove(slot);
|
||||
_movieStopTimesMs.Remove(slot);
|
||||
MarkRetainedMutation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,6 +476,7 @@ public sealed class GfxState
|
||||
}
|
||||
if (CurrentRenderTargetSlot >= firstSlot && CurrentRenderTargetSlot < end)
|
||||
CurrentRenderTargetSlot = -1;
|
||||
MarkRetainedMutation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,6 +576,7 @@ public sealed class GfxState
|
||||
_surfaces[slot] = (0, -1); // create-texture: real mutable pixels, no asset id or color key
|
||||
_createdSurfaces.Add(slot);
|
||||
_movieStopTimesMs.Remove(slot);
|
||||
MarkRetainedMutation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -509,6 +588,7 @@ public sealed class GfxState
|
||||
_createdSurfaces.Remove(slot);
|
||||
_movieStopTimesMs.Remove(slot);
|
||||
_surfaceTransitions.Remove(slot);
|
||||
MarkRetainedMutation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,6 +606,7 @@ public sealed class GfxState
|
||||
RangeBStart = rangeBStart, RangeBCount = System.Math.Max(0, rangeBCount),
|
||||
DelayMs = System.Math.Max(0, delayMs), DurationMs = System.Math.Max(0, durationMs),
|
||||
};
|
||||
MarkRetainedMutation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -684,6 +765,7 @@ public sealed class GfxState
|
||||
int completed = 0;
|
||||
foreach (var t in _surfaceTransitions.Values)
|
||||
if (!t.Forced && TransitionProgress(t, nowMs) < 1.0) { t.Forced = true; completed++; }
|
||||
if (completed > 0) MarkRetainedMutation();
|
||||
return completed;
|
||||
}
|
||||
}
|
||||
@@ -883,7 +965,12 @@ public sealed class GfxState
|
||||
/// <summary>Op 0x238: set its separate global animation-service duration and reset marker.</summary>
|
||||
public void SetAnimClock(long durationTicks)
|
||||
{
|
||||
lock (_lock) { AnimClockDurationTicks = durationTicks; AnimClockGeneration++; }
|
||||
lock (_lock)
|
||||
{
|
||||
AnimClockDurationTicks = durationTicks;
|
||||
AnimClockGeneration++;
|
||||
MarkRetainedMutation();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetAnimationServiceFlags(long flags)
|
||||
@@ -895,6 +982,8 @@ public sealed class GfxState
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_previousFrameTimeMs = _currentFrameTimeMs;
|
||||
_currentFrameTimeMs = nowMilliseconds;
|
||||
PreviousFrameTimeMilliseconds = CurrentFrameTimeMilliseconds;
|
||||
CurrentFrameTimeMilliseconds = unchecked((uint)nowMilliseconds);
|
||||
}
|
||||
@@ -910,9 +999,65 @@ public sealed class GfxState
|
||||
ForceCompleteOneShotChannels();
|
||||
AnimClockDurationTicks = 0;
|
||||
AnimClockGeneration++;
|
||||
MarkRetainedMutation();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Sample the shared native frame clock and report why the retained scene needs publishing.
|
||||
/// Continuous channels remain frame-driven; op-0x231 spritesheets become dirty only when the shared
|
||||
/// previous/current samples select different cells. Retained VM writes are published exactly once.</summary>
|
||||
public GfxPresentationReason ConsumePresentationReasons(long nowMs)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_previousFrameTimeMs = _currentFrameTimeMs;
|
||||
_currentFrameTimeMs = nowMs;
|
||||
PreviousFrameTimeMilliseconds = unchecked((uint)_previousFrameTimeMs);
|
||||
CurrentFrameTimeMilliseconds = unchecked((uint)_currentFrameTimeMs);
|
||||
|
||||
GfxPresentationReason reasons = GfxPresentationReason.None;
|
||||
if (_publishedMutationGeneration != _mutationGeneration)
|
||||
{
|
||||
_publishedMutationGeneration = _mutationGeneration;
|
||||
reasons |= GfxPresentationReason.RetainedMutation;
|
||||
}
|
||||
|
||||
if (_surfaceTransitions.Values.Any(t => TransitionProgress(t, nowMs) < 1.0) ||
|
||||
_rangeTransform.ScaleEnabled || _rangeTransform.RotationChannelEnabled ||
|
||||
_rangeTransform.TranslationEnabled ||
|
||||
_objects.Values.Any(o => o.Visible &&
|
||||
(o.OneShotColorEnabled || o.ScaleEnabled || o.RotationChannelEnabled ||
|
||||
o.TranslationEnabled ||
|
||||
(o.ColorAnim && o.ColorPeriod > 0) ||
|
||||
(o.RotationEnabled && o.RotationPeriodMs > 0))))
|
||||
reasons |= GfxPresentationReason.ContinuousChannel;
|
||||
|
||||
foreach (var o in _objects.Values)
|
||||
{
|
||||
if (!o.Visible || !o.SrcAnim || o.SrcPeriod <= 0 || o.SrcFrameCount < 1) continue;
|
||||
if (o.SrcStart < 0)
|
||||
{
|
||||
// Prototype clones made before first publication all enter here in the same shared sample,
|
||||
// reproducing FIELD's native phase lock.
|
||||
o.SrcStart = nowMs;
|
||||
continue;
|
||||
}
|
||||
if (SourceCellAt(o, _previousFrameTimeMs) != SourceCellAt(o, _currentFrameTimeMs))
|
||||
{
|
||||
reasons |= GfxPresentationReason.DiscreteSourceCell;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return reasons;
|
||||
}
|
||||
}
|
||||
|
||||
private static long SourceCellAt(GfxObject o, long nowMs)
|
||||
{
|
||||
long elapsed = System.Math.Max(0, nowMs - o.SrcStart);
|
||||
return elapsed / o.SrcPeriod % o.SrcFrameCount;
|
||||
}
|
||||
|
||||
/// <summary>Back-compat: snapshot with no animation clock (nowMs = 0) — deterministic, for headless
|
||||
/// callers and existing tests.</summary>
|
||||
public IReadOnlyList<RenderObject> SnapshotVisibleObjects() => SnapshotVisibleObjects(0);
|
||||
@@ -925,10 +1070,22 @@ public sealed class GfxState
|
||||
/// alpha/tint. Channel Start fields seed to nowMs on first sight.</summary>
|
||||
public IReadOnlyList<RenderObject> SnapshotVisibleObjects(long nowMs)
|
||||
{
|
||||
var list = new List<RenderObject>();
|
||||
SnapshotVisibleObjects(nowMs, list);
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>Fill a caller-owned snapshot buffer. The Godot compositor reuses one list so its backing
|
||||
/// array survives across frames; callers that need an independently retained snapshot should use the
|
||||
/// returning overload.</summary>
|
||||
public void SnapshotVisibleObjects(long nowMs, List<RenderObject> list)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(list);
|
||||
lock (_lock)
|
||||
{
|
||||
var list = new List<RenderObject>();
|
||||
list.Clear();
|
||||
Affine2D? rangeAffine = null;
|
||||
bool rangeTimeVarying = false;
|
||||
if (_rangeTransformCount > 0)
|
||||
{
|
||||
var r = _rangeTransform;
|
||||
@@ -944,15 +1101,16 @@ public sealed class GfxState
|
||||
ref r.TranslationEnabled, nowMs);
|
||||
if (!r.ScaleEnabled && !r.RotationChannelEnabled && !r.TranslationEnabled)
|
||||
r.OneShotStartMs = -1;
|
||||
rangeTimeVarying = r.ScaleEnabled || r.RotationChannelEnabled || r.TranslationEnabled;
|
||||
rangeAffine = Transform2DMath.Build(new TransformState(
|
||||
rangeScale.X, rangeScale.Y, rangeScale.Z,
|
||||
rangeTranslation.X, rangeTranslation.Y, rangeTranslation.Z,
|
||||
r.V18.X, r.V18.Y, r.V18.Z,
|
||||
rangeRotation.X, rangeRotation.Y, rangeRotation.Z, rangeRotation.Angle));
|
||||
}
|
||||
foreach (var kv in _objects.OrderBy(k => k.Key))
|
||||
foreach (long handle in _orderedObjectHandles)
|
||||
{
|
||||
var o = kv.Value;
|
||||
var o = _objects[handle];
|
||||
if (!o.Visible) continue;
|
||||
bool hadOneShot = o.OneShotColorEnabled || o.ScaleEnabled ||
|
||||
o.RotationChannelEnabled || o.TranslationEnabled;
|
||||
@@ -1064,9 +1222,17 @@ public sealed class GfxState
|
||||
SurfaceTransitionState? transition = _surfaceTransitions.TryGetValue(o.SourceSlot, out var st)
|
||||
? SampleTransition(st, nowMs) : null;
|
||||
Affine2D? objectRangeTransform = rangeAffine is { } ra &&
|
||||
kv.Key >= _rangeTransformFirst && kv.Key - _rangeTransformFirst < _rangeTransformCount
|
||||
handle >= _rangeTransformFirst && handle - _rangeTransformFirst < _rangeTransformCount
|
||||
? ra : null;
|
||||
list.Add(new RenderObject(kv.Key, resId, ck, srcX, srcY, w, h,
|
||||
bool timeVarying =
|
||||
o.OneShotColorEnabled || o.ScaleEnabled || o.RotationChannelEnabled ||
|
||||
o.TranslationEnabled ||
|
||||
(o.SrcAnim && o.SrcPeriod > 0) ||
|
||||
(o.ColorAnim && o.ColorPeriod > 0) ||
|
||||
(o.RotationEnabled && o.RotationPeriodMs > 0) ||
|
||||
transition is { Progress: < 1.0 } ||
|
||||
(objectRangeTransform != null && rangeTimeVarying);
|
||||
list.Add(new RenderObject(handle, resId, ck, srcX, srcY, w, h,
|
||||
(int)o.V24.X, (int)o.V24.Y,
|
||||
new TransformState(scale.X, scale.Y, scale.Z,
|
||||
translation.X, translation.Y, translation.Z,
|
||||
@@ -1076,12 +1242,17 @@ public sealed class GfxState
|
||||
o.RotationAxis.X, o.RotationAxis.Y,
|
||||
o.RotationAxis.Z, cycleAngle),
|
||||
alpha, tint, strength, blend, multiplyTint, transition,
|
||||
colorTransition, objectRangeTransform));
|
||||
colorTransition, objectRangeTransform, timeVarying));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
private void InsertOrderedHandle(long handle)
|
||||
{
|
||||
int index = _orderedObjectHandles.BinarySearch(handle);
|
||||
if (index < 0) _orderedObjectHandles.Insert(~index, handle);
|
||||
}
|
||||
|
||||
private static (long Packed, ColorTransitionState State) SampleOneShotColor(GfxObject o, long nowMs)
|
||||
{
|
||||
long current = o.Color & 0xffffffff;
|
||||
|
||||
@@ -13,14 +13,23 @@ public static class SoftwareAffineRasterizer
|
||||
int ia = (int)(System.Math.Clamp(opacity, 0f, 1f) * 255);
|
||||
if (ia == 0) return;
|
||||
int tr=(int)(tint>>16&255), tg=(int)(tint>>8&255), tb=(int)(tint&255);
|
||||
bool unmodulatedSourceOver = ia == 255 && istr == 0 && !multiplyTint && blend != BlendKind.Additive;
|
||||
if (TryIntegerTranslation(localToDest, out int tx, out int ty))
|
||||
{
|
||||
BlitTranslated(dst, dstW, dstH, src, srcW, srcX, srcY, width, height,
|
||||
tx, ty, tr, tg, tb, istr, ia, multiplyTint, blend);
|
||||
tx, ty, tr, tg, tb, istr, ia, multiplyTint, blend, unmodulatedSourceOver);
|
||||
return;
|
||||
}
|
||||
if (!localToDest.TryInverse(out var inv)) return;
|
||||
Bounds(localToDest, width, height, dstW, dstH, out int x0, out int y0, out int x1, out int y1);
|
||||
if (x1 <= x0 || y1 <= y0) return;
|
||||
if (localToDest.XY == 0 && localToDest.YX == 0 && x1 - x0 <= 4096)
|
||||
{
|
||||
BlitAxisAligned(dst, dstW, src, srcW, srcX, srcY, width, height, inv,
|
||||
x0, y0, x1, y1, tr, tg, tb, istr, ia, multiplyTint, blend,
|
||||
unmodulatedSourceOver);
|
||||
return;
|
||||
}
|
||||
for (int y=y0; y<y1; y++) for (int x=x0; x<x1; x++)
|
||||
{
|
||||
var p = inv.Apply(x + 0.5, y + 0.5);
|
||||
@@ -48,6 +57,12 @@ public static class SoftwareAffineRasterizer
|
||||
}
|
||||
if (!localToDest.TryInverse(out var inv)) return;
|
||||
Bounds(localToDest,width,height,dstW,dstH,out int x0,out int y0,out int x1,out int y1);
|
||||
if (x1 <= x0 || y1 <= y0) return;
|
||||
if (localToDest.XY == 0 && localToDest.YX == 0 && x1 - x0 <= 4096)
|
||||
{
|
||||
FillAxisAligned(dst, dstW, width, height, inv, x0, y0, x1, y1, r, g, b, a);
|
||||
return;
|
||||
}
|
||||
for(int y=y0;y<y1;y++)for(int x=x0;x<x1;x++){
|
||||
var p=inv.Apply(x+.5,y+.5);
|
||||
if(p.X>=0&&p.X<width&&p.Y>=0&&p.Y<height) Blend(dst,(y*dstW+x)*4,r,g,b,a);
|
||||
@@ -69,11 +84,16 @@ public static class SoftwareAffineRasterizer
|
||||
private static void BlitTranslated(byte[] dst, int dstW, int dstH, byte[] src, int srcW,
|
||||
int srcX, int srcY, int width, int height, int tx, int ty,
|
||||
int tr, int tg, int tb, int istr, int ia, bool multiplyTint,
|
||||
BlendKind blend)
|
||||
BlendKind blend, bool unmodulatedSourceOver)
|
||||
{
|
||||
int x0 = System.Math.Max(0, tx), y0 = System.Math.Max(0, ty);
|
||||
int x1 = (int)System.Math.Min(dstW, (long)tx + width);
|
||||
int y1 = (int)System.Math.Min(dstH, (long)ty + height);
|
||||
if (unmodulatedSourceOver)
|
||||
{
|
||||
BlitTranslatedUnmodulated(dst, dstW, src, srcW, srcX, srcY, tx, ty, x0, y0, x1, y1);
|
||||
return;
|
||||
}
|
||||
for (int y = y0; y < y1; y++)
|
||||
{
|
||||
int v = y - ty;
|
||||
@@ -91,6 +111,20 @@ public static class SoftwareAffineRasterizer
|
||||
}
|
||||
}
|
||||
|
||||
private static void BlitTranslatedUnmodulated(byte[] dst, int dstW, byte[] src, int srcW,
|
||||
int srcX, int srcY, int tx, int ty,
|
||||
int x0, int y0, int x1, int y1)
|
||||
{
|
||||
for (int y = y0; y < y1; y++)
|
||||
{
|
||||
int sourceRow = (srcY + y - ty) * srcW + srcX - tx;
|
||||
int destinationRow = y * dstW;
|
||||
for (int x = x0; x < x1; x++)
|
||||
BlendUnmodulatedSourceOver(dst, (destinationRow + x) * 4,
|
||||
src, (sourceRow + x) * 4);
|
||||
}
|
||||
}
|
||||
|
||||
private static void FillTranslated(byte[] dst, int dstW, int dstH, int width, int height,
|
||||
int tx, int ty, int r, int g, int b, int a)
|
||||
{
|
||||
@@ -102,6 +136,102 @@ public static class SoftwareAffineRasterizer
|
||||
Blend(dst, (y * dstW + x) * 4, r, g, b, a);
|
||||
}
|
||||
|
||||
// Axis-aligned scale is FIELD's dominant non-integer path. Its inverse source column is independent
|
||||
// of destination Y, and its source row is independent of destination X. Cache the former once per
|
||||
// layer and compute the latter once per scanline while preserving the general path's exact center-
|
||||
// sample/floor arithmetic. Pooled lookup storage avoids both per-layer garbage and repeated-stackalloc
|
||||
// growth when the JIT inlines this hot path; the 4096-column caller gate keeps the rented bucket small.
|
||||
private static void BlitAxisAligned(byte[] dst, int dstW, byte[] src, int srcW,
|
||||
int srcX, int srcY, int width, int height, Affine2D inv,
|
||||
int x0, int y0, int x1, int y1,
|
||||
int tr, int tg, int tb, int istr, int ia,
|
||||
bool multiplyTint, BlendKind blend, bool unmodulatedSourceOver)
|
||||
{
|
||||
int count = x1 - x0;
|
||||
int[] sourceColumns = System.Buffers.ArrayPool<int>.Shared.Rent(count);
|
||||
try
|
||||
{
|
||||
for (int x = x0; x < x1; x++)
|
||||
{
|
||||
int u = (int)System.Math.Floor(inv.Apply(x + 0.5, y0 + 0.5).X);
|
||||
sourceColumns[x - x0] = (uint)u < (uint)width ? u : -1;
|
||||
}
|
||||
for (int y = y0; y < y1; y++)
|
||||
{
|
||||
int v = (int)System.Math.Floor(inv.Apply(x0 + 0.5, y + 0.5).Y);
|
||||
if ((uint)v >= (uint)height) continue;
|
||||
int sourceRow = (srcY + v) * srcW;
|
||||
int destinationRow = y * dstW;
|
||||
for (int x = x0; x < x1; x++)
|
||||
{
|
||||
int u = sourceColumns[x - x0];
|
||||
if (u < 0) continue;
|
||||
int si = (sourceRow + srcX + u) * 4;
|
||||
int di = (destinationRow + x) * 4;
|
||||
if (unmodulatedSourceOver)
|
||||
{
|
||||
BlendUnmodulatedSourceOver(dst, di, src, si);
|
||||
continue;
|
||||
}
|
||||
int sa = src[si + 3] * ia / 255;
|
||||
if (sa == 0) continue;
|
||||
int sr = multiplyTint ? src[si] * tr / 255 : (src[si] * (255 - istr) + tr * istr) / 255;
|
||||
int sg = multiplyTint ? src[si + 1] * tg / 255 : (src[si + 1] * (255 - istr) + tg * istr) / 255;
|
||||
int sb = multiplyTint ? src[si + 2] * tb / 255 : (src[si + 2] * (255 - istr) + tb * istr) / 255;
|
||||
Blend(dst, di, sr, sg, sb, sa, blend);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally { System.Buffers.ArrayPool<int>.Shared.Return(sourceColumns); }
|
||||
}
|
||||
|
||||
// Most FIELD layers have full object opacity and no tint. Preserve color-key transparency and partially
|
||||
// transparent edge texels, but make the overwhelmingly common alpha-255 case a four-byte copy instead of
|
||||
// performing tint and source-over multiply/divide work whose result is exactly the source texel.
|
||||
private static void BlendUnmodulatedSourceOver(byte[] dst, int di, byte[] src, int si)
|
||||
{
|
||||
int a = src[si + 3];
|
||||
if (a == 0) return;
|
||||
if (a == 255)
|
||||
{
|
||||
dst[di] = src[si];
|
||||
dst[di + 1] = src[si + 1];
|
||||
dst[di + 2] = src[si + 2];
|
||||
dst[di + 3] = 255;
|
||||
return;
|
||||
}
|
||||
int inverse = 255 - a;
|
||||
dst[di] = (byte)((src[si] * a + dst[di] * inverse) / 255);
|
||||
dst[di + 1] = (byte)((src[si + 1] * a + dst[di + 1] * inverse) / 255);
|
||||
dst[di + 2] = (byte)((src[si + 2] * a + dst[di + 2] * inverse) / 255);
|
||||
dst[di + 3] = (byte)System.Math.Min(255, dst[di + 3] + a);
|
||||
}
|
||||
|
||||
private static void FillAxisAligned(byte[] dst, int dstW, int width, int height, Affine2D inv,
|
||||
int x0, int y0, int x1, int y1,
|
||||
int r, int g, int b, int a)
|
||||
{
|
||||
int count = x1 - x0;
|
||||
bool[] includedColumns = System.Buffers.ArrayPool<bool>.Shared.Rent(count);
|
||||
try
|
||||
{
|
||||
for (int x = x0; x < x1; x++)
|
||||
{
|
||||
double u = inv.Apply(x + 0.5, y0 + 0.5).X;
|
||||
includedColumns[x - x0] = u >= 0 && u < width;
|
||||
}
|
||||
for (int y = y0; y < y1; y++)
|
||||
{
|
||||
double v = inv.Apply(x0 + 0.5, y + 0.5).Y;
|
||||
if (v < 0 || v >= height) continue;
|
||||
int destinationRow = y * dstW;
|
||||
for (int x = x0; x < x1; x++)
|
||||
if (includedColumns[x - x0]) Blend(dst, (destinationRow + x) * 4, r, g, b, a);
|
||||
}
|
||||
}
|
||||
finally { System.Buffers.ArrayPool<bool>.Shared.Return(includedColumns); }
|
||||
}
|
||||
|
||||
private static void Bounds(Affine2D m,int w,int h,int dw,int dh,out int x0,out int y0,out int x1,out int y1)
|
||||
{
|
||||
var a=m.Apply(0,0);var b=m.Apply(w,0);var c=m.Apply(0,h);var d=m.Apply(w,h);
|
||||
|
||||
@@ -40,46 +40,74 @@ public static class Transform2DMath
|
||||
{
|
||||
public static Affine2D Build(TransformState t, RotationCycleState cycle = default)
|
||||
{
|
||||
double[] m = Identity();
|
||||
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 (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[0], m[1], m[4], m[5], m[12], m[13]);
|
||||
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);
|
||||
|
||||
private static double[] Identity() => new double[] { 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1 };
|
||||
private static double[] Scale(double x, double y, double z)
|
||||
=> new double[] { x,0,0,0, 0,y,0,0, 0,0,z,0, 0,0,0,1 };
|
||||
private static double[] Translation(double x, double y, double z)
|
||||
=> new double[] { 1,0,0,0, 0,1,0,0, 0,0,1,0, x,y,z,1 };
|
||||
// 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
|
||||
// about eleven arrays per rendered object, or roughly 2.2 MB on every DEBUGMAP composition.
|
||||
private readonly record struct Matrix3D(
|
||||
double M11, double M12, double M13,
|
||||
double M21, double M22, double M23,
|
||||
double M31, double M32, double M33,
|
||||
double TX, double TY, double TZ);
|
||||
|
||||
private static double[] AxisAngle(double x, double y, double z, double degrees)
|
||||
private static Matrix3D Identity() => new(
|
||||
1,0,0, 0,1,0, 0,0,1, 0,0,0);
|
||||
|
||||
private static Matrix3D Scale(double x, double y, double z) => new(
|
||||
x,0,0, 0,y,0, 0,0,z, 0,0,0);
|
||||
|
||||
private static Matrix3D Translation(double x, double y, double z) => new(
|
||||
1,0,0, 0,1,0, 0,0,1, x,y,z);
|
||||
|
||||
private static Matrix3D AxisAngle(double x, double y, double z, double degrees)
|
||||
{
|
||||
double len = System.Math.Sqrt(x*x + y*y + z*z);
|
||||
if (len < 1e-12 || System.Math.Abs(degrees) < 1e-12) return Identity();
|
||||
x /= len; y /= len; z /= len;
|
||||
double r = degrees * System.Math.PI / 180.0, c = System.Math.Cos(r), s = System.Math.Sin(r), q = 1-c;
|
||||
return new double[] {
|
||||
x*x*q+c, x*y*q+z*s, x*z*q-y*s, 0,
|
||||
x*y*q-z*s, y*y*q+c, y*z*q+x*s, 0,
|
||||
x*z*q+y*s, y*z*q-x*s, z*z*q+c, 0,
|
||||
0,0,0,1
|
||||
};
|
||||
return new(
|
||||
x*x*q+c, x*y*q+z*s, x*z*q-y*s,
|
||||
x*y*q-z*s, y*y*q+c, y*z*q+x*s,
|
||||
x*z*q+y*s, y*z*q-x*s, z*z*q+c,
|
||||
0,0,0);
|
||||
}
|
||||
|
||||
private static double[] Mul(double[] a, double[] b)
|
||||
private static Matrix3D Mul(Matrix3D a, Matrix3D b) => new(
|
||||
Sum4(a.M11*b.M11, a.M12*b.M21, a.M13*b.M31, 0),
|
||||
Sum4(a.M11*b.M12, a.M12*b.M22, a.M13*b.M32, 0),
|
||||
Sum4(a.M11*b.M13, a.M12*b.M23, a.M13*b.M33, 0),
|
||||
Sum4(a.M21*b.M11, a.M22*b.M21, a.M23*b.M31, 0),
|
||||
Sum4(a.M21*b.M12, a.M22*b.M22, a.M23*b.M32, 0),
|
||||
Sum4(a.M21*b.M13, a.M22*b.M23, a.M23*b.M33, 0),
|
||||
Sum4(a.M31*b.M11, a.M32*b.M21, a.M33*b.M31, 0),
|
||||
Sum4(a.M31*b.M12, a.M32*b.M22, a.M33*b.M32, 0),
|
||||
Sum4(a.M31*b.M13, a.M32*b.M23, a.M33*b.M33, 0),
|
||||
Sum4(a.TX*b.M11, a.TY*b.M21, a.TZ*b.M31, b.TX),
|
||||
Sum4(a.TX*b.M12, a.TY*b.M22, a.TZ*b.M32, b.TY),
|
||||
Sum4(a.TX*b.M13, a.TY*b.M23, a.TZ*b.M33, b.TZ));
|
||||
|
||||
// Accumulate in the same order as the former 4x4 loop so boundary-sensitive nearest-neighbour
|
||||
// projection retains its floating-point behavior while avoiding an intermediate array.
|
||||
private static double Sum4(double a, double b, double c, double d)
|
||||
{
|
||||
var o = new double[16];
|
||||
for (int row=0; row<4; row++)
|
||||
for (int col=0; col<4; col++)
|
||||
for (int k=0; k<4; k++) o[row*4+col] += a[row*4+k] * b[k*4+col];
|
||||
return o;
|
||||
double result = 0;
|
||||
result += a;
|
||||
result += b;
|
||||
result += c;
|
||||
result += d;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1712,16 +1712,16 @@ public sealed class VirtualMachine
|
||||
Write(a[1], v.X); Write(a[2], v.Y); Write(a[3], v.Z); return pc + 1;
|
||||
}
|
||||
case "set-gfx-geom3": // 0x217 (handle)(a)(b)(c) -> V18
|
||||
Gfx.GetOrCreate(Read(a[0])).V18 = (Read(a[1]), Read(a[2]), Read(a[3])); return pc + 1;
|
||||
Gfx.SetObjectAnchor(Read(a[0]), (Read(a[1]), Read(a[2]), Read(a[3]))); return pc + 1;
|
||||
case "set-gfx-geom3-b": // 0x219 (handle)(a)(b)(c) -> V24
|
||||
Gfx.GetOrCreate(Read(a[0])).V24 = (Read(a[1]), Read(a[2]), Read(a[3])); return pc + 1;
|
||||
Gfx.SetObjectPosition(Read(a[0]), (Read(a[1]), Read(a[2]), Read(a[3]))); return pc + 1;
|
||||
case "u0041AF00": // 0x80: default object slot substituted by native op 0x1d9
|
||||
case "set-default-gfx-object-slot":
|
||||
Gfx.SetDefaultObjectSlot((int)Read(a[0])); return pc + 1;
|
||||
|
||||
// ---- SC0000 anim/transform/spritesheet cluster (docs/engine-re.md §"SC0000 anim ... cluster") ----
|
||||
case "u00421DD0": // 0x22f set-position: (handle)(op2)(x)(y)(z) -> base position (direct set)
|
||||
Gfx.GetOrCreate(Read(a[0])).V24 = (Read(a[2]), Read(a[3]), Read(a[4])); return pc + 1;
|
||||
Gfx.SetObjectPosition(Read(a[0]), (Read(a[2]), Read(a[3]), Read(a[4]))); return pc + 1;
|
||||
case "u004219E0": // pre-reference compatibility
|
||||
case "set-gfx-range-transform": // 0x229 (first)(count)(anchor x/y/z)
|
||||
Gfx.SetRangeTransform(Read(a[0]), Read(a[1]), (Read(a[2]), Read(a[3]), Read(a[4])));
|
||||
@@ -1782,10 +1782,10 @@ public sealed class VirtualMachine
|
||||
case "gfx-set-scale-current": // 0x1fd (handle)(sx%)(sy%)(sz%) -> current scale matrix
|
||||
Gfx.SetCurrentScale(Read(a[0]), (Read(a[1]), Read(a[2]), Read(a[3]))); return pc + 1;
|
||||
case "set-gfx-field64": // 0x212 (idx)(val)
|
||||
Gfx.GetOrCreate(Read(a[0])).Field64 = Read(a[1]); return pc + 1;
|
||||
Gfx.SetObjectField64(Read(a[0]), Read(a[1])); return pc + 1;
|
||||
case "set-gfx-xy": // 0x213 (idx)(x)(y)
|
||||
{
|
||||
var o = Gfx.GetOrCreate(Read(a[0])); o.Field68 = Read(a[1]); o.Field6c = Read(a[2]); return pc + 1;
|
||||
Gfx.SetObjectFields68And6c(Read(a[0]), Read(a[1]), Read(a[2])); return pc + 1;
|
||||
}
|
||||
case "gfx-elem-erase": // 0x1f7 (handle)(count) — erase retained-object range
|
||||
Gfx.EraseRange(Read(a[0]), Read(a[1])); return pc + 1;
|
||||
|
||||
Reference in New Issue
Block a user