1612 lines
76 KiB
C#
1612 lines
76 KiB
C#
using System.Linq;
|
|
using Age.Engine.Hosting;
|
|
|
|
namespace Age.Engine.Model;
|
|
|
|
/// <summary>The sampled native one-shot channels carried to the compositor: op 0x21e scale, op 0x21f
|
|
/// axis-angle rotation, and op 0x220 translation. Z is retained through full 4x4 composition.</summary>
|
|
public readonly record struct TransformState(double ScaleX, double ScaleY, double ScaleZ,
|
|
double TranslateX, double TranslateY, double TranslateZ,
|
|
double AnchorX, double AnchorY, double AnchorZ,
|
|
double RotationAxisX = 0, double RotationAxisY = 0,
|
|
double RotationAxisZ = 0, double RotationAngleDegrees = 0);
|
|
|
|
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
|
|
{
|
|
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,
|
|
long RangeAStart, int RangeACount, long RangeBStart, int RangeBCount,
|
|
long DelayMs, long DurationMs, long StartMs, double Progress, bool Forced);
|
|
|
|
public readonly record struct MovieMaskTransitionState(
|
|
MovieMaskTransitionRequest Request, bool Completed);
|
|
|
|
/// <summary>One synchronized sample of op 0x202's native one-shot packed-color channel.</summary>
|
|
public readonly record struct ColorTransitionState(long Current, long Target,
|
|
long DelayMs, long DurationMs, long StartMs, double Progress, bool Active);
|
|
|
|
/// <summary>One EngineCtx numeric-glyph style registered by opcode 0x13a.</summary>
|
|
public readonly record struct NumericGlyphStyle(int SurfaceSlot, int AtlasX, int AtlasY,
|
|
int DigitWidth, int DigitHeight)
|
|
{
|
|
public bool Registered => SurfaceSlot != 0;
|
|
}
|
|
|
|
public sealed record BlockingGfxObjectDiagnostic(
|
|
long Handle, int SourceSlot, long StartMs, long ControlFlags,
|
|
bool ColorEnabled, long ColorDelayMs, long ColorDurationMs,
|
|
bool ScaleEnabled, long ScaleDelayMs, long ScaleDurationMs,
|
|
bool RotationEnabled, long RotationDelayMs, long RotationDurationMs,
|
|
bool TranslationEnabled, long TranslationDelayMs, long TranslationDurationMs);
|
|
|
|
public sealed record GfxDiagnosticSnapshot(
|
|
long NowMs, bool HasActiveTimedPresentation, int ObjectCount, int VisibleObjectCount,
|
|
int ActiveSurfaceTransitionCount, long AnimationServiceFlags,
|
|
long AnimClockDurationTicks, long AnimClockGeneration,
|
|
uint PreviousFrameTimeMilliseconds, uint CurrentFrameTimeMilliseconds,
|
|
long RangeTransformFirst, long RangeTransformCount,
|
|
BlockingGfxObjectDiagnostic? BlockingRangeTransform,
|
|
IReadOnlyList<BlockingGfxObjectDiagnostic> BlockingObjects);
|
|
|
|
public readonly record struct GfxSurfacePersistenceState(
|
|
int Slot, long ResourceId, long ColorKey, bool Created, bool ReloadOnRestore);
|
|
|
|
public sealed record GfxPersistenceSnapshot(
|
|
IReadOnlyList<GfxSurfacePersistenceState> Surfaces,
|
|
IReadOnlyList<(long Handle, GfxState.GfxObject Object)> Objects,
|
|
long RangeFirst,
|
|
long RangeCount,
|
|
GfxState.GfxObject RangeTransform);
|
|
|
|
/// <summary>A renderable view of one visible gfx object — the host composites these in ascending-handle order
|
|
/// (= the engine's z-order) each frame. Built by <see cref="GfxState.SnapshotVisibleObjects"/>; the surface
|
|
/// resId/colorkey are resolved from the object's live source slot at snapshot time (see docs/engine-re.md,
|
|
/// "The full gfx render model").</summary>
|
|
/// <summary>The packed-color channel is mode-dependent. Textured mode 0 ignores packed alpha and
|
|
/// multiplicatively modulates by <paramref name="Tint"/>; surfaceless mode 0 uses <paramref name="TintStrength"/>.
|
|
/// A mode-0 color which has passed through op 0x202, and mode 1, use <paramref name="Alpha"/> as opacity.
|
|
/// <paramref name="MultiplyTint"/> selects the latter compositor path.</summary>
|
|
public readonly record struct RenderObject(long Handle, long SurfaceResId, long ColorKey,
|
|
int SrcX, int SrcY, int W, int H, int DstX, int DstY,
|
|
TransformState Transform, RotationCycleState Rotation,
|
|
int Alpha, long Tint, int TintStrength, BlendKind Blend,
|
|
bool MultiplyTint,
|
|
SurfaceTransitionState? SurfaceTransition = null,
|
|
ColorTransitionState? ColorTransition = null,
|
|
Affine2D? RangeTransform = null,
|
|
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)
|
|
{
|
|
public static GfxHandleRange All => new(0, long.MaxValue);
|
|
|
|
public bool Contains(long handle)
|
|
=> Count > 0 && handle >= First && (ulong)(handle - First) < (ulong)Count;
|
|
}
|
|
|
|
/// <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
|
|
/// native retained-gfx owner+0x408 map (EngineCtx+0x46a1c) that op 0x215 queries and the geometry get/set ops share. Each object carries a
|
|
/// slot (returned by 0x215) and three 3-vectors: V18 (set 0x217 / get 0x218, anchor), V24 (set 0x219 /
|
|
/// get 0x21a, position), V16c (set 0x1ff). The native DirectDraw workers are NOT modelled — only the data
|
|
/// the query ops read back, which is all the bytecode geometry math needs.</summary>
|
|
public sealed class GfxState
|
|
{
|
|
private sealed class SurfaceTransition
|
|
{
|
|
public long CommandKey, RangeAStart, RangeBStart, DelayMs, DurationMs;
|
|
public int TargetSlot, RangeACount, RangeBCount;
|
|
public long StartMs = -1;
|
|
public bool Forced;
|
|
}
|
|
private sealed class MovieMaskTransition
|
|
{
|
|
public required MovieMaskTransitionRequest Request;
|
|
public bool Completed;
|
|
}
|
|
public sealed class GfxObject
|
|
{
|
|
// The native numbered-save record contains several full transform matrices and reserved fields
|
|
// beyond the port's semantic model. Preserve the decoded bytes so a native load/save round trip
|
|
// does not erase state that the port has not modeled yet; known fields are patched by the codec.
|
|
public byte[]? NativePersistenceRecord;
|
|
public (long X, long Y, long Z) V18, V24, V16c;
|
|
public long Color = 0xffffffff; // native gfx_object_init_default obj+0x60: identity packed ARGB
|
|
public bool HasColor; // true once op 0x202/0x203 set a color/alpha modulation on this object
|
|
public long StaticColorMode; // op 0x203 operand 2 -> obj+0x30; mode 2 is transition alpha/identity
|
|
// Op 0x202 one-shot packed-color channel: current +0x60, target +0x64, delay +0x38,
|
|
// duration +0x4c. It shares obj+0x34's start timestamp with the one-shot matrix channels.
|
|
public long OneShotColorTarget = -1, ColorDelayMs, ColorDurationMs;
|
|
public bool OneShotColorEnabled;
|
|
// Once op 0x202 arms this object's color channel, its sampled/current ARGB is consumed as D3D
|
|
// opacity + multiplicative modulation even after the target commits. This distinguishes ADV chrome
|
|
// fades from a static mode-0 0x203 such as a CG initialized with 0x00ffffff (opaque identity).
|
|
public bool OneShotColorBlend;
|
|
// A mode-0 static color written after draw-texture binds an object consumes packed alpha as
|
|
// opacity. FIELD uses this to suppress a bound idle unit, and CALLBACK_SETTING uses it when
|
|
// rebuilding the ADV backing after CONFIG. A later draw-texture rebind clears the latch.
|
|
public bool PostBindStaticColorBlend;
|
|
|
|
// ---- src-rect / spritesheet-cell channel (ops 0x239 one-shot cell, 0x231 looping animation).
|
|
// Native obj+0x238 is the TOTAL FRAME COUNT and +0x23c is the COLUMN COUNT. Each frame keeps
|
|
// draw-texture's original source-rect size; those operands are not a grid to divide it by. ----
|
|
public long SrcFrameCount = 1, SrcColumns = 1, SrcCell, SrcPeriod, SrcStart = -1;
|
|
public bool SrcAnim;
|
|
// ---- animated color/glow channel (op 0x232). Interpolator COLOR channel: period obj+0x220,
|
|
// start obj+0x20c, target obj+0x240 — PING-PONG (distinct from static 0x202/0x203). ----
|
|
public long ColorPeriod, ColorStart = -1, ColorTarget;
|
|
public bool ColorAnim;
|
|
// draw-texture bind (gfx_object_bind_draw): the surface to draw + its source rect + the visible flag.
|
|
// Native gfx_object_init_default zeroes obj+4. Querying an object created by a geometry/animation
|
|
// setter therefore returns slot 0 even before draw-texture binds it; only an absent object returns -1.
|
|
public int SourceSlot;
|
|
public (int X, int Y, int W, int H) SrcRect;
|
|
public bool Visible;
|
|
|
|
// ---- independent one-shot matrix channels (gfx_object_apply_transform_channels@0x472f00). ----
|
|
// Scale: current obj+0x6c, target obj+0xac, delay obj+0x3c, duration obj+0x50.
|
|
public (double X, double Y, double Z) ScaleCurrent = (1, 1, 1), ScaleTarget = (1, 1, 1);
|
|
public long ScaleDelayMs, ScaleDurationMs;
|
|
public bool ScaleEnabled;
|
|
// Translation: current obj+0x16c, target obj+0x1ac, delay obj+0x44, duration obj+0x58.
|
|
public (double X, double Y, double Z) TranslationCurrent, TranslationTarget;
|
|
public long TranslationDelayMs, TranslationDurationMs;
|
|
public bool TranslationEnabled;
|
|
public (double X, double Y, double Z, double Angle) RotationCurrent;
|
|
public (double X, double Y, double Z, double Angle) RotationTarget;
|
|
public long RotationDelayMs, RotationDurationMs;
|
|
public bool RotationChannelEnabled;
|
|
// Op 0x242 writes obj+0x2d0. Bit 0 detaches the finite one-shot group from the blocking-dirty
|
|
// service and protects it from op 0x243's global force-completion request until natural completion.
|
|
public long OneShotAnimationControlFlags;
|
|
// Shared matrix-channel start timestamp obj+0x34, seeded from retained-gfx owner+0xb550
|
|
// (EngineCtx+0x51b64).
|
|
public long OneShotStartMs = -1;
|
|
|
|
// Op 0x234 is a separate cyclic rotation channel (period obj+0x228, axis obj+0x244..0x24c).
|
|
public long RotationPeriodMs;
|
|
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) ----
|
|
// 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
|
|
// matrix is post-multiplied onto only the selected handle range during native composition. FIELD uses
|
|
// this as its map camera while the surrounding dungeon UI remains screen-fixed.
|
|
private long _rangeTransformFirst, _rangeTransformCount;
|
|
private GfxObject _rangeTransform = new();
|
|
|
|
private readonly Dictionary<long, long> _fieldTable = new(); // ctx+0x46d14 (0x216); no family writer -> default 0
|
|
public long CurrentObject { get; private set; }
|
|
/// <summary>EngineCtx+0x14e08, selected by op 0x80 and used by op 0x1d9 when its slot is zero.</summary>
|
|
public int DefaultObjectSlot { get; private set; }
|
|
/// <summary>The D3D render target selected by op 0x20d. -1 denotes the main backbuffer.</summary>
|
|
public int CurrentRenderTargetSlot { get; private set; } = -1;
|
|
/// <summary>Legacy mode-1 surface tile edge selected by op 0x248. The portable backend retains
|
|
/// the native process-global value for parity but stores every surface as one contiguous image.</summary>
|
|
public int TiledSurfaceEdgeLength { get; private set; }
|
|
|
|
// ---- Separate global animation service clock (op 0x238; ctx+0x51b7c total / +0x51b78 elapsed).
|
|
// Retained for its opcode family; 0x21e scale and 0x220 translation use frame-time directly instead. ----
|
|
public long AnimClockDurationTicks { get; private set; }
|
|
public long AnimClockGeneration { get; private set; }
|
|
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
|
|
{
|
|
get { foreach (var kv in _objects) yield return (kv.Key, kv.Value.SourceSlot); }
|
|
}
|
|
|
|
public GfxObject GetOrCreate(long handle)
|
|
{
|
|
// Locked: called from the VM thread (directly by geometry/animation ops and inside BindDraw/anim
|
|
// ops) while the main-thread compositor enumerates _objects in SnapshotVisibleObjects. _lock is re-entrant
|
|
// (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;
|
|
InsertOrderedHandle(handle);
|
|
}
|
|
CurrentObject = handle;
|
|
MarkRetainedMutation();
|
|
return o;
|
|
}
|
|
}
|
|
|
|
public void SetDefaultObjectSlot(int slot)
|
|
{
|
|
lock (_lock) DefaultObjectSlot = slot;
|
|
}
|
|
|
|
public void SetTiledSurfaceEdgeLength(long edgeLength)
|
|
{
|
|
// Native stores the complete operand in one signed dword. It does not invalidate or rebuild
|
|
// already-created mode-1 surfaces, nor does it mark the retained compositor dirty.
|
|
lock (_lock) TiledSurfaceEdgeLength = unchecked((int)edgeLength);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/// <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)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
_rangeTransformFirst = first;
|
|
_rangeTransformCount = System.Math.Max(0, count);
|
|
_rangeTransform = new GfxObject { V18 = anchor };
|
|
MarkRetainedMutation();
|
|
}
|
|
}
|
|
|
|
/// <summary>Op 0x22a: immediately replace the embedded range transform's current scale.</summary>
|
|
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;
|
|
MarkRetainedMutation();
|
|
}
|
|
}
|
|
|
|
/// <summary>Op 0x22d: arm the range transform's delayed one-shot scale target.</summary>
|
|
public void SetRangeScaleChannel(long delayMs, long durationMs, (long X, long Y, long Z) percent)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
_rangeTransform.ScaleDelayMs = delayMs;
|
|
_rangeTransform.ScaleDurationMs = durationMs;
|
|
_rangeTransform.ScaleTarget = (percent.X / 100.0, percent.Y / 100.0, percent.Z / 100.0);
|
|
_rangeTransform.ScaleEnabled = durationMs > 0;
|
|
_rangeTransform.OneShotStartMs = -1;
|
|
MarkRetainedMutation();
|
|
}
|
|
}
|
|
|
|
/// <summary>Op 0x21d: clone the native 0x2d4-byte retained-object record from source to destination.</summary>
|
|
public bool CloneObject(long sourceHandle, long destinationHandle)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
if (!_objects.TryGetValue(sourceHandle, out var s)) return false;
|
|
bool destinationIsNew = !_objects.ContainsKey(destinationHandle);
|
|
_objects[destinationHandle] = CloneState(s);
|
|
if (destinationIsNew) InsertOrderedHandle(destinationHandle);
|
|
CurrentObject = destinationHandle;
|
|
MarkRetainedMutation();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public GfxObject? TryGet(long handle) => _objects.TryGetValue(handle, out var o) ? o : null;
|
|
|
|
/// <summary>Op 0x215: look up <paramref name="handle"/> in the retained gfx-object map and return obj+4,
|
|
/// the live source-surface slot written by draw-texture, or -1 when absent/unbound.</summary>
|
|
public int QuerySlot(long handle)
|
|
{
|
|
lock (_lock)
|
|
return _objects.TryGetValue(handle, out var o) ? o.SourceSlot : -1;
|
|
}
|
|
|
|
/// <summary>Rebind one retained object from one source surface to another. Used by SC0000's bounded
|
|
/// movie site to reproduce the native warm-engine slot assignment before the following static loaders
|
|
/// reuse the port's cold-bootstrap slot.</summary>
|
|
public void RemapObjectSurface(long handle, int fromSlot, int toSlot)
|
|
{
|
|
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;
|
|
|
|
public void Release(long handle)
|
|
{
|
|
lock (_lock) // re-entrant: EraseRange already holds _lock
|
|
{
|
|
if (_objects.Remove(handle))
|
|
{
|
|
int index = _orderedObjectHandles.BinarySearch(handle);
|
|
if (index >= 0) _orderedObjectHandles.RemoveAt(index);
|
|
MarkRetainedMutation();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Op 0x1f7 semantics (native gfx_object_erase_range @0x47d8b0): erase handles in
|
|
/// [handle, handle+count) when count>1, else just <paramref name="handle"/>. It is a teardown/erase,
|
|
/// NOT a create — objects are created lazily by the geometry SET ops (gfx_object_get_or_create).</summary>
|
|
public void EraseRange(long handle, long count)
|
|
{
|
|
// Retained-object cleanup (native gfx_object_erase): removes the object from the map, so it stops
|
|
// compositing next frame. Faithful to the engine (the render loop iterates the retained-object map).
|
|
lock (_lock)
|
|
{
|
|
if (count > 1) for (long i = handle; i < handle + count; i++) Release(i);
|
|
else Release(handle);
|
|
}
|
|
}
|
|
|
|
/// <summary>Op 0x1f6: clear every retained gfx-object record without releasing surface resources.</summary>
|
|
public void ClearRetainedObjects()
|
|
{
|
|
lock (_lock)
|
|
{
|
|
_objects.Clear();
|
|
_orderedObjectHandles.Clear();
|
|
CurrentObject = 0;
|
|
MarkRetainedMutation();
|
|
}
|
|
}
|
|
|
|
/// <summary>Native scene_context_init_reset ownership boundary used by opcode 0x9: discard
|
|
/// retained objects, command/query state, surfaces, transitions, render-target selection, and the
|
|
/// scene animation clock while leaving VM globals and decoded host assets outside this model.</summary>
|
|
public void ResetSceneContext()
|
|
{
|
|
lock (_lock)
|
|
{
|
|
_objects.Clear();
|
|
_orderedObjectHandles.Clear();
|
|
_fieldTable.Clear();
|
|
_surfaces.Clear();
|
|
_createdSurfaces.Clear();
|
|
_reloadableSurfaces.Clear();
|
|
_movieStopTimesMs.Clear();
|
|
_surfaceTransitions.Clear();
|
|
_movieMaskTransitions.Clear();
|
|
CurrentObject = 0;
|
|
CurrentRenderTargetSlot = -1;
|
|
_rangeTransformFirst = 0;
|
|
_rangeTransformCount = 0;
|
|
_rangeTransform = new GfxObject();
|
|
AnimClockDurationTicks = 0;
|
|
AnimClockGeneration++;
|
|
AnimationServiceFlags = 0;
|
|
PreviousFrameTimeMilliseconds = 0;
|
|
CurrentFrameTimeMilliseconds = 0;
|
|
_previousFrameTimeMs = 0;
|
|
_currentFrameTimeMs = 0;
|
|
MarkRetainedMutation();
|
|
}
|
|
}
|
|
|
|
public GfxPersistenceSnapshot CapturePersistenceSnapshot()
|
|
{
|
|
lock (_lock)
|
|
{
|
|
var surfaces = _surfaces.Keys
|
|
.Concat(_reloadableSurfaces)
|
|
.Distinct()
|
|
.Select(slot =>
|
|
{
|
|
bool active = _surfaces.TryGetValue(slot, out var value);
|
|
return new GfxSurfacePersistenceState(
|
|
slot, active ? value.ResId : -1, active ? value.ColorKey : 0,
|
|
active && _createdSurfaces.Contains(slot),
|
|
_reloadableSurfaces.Contains(slot));
|
|
})
|
|
.OrderBy(item => item.Slot)
|
|
.ToArray();
|
|
var objects = _orderedObjectHandles
|
|
.Select(handle => (handle, CloneState(_objects[handle])))
|
|
.ToArray();
|
|
return new GfxPersistenceSnapshot(
|
|
surfaces, objects, _rangeTransformFirst, _rangeTransformCount,
|
|
CloneState(_rangeTransform));
|
|
}
|
|
}
|
|
|
|
public void RestorePersistenceSnapshot(GfxPersistenceSnapshot snapshot)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(snapshot);
|
|
lock (_lock)
|
|
{
|
|
_surfaces.Clear();
|
|
_createdSurfaces.Clear();
|
|
_reloadableSurfaces.Clear();
|
|
foreach (GfxSurfacePersistenceState surface in snapshot.Surfaces)
|
|
{
|
|
if (surface.ResourceId >= 0 || surface.Created)
|
|
_surfaces[surface.Slot] = (surface.ResourceId, surface.ColorKey);
|
|
if (surface.Created) _createdSurfaces.Add(surface.Slot);
|
|
if (surface.ReloadOnRestore) _reloadableSurfaces.Add(surface.Slot);
|
|
}
|
|
_objects.Clear();
|
|
_orderedObjectHandles.Clear();
|
|
foreach (var (handle, state) in snapshot.Objects)
|
|
{
|
|
_objects[handle] = CloneState(state);
|
|
_orderedObjectHandles.Add(handle);
|
|
}
|
|
_orderedObjectHandles.Sort();
|
|
_rangeTransformFirst = snapshot.RangeFirst;
|
|
_rangeTransformCount = snapshot.RangeCount;
|
|
_rangeTransform = CloneState(snapshot.RangeTransform);
|
|
MarkRetainedMutation();
|
|
}
|
|
}
|
|
|
|
private static GfxObject CloneState(GfxObject s)
|
|
=> new()
|
|
{
|
|
NativePersistenceRecord = s.NativePersistenceRecord?.ToArray(),
|
|
V18 = s.V18, V24 = s.V24, V16c = s.V16c,
|
|
Color = s.Color, HasColor = s.HasColor, StaticColorMode = s.StaticColorMode,
|
|
OneShotColorTarget = s.OneShotColorTarget, ColorDelayMs = s.ColorDelayMs,
|
|
ColorDurationMs = s.ColorDurationMs, OneShotColorEnabled = s.OneShotColorEnabled,
|
|
OneShotColorBlend = s.OneShotColorBlend, PostBindStaticColorBlend = s.PostBindStaticColorBlend,
|
|
SrcFrameCount = s.SrcFrameCount, SrcColumns = s.SrcColumns, SrcCell = s.SrcCell,
|
|
SrcPeriod = s.SrcPeriod, SrcStart = s.SrcStart, SrcAnim = s.SrcAnim,
|
|
ColorPeriod = s.ColorPeriod, ColorStart = s.ColorStart, ColorTarget = s.ColorTarget,
|
|
ColorAnim = s.ColorAnim, SourceSlot = s.SourceSlot, SrcRect = s.SrcRect, Visible = s.Visible,
|
|
ScaleCurrent = s.ScaleCurrent, ScaleTarget = s.ScaleTarget,
|
|
ScaleDelayMs = s.ScaleDelayMs, ScaleDurationMs = s.ScaleDurationMs, ScaleEnabled = s.ScaleEnabled,
|
|
TranslationCurrent = s.TranslationCurrent, TranslationTarget = s.TranslationTarget,
|
|
TranslationDelayMs = s.TranslationDelayMs, TranslationDurationMs = s.TranslationDurationMs,
|
|
TranslationEnabled = s.TranslationEnabled,
|
|
RotationCurrent = s.RotationCurrent, RotationTarget = s.RotationTarget,
|
|
RotationDelayMs = s.RotationDelayMs, RotationDurationMs = s.RotationDurationMs,
|
|
RotationChannelEnabled = s.RotationChannelEnabled,
|
|
OneShotAnimationControlFlags = s.OneShotAnimationControlFlags,
|
|
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();
|
|
|
|
// ---- surfaces (image buffers per slot): ctx+0x52bd4[slot], from create/set-texture ----
|
|
private readonly Dictionary<int, (long ResId, long ColorKey)> _surfaces = new();
|
|
// Created surfaces have real pixels but no asset resource id. Keep their class separate from both
|
|
// loaded textures and truly surfaceless objects because native mode-0 consumes packed alpha differently.
|
|
private readonly HashSet<int> _createdSurfaces = new();
|
|
// Native surface record +0x08. Ordinary create/load/release workers leave this bit unchanged;
|
|
// the numbered-save restore loop consults it to decide which asset-backed surfaces to reopen.
|
|
private readonly HashSet<int> _reloadableSurfaces = new();
|
|
// A separate entry models the native CMovieToTexture object attached to a surface. A null value means
|
|
// the movie object exists but its host decoder supplied no usable IMediaPosition stop time.
|
|
private readonly Dictionary<int, long?> _movieStopTimesMs = new();
|
|
private readonly Dictionary<int, SurfaceTransition> _surfaceTransitions = new();
|
|
private readonly Dictionary<int, MovieMaskTransition> _movieMaskTransitions = new();
|
|
public void SetSurface(int slot, long resId, long colorKey)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
_surfaces[slot] = (resId, colorKey);
|
|
_createdSurfaces.Remove(slot);
|
|
_movieStopTimesMs.Remove(slot);
|
|
MarkRetainedMutation();
|
|
}
|
|
}
|
|
|
|
/// <summary>Set the native surface-record +0x08 reload policy. This is separate from loading a
|
|
/// texture because AGE's ordinary create/load/release workers preserve the existing bit.</summary>
|
|
public void SetSurfaceReloadOnRestore(int slot, bool reload)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
if (reload) _reloadableSurfaces.Add(slot);
|
|
else _reloadableSurfaces.Remove(slot);
|
|
}
|
|
}
|
|
|
|
/// <summary>Opcode 0x259 script-entry lifecycle: clear record +0x08 for every surface.
|
|
/// Native also clears the adjacent unknown +0x0c field, which the port does not otherwise model.</summary>
|
|
public void ClearSurfaceReloadPolicies()
|
|
{
|
|
lock (_lock) _reloadableSurfaces.Clear();
|
|
}
|
|
|
|
/// <summary>Op 0x236 handoff: retain the initialized movie graph's IMediaPosition stop time. Null
|
|
/// deliberately distinguishes a movie surface with unavailable metadata from an empty movie slot.</summary>
|
|
public void SetMovieStopTime(int slot, long? stopTimeMs)
|
|
{
|
|
lock (_lock) _movieStopTimesMs[slot] = stopTimeMs;
|
|
}
|
|
|
|
/// <summary>Op 0x23f query. False means no movie object occupies the slot; true with a null value
|
|
/// means the movie exists but its stop-time query failed or returned unusable metadata.</summary>
|
|
public bool TryGetMovieStopTime(int slot, out long? stopTimeMs)
|
|
{
|
|
lock (_lock) return _movieStopTimesMs.TryGetValue(slot, out stopTimeMs);
|
|
}
|
|
|
|
/// <summary>Op 0x20d: select a surface as the D3D render target; values at or above 1000 restore the
|
|
/// device backbuffer in the native engine.</summary>
|
|
public void SelectRenderTarget(long slot)
|
|
{
|
|
lock (_lock) CurrentRenderTargetSlot = slot is >= 0 and < 1000 ? (int)slot : -1;
|
|
}
|
|
|
|
/// <summary>Op 0x23d: release the transient surface range while retaining system-owned low slots.</summary>
|
|
public void ReleaseSurfaceRange(int firstSlot, int count)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
int end = checked(firstSlot + count);
|
|
for (int slot = firstSlot; slot < end; slot++)
|
|
{
|
|
_surfaces.Remove(slot);
|
|
_createdSurfaces.Remove(slot);
|
|
_movieStopTimesMs.Remove(slot);
|
|
_surfaceTransitions.Remove(slot);
|
|
_movieMaskTransitions.Remove(slot);
|
|
}
|
|
if (CurrentRenderTargetSlot >= firstSlot && CurrentRenderTargetSlot < end)
|
|
CurrentRenderTargetSlot = -1;
|
|
MarkRetainedMutation();
|
|
}
|
|
}
|
|
|
|
/// <summary>Ops 0x202/0x203: record a packed 0xAARRGGBB color/alpha modulation on the object and mark it
|
|
/// HasColor so the compositor applies alpha+tint (vs the opaque default).</summary>
|
|
public void SetObjectColor(long handle, long packed)
|
|
{
|
|
lock (_lock) { var o = GetOrCreate(handle); o.Color = packed; o.HasColor = true; }
|
|
}
|
|
|
|
/// <summary>Resolve 0x202/0x203's native negative sentinels against obj+0x60 (the current static
|
|
/// packed color): negative alpha preserves its alpha byte; negative RGB preserves its RGB bytes.</summary>
|
|
public void SetObjectColorResolved(long handle, long alpha, long rgb)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
var o = GetOrCreate(handle);
|
|
long current = o.HasColor ? o.Color : 0xffffffff;
|
|
long resolvedAlpha = alpha < 0 ? (current >> 24) & 0xff : System.Math.Min(alpha, 0xff);
|
|
long resolvedRgb = rgb < 0 ? current & 0xffffff : rgb & 0xffffff;
|
|
o.Color = PackColor(resolvedAlpha, resolvedRgb);
|
|
o.HasColor = true;
|
|
}
|
|
}
|
|
|
|
/// <summary>Op 0x202: arm the delayed one-shot packed-color interpolation without replacing the
|
|
/// current static color. Negative alpha/RGB operands resolve from current obj+0x60 independently.</summary>
|
|
public void SetAnimatedObjectColorResolved(long handle, long delayMs, long durationMs, long alpha, long rgb)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
var o = GetOrCreate(handle);
|
|
long current = o.HasColor ? o.Color : 0xffffffff;
|
|
long resolvedAlpha = alpha < 0 ? (current >> 24) & 0xff : System.Math.Min(alpha, 0xff);
|
|
long resolvedRgb = rgb < 0 ? current & 0xffffff : rgb & 0xffffff;
|
|
if (!o.HasColor) { o.Color = current; o.HasColor = true; }
|
|
o.OneShotColorTarget = PackColor(resolvedAlpha, resolvedRgb);
|
|
o.ColorDelayMs = delayMs;
|
|
o.ColorDurationMs = durationMs;
|
|
o.OneShotColorEnabled = true;
|
|
o.OneShotColorBlend = true;
|
|
o.OneShotStartMs = -1;
|
|
}
|
|
}
|
|
|
|
public void SetStaticObjectColorResolved(long handle, long mode, long alpha, long rgb)
|
|
{
|
|
SetObjectColorResolved(handle, alpha, rgb);
|
|
lock (_lock)
|
|
{
|
|
var o = GetOrCreate(handle);
|
|
o.StaticColorMode = mode;
|
|
o.PostBindStaticColorBlend = mode == 0 && o.Visible;
|
|
}
|
|
}
|
|
|
|
/// <summary>Ops 0x239 (one-shot cell endpoint, period=0 in this retained model) / 0x231 (looping):
|
|
/// set total frame count, column count, and visible cell. Every frame retains draw-texture's source
|
|
/// rectangle dimensions. For 0x231, <paramref name="period"/> is milliseconds per frame.</summary>
|
|
public void SetSrcRect(long handle, long frameCount, long columns, long cell, long period)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
var o = GetOrCreate(handle);
|
|
o.SrcFrameCount = frameCount < 1 ? 1 : frameCount;
|
|
o.SrcColumns = columns < 1 ? 1 : columns;
|
|
o.SrcCell = cell; o.SrcPeriod = period; o.SrcStart = -1; o.SrcAnim = true;
|
|
}
|
|
}
|
|
|
|
/// <summary>Op 0x232 worker contract: ping-pong a temporary copy of static packed ARGB toward
|
|
/// <paramref name="target"/>. The sampled value is consumed by the object's unchanged blend mode.</summary>
|
|
public void SetColorAnim(long handle, long period, long target)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
var o = GetOrCreate(handle);
|
|
o.ColorPeriod = period; o.ColorTarget = target; o.ColorStart = -1; o.ColorAnim = true;
|
|
}
|
|
}
|
|
|
|
/// <summary>Op 0x232 handler contract: resolve negative alpha/RGB sentinels from static obj+0x60,
|
|
/// clamp alpha above 255, then arm the cyclic packed-color channel without changing blend mode.</summary>
|
|
public void SetColorAnimResolved(long handle, long period, long alpha, long rgb)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
var o = GetOrCreate(handle);
|
|
long current = o.Color & 0xffffffff;
|
|
long resolvedAlpha = alpha < 0 ? (current >> 24) & 0xff : System.Math.Min(alpha, 0xff);
|
|
long resolvedRgb = rgb < 0 ? current & 0xffffff : rgb & 0xffffff;
|
|
o.ColorPeriod = period;
|
|
o.ColorTarget = PackColor(resolvedAlpha, resolvedRgb);
|
|
o.ColorStart = -1;
|
|
o.ColorAnim = true;
|
|
}
|
|
}
|
|
public void CreateSurface(int slot)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
_surfaces[slot] = (0, -1); // create-texture: real mutable pixels, no asset id or color key
|
|
_createdSurfaces.Add(slot);
|
|
_movieStopTimesMs.Remove(slot);
|
|
MarkRetainedMutation();
|
|
}
|
|
}
|
|
|
|
public void ClearSurface(int slot)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
_surfaces.Remove(slot);
|
|
_createdSurfaces.Remove(slot);
|
|
_movieStopTimesMs.Remove(slot);
|
|
_surfaceTransitions.Remove(slot);
|
|
_movieMaskTransitions.Remove(slot);
|
|
MarkRetainedMutation();
|
|
}
|
|
}
|
|
|
|
/// <summary>Op 0x223: queue a type-0 timed alpha transition into a target surface slot.</summary>
|
|
public void QueueSurfaceAlphaTransition(long commandKey, int targetSlot,
|
|
long rangeAStart, int rangeACount, long rangeBStart, int rangeBCount,
|
|
long delayMs, long durationMs)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
_surfaceTransitions[targetSlot] = new SurfaceTransition
|
|
{
|
|
CommandKey = commandKey, TargetSlot = targetSlot,
|
|
RangeAStart = rangeAStart, RangeACount = System.Math.Max(0, rangeACount),
|
|
RangeBStart = rangeBStart, RangeBCount = System.Math.Max(0, rangeBCount),
|
|
DelayMs = System.Math.Max(0, delayMs), DurationMs = System.Math.Max(0, durationMs),
|
|
};
|
|
MarkRetainedMutation();
|
|
}
|
|
}
|
|
|
|
public void QueueMovieMaskTransition(MovieMaskTransitionRequest request)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
_movieMaskTransitions[request.SurfaceSlot] = new MovieMaskTransition
|
|
{
|
|
Request = request with
|
|
{
|
|
SourceRangeCount = System.Math.Max(0, request.SourceRangeCount),
|
|
Width = System.Math.Max(0, request.Width),
|
|
Height = System.Math.Max(0, request.Height),
|
|
StartDelayMs = System.Math.Max(0, request.StartDelayMs),
|
|
DurationMs = System.Math.Max(0, request.DurationMs),
|
|
},
|
|
};
|
|
MarkRetainedMutation();
|
|
}
|
|
}
|
|
|
|
public bool CompleteMovieMaskTransition(int surfaceSlot)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
if (!_movieMaskTransitions.TryGetValue(surfaceSlot, out var transition)
|
|
|| transition.Completed)
|
|
return false;
|
|
transition.Completed = true;
|
|
MarkRetainedMutation();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public IReadOnlyList<MovieMaskTransitionState> SnapshotMovieMaskTransitions()
|
|
{
|
|
lock (_lock)
|
|
return _movieMaskTransitions.Values
|
|
.Select(t => new MovieMaskTransitionState(t.Request, t.Completed)).ToList();
|
|
}
|
|
|
|
/// <summary>Start every pending foreground transition at the native present boundary.</summary>
|
|
public int StartForegroundTransitions(long nowMs)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
int started = 0;
|
|
foreach (var t in _surfaceTransitions.Values)
|
|
if (t.StartMs < 0) { t.StartMs = nowMs; started++; }
|
|
return started;
|
|
}
|
|
}
|
|
|
|
public bool HasActiveForegroundTransitions(long nowMs)
|
|
{
|
|
lock (_lock)
|
|
return _surfaceTransitions.Values.Any(t => TransitionProgress(t, nowMs) < 1.0)
|
|
|| _movieMaskTransitions.Values.Any(t => !t.Completed);
|
|
}
|
|
|
|
/// <summary>Native op 0x21c keeps presenting until both queued surface commands and finite one-shot
|
|
/// object channels have completed. Ambient cyclic/spritesheet/color pulses are deliberately excluded.</summary>
|
|
public bool HasActiveTimedPresentation(long nowMs)
|
|
{
|
|
lock (_lock)
|
|
return _surfaceTransitions.Values.Any(t => TransitionProgress(t, nowMs) < 1.0) ||
|
|
_movieMaskTransitions.Values.Any(t => !t.Completed) ||
|
|
_rangeTransform.ScaleEnabled || _rangeTransform.RotationChannelEnabled ||
|
|
_rangeTransform.TranslationEnabled ||
|
|
_objects.Values.Any(o => o.Visible &&
|
|
(o.OneShotAnimationControlFlags & 1) == 0 &&
|
|
(o.OneShotColorEnabled || o.ScaleEnabled ||
|
|
o.RotationChannelEnabled || o.TranslationEnabled));
|
|
}
|
|
|
|
/// <summary>Observe-only state for a runtime stall capture. It identifies the exact finite channels
|
|
/// which can keep the host's op-0x21c presentation wait active.</summary>
|
|
public GfxDiagnosticSnapshot CaptureDiagnosticSnapshot(long nowMs)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
BlockingGfxObjectDiagnostic? range = HasBlockingChannels(_rangeTransform)
|
|
? DescribeBlockingObject(-1, _rangeTransform)
|
|
: null;
|
|
var objects = _objects
|
|
.Where(pair => pair.Value.Visible
|
|
&& (pair.Value.OneShotAnimationControlFlags & 1) == 0
|
|
&& HasBlockingChannels(pair.Value))
|
|
.OrderBy(pair => pair.Key)
|
|
.Select(pair => DescribeBlockingObject(pair.Key, pair.Value))
|
|
.ToArray();
|
|
return new GfxDiagnosticSnapshot(
|
|
nowMs,
|
|
_surfaceTransitions.Values.Any(t => TransitionProgress(t, nowMs) < 1.0)
|
|
|| _movieMaskTransitions.Values.Any(t => !t.Completed)
|
|
|| range != null || objects.Length != 0,
|
|
_objects.Count,
|
|
_objects.Values.Count(o => o.Visible),
|
|
_surfaceTransitions.Values.Count(t => TransitionProgress(t, nowMs) < 1.0)
|
|
+ _movieMaskTransitions.Values.Count(t => !t.Completed),
|
|
AnimationServiceFlags,
|
|
AnimClockDurationTicks,
|
|
AnimClockGeneration,
|
|
PreviousFrameTimeMilliseconds,
|
|
CurrentFrameTimeMilliseconds,
|
|
_rangeTransformFirst,
|
|
_rangeTransformCount,
|
|
range,
|
|
objects);
|
|
}
|
|
}
|
|
|
|
private static bool HasBlockingChannels(GfxObject o)
|
|
=> o.OneShotColorEnabled || o.ScaleEnabled || o.RotationChannelEnabled || o.TranslationEnabled;
|
|
|
|
private static BlockingGfxObjectDiagnostic DescribeBlockingObject(long handle, GfxObject o)
|
|
=> new(handle, o.SourceSlot, o.OneShotStartMs, o.OneShotAnimationControlFlags,
|
|
o.OneShotColorEnabled, o.ColorDelayMs, o.ColorDurationMs,
|
|
o.ScaleEnabled, o.ScaleDelayMs, o.ScaleDurationMs,
|
|
o.RotationChannelEnabled, o.RotationDelayMs, o.RotationDurationMs,
|
|
o.TranslationEnabled, o.TranslationDelayMs, o.TranslationDurationMs);
|
|
|
|
/// <summary>Op 0x242: replace the retained object's animation-control word. Native bit 0 makes its
|
|
/// finite one-shot channels nonblocking and immune to op 0x243 forced completion.</summary>
|
|
public void SetOneShotAnimationControl(long handle, long flags)
|
|
{
|
|
lock (_lock) GetOrCreate(handle).OneShotAnimationControlFlags = flags;
|
|
}
|
|
|
|
/// <summary>Part of op 0x243: immediately commit every unprotected finite one-shot channel. Objects
|
|
/// marked by op 0x242 bit 0 keep sampling asynchronously.</summary>
|
|
private void ForceCompleteOneShotChannels()
|
|
{
|
|
CommitOneShotChannels(_rangeTransform);
|
|
foreach (var o in _objects.Values)
|
|
{
|
|
if ((o.OneShotAnimationControlFlags & 1) != 0) continue;
|
|
CommitOneShotChannels(o);
|
|
}
|
|
}
|
|
|
|
private static void CommitOneShotChannels(GfxObject o)
|
|
{
|
|
if (o.OneShotColorEnabled)
|
|
{
|
|
o.Color = o.OneShotColorTarget & 0xffffffff;
|
|
o.OneShotColorTarget = -1;
|
|
o.ColorDelayMs = 0;
|
|
o.ColorDurationMs = 0;
|
|
o.OneShotColorEnabled = false;
|
|
}
|
|
if (o.ScaleEnabled)
|
|
{
|
|
o.ScaleCurrent = o.ScaleTarget;
|
|
o.ScaleDelayMs = 0;
|
|
o.ScaleDurationMs = 0;
|
|
o.ScaleEnabled = false;
|
|
}
|
|
if (o.RotationChannelEnabled)
|
|
{
|
|
o.RotationCurrent = o.RotationTarget;
|
|
o.RotationDelayMs = 0;
|
|
o.RotationDurationMs = 0;
|
|
o.RotationChannelEnabled = false;
|
|
}
|
|
if (o.TranslationEnabled)
|
|
{
|
|
o.TranslationCurrent = o.TranslationTarget;
|
|
o.TranslationDelayMs = 0;
|
|
o.TranslationDurationMs = 0;
|
|
o.TranslationEnabled = false;
|
|
}
|
|
o.OneShotStartMs = -1;
|
|
}
|
|
|
|
/// <summary>Whether sampling the retained scene at a later frame can change its pixels without another
|
|
/// VM mutation. Includes finite presentation work plus the ambient channels that may remain active while
|
|
/// the interpreter is parked at an input wait. Static waits themselves are deliberately not animation.</summary>
|
|
public bool HasActiveVisualPresentation(long nowMs)
|
|
{
|
|
lock (_lock)
|
|
return _surfaceTransitions.Values.Any(t => TransitionProgress(t, nowMs) < 1.0) ||
|
|
_movieMaskTransitions.Values.Any(t => !t.Completed) ||
|
|
_rangeTransform.ScaleEnabled || _rangeTransform.RotationChannelEnabled ||
|
|
_rangeTransform.TranslationEnabled ||
|
|
_objects.Values.Any(o => o.Visible &&
|
|
(o.OneShotColorEnabled || o.ScaleEnabled || o.RotationChannelEnabled ||
|
|
o.TranslationEnabled ||
|
|
(o.SrcAnim && o.SrcPeriod > 0) ||
|
|
(o.ColorAnim && o.ColorPeriod > 0) ||
|
|
(o.ScaleCycleEnabled && o.ScaleCyclePeriodMs > 0) ||
|
|
(o.RotationEnabled && o.RotationPeriodMs > 0)));
|
|
}
|
|
|
|
/// <summary>Click completion affects only type-0 foreground transitions, never ambient object channels.</summary>
|
|
public int CompleteForegroundTransitions(long nowMs)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
|
|
public IReadOnlyList<SurfaceTransitionState> SnapshotForegroundTransitions(long nowMs)
|
|
{
|
|
lock (_lock)
|
|
return _surfaceTransitions.Values.Select(t => SampleTransition(t, nowMs)).ToList();
|
|
}
|
|
|
|
private static double TransitionProgress(SurfaceTransition t, long nowMs)
|
|
{
|
|
if (t.Forced) return 1.0;
|
|
if (t.StartMs < 0) return 0.0;
|
|
long elapsed = nowMs - t.StartMs - t.DelayMs;
|
|
if (elapsed <= 0) return 0.0;
|
|
if (t.DurationMs <= 0) return 1.0;
|
|
return System.Math.Clamp(elapsed / (double)t.DurationMs, 0.0, 1.0);
|
|
}
|
|
|
|
private static SurfaceTransitionState SampleTransition(SurfaceTransition t, long nowMs)
|
|
=> new(t.CommandKey, t.TargetSlot, t.RangeAStart, t.RangeACount, t.RangeBStart, t.RangeBCount,
|
|
t.DelayMs, t.DurationMs, t.StartMs, TransitionProgress(t, nowMs), t.Forced);
|
|
|
|
/// <summary>draw-texture bind (gfx_object_bind_draw): object <paramref name="handle"/> draws surface
|
|
/// <paramref name="slot"/>'s rect at (dstX,dstY) and becomes visible.</summary>
|
|
public void BindDraw(long handle, int slot, int sx, int sy, int w, int h, int dstX, int dstY)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
var o = GetOrCreate(handle);
|
|
o.SourceSlot = slot; o.SrcRect = (sx, sy, w, h); o.V24 = (dstX, dstY, 0); o.Visible = true;
|
|
o.PostBindStaticColorBlend = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>Op 0x13a: replace one of eleven safely isolated handler-addressable styles.
|
|
/// Native physically clears ten records; its admitted index 10 aliases return-stack storage.</summary>
|
|
public bool RegisterNumericGlyphStyle(int styleIndex, int surfaceSlot, int atlasX, int atlasY,
|
|
int digitWidth, int digitHeight)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
if ((uint)styleIndex >= (uint)_numericGlyphStyles.Length) return false;
|
|
_numericGlyphStyles[styleIndex] =
|
|
new NumericGlyphStyle(surfaceSlot, atlasX, atlasY, digitWidth, digitHeight);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/// <summary>Op 0x23b: erase a destination handle range, split a signed 32-bit value into decimal
|
|
/// digits, and bind one retained object per displayed atlas cell. Returns false for an invalid or
|
|
/// unregistered style, matching the native handler's script-error path.</summary>
|
|
public bool DrawDecimalGlyphs(long baseHandle, int styleIndex, int value, int x, int y,
|
|
int digitCapacity, int flags)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
if ((uint)styleIndex >= (uint)_numericGlyphStyles.Length
|
|
|| !_numericGlyphStyles[styleIndex].Registered) return false;
|
|
|
|
NumericGlyphStyle style = _numericGlyphStyles[styleIndex];
|
|
EraseRange(baseHandle, digitCapacity);
|
|
|
|
int remaining = value;
|
|
int handleOffset = 0;
|
|
int digitCount = 1;
|
|
for (int n = remaining / 10; n != 0; n /= 10) digitCount++;
|
|
|
|
for (int slot = digitCapacity - 1; slot >= 0; slot--)
|
|
{
|
|
int digit = remaining % 10;
|
|
int drawX;
|
|
bool draw;
|
|
|
|
if ((flags & 0x2) != 0)
|
|
{
|
|
draw = slot == digitCapacity - 1 || remaining != 0;
|
|
drawX = unchecked(x + style.DigitWidth * slot
|
|
- (digitCapacity - digitCount) * style.DigitWidth / 2);
|
|
}
|
|
else if ((flags & 0x4) != 0)
|
|
{
|
|
digitCount--;
|
|
draw = slot == digitCapacity - 1 || remaining != 0;
|
|
drawX = unchecked(x + style.DigitWidth * digitCount);
|
|
}
|
|
else
|
|
{
|
|
draw = (flags & 0x1) != 0 || slot == digitCapacity - 1 || remaining != 0;
|
|
drawX = unchecked(x + style.DigitWidth * slot);
|
|
}
|
|
|
|
if (draw)
|
|
{
|
|
int srcX = unchecked(style.AtlasX + digit * style.DigitWidth);
|
|
BindDraw(baseHandle + handleOffset, style.SurfaceSlot,
|
|
srcX, style.AtlasY, style.DigitWidth, style.DigitHeight, drawX, y);
|
|
handleOffset++;
|
|
}
|
|
remaining /= 10;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/// <summary>Op 0x1fd: immediately replace the object's current scale matrix. Script operands are
|
|
/// integer percentages; native divides them by 100 before matrix4_make_scale at obj+0x6c.</summary>
|
|
public void SetCurrentScale(long handle, (long X, long Y, long Z) percent)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
var o = GetOrCreate(handle);
|
|
o.ScaleCurrent = (percent.X / 100.0, percent.Y / 100.0, percent.Z / 100.0);
|
|
}
|
|
}
|
|
|
|
/// <summary>Op 0x1fe: immediately replace the object's current axis-angle rotation matrix at
|
|
/// obj+0xec. Axis components and angle are retained as native floating-point values.</summary>
|
|
public void SetCurrentRotation(long handle, (long X, long Y, long Z) axis, long angleDegrees)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
var o = GetOrCreate(handle);
|
|
o.RotationCurrent = (axis.X, axis.Y, axis.Z, angleDegrees);
|
|
}
|
|
}
|
|
|
|
/// <summary>Op 0x1ff: immediately replace the object's current translation matrix at obj+0x16c.</summary>
|
|
public void SetCurrentTranslation(long handle, (long X, long Y, long Z) translation)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
var o = GetOrCreate(handle);
|
|
o.V16c = translation;
|
|
o.TranslationCurrent = translation;
|
|
}
|
|
}
|
|
|
|
/// <summary>Op 0x21e: normalized scale target (100 = identity), with independent delay/duration.</summary>
|
|
public void SetScaleChannel(long handle, long delayMs, long durationMs, (long X, long Y, long Z) percent)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
var o = GetOrCreate(handle);
|
|
// Native setters get-or-create first, then require object flag bit 0 (draw-bound/visible).
|
|
// Pre-bind calls leave a neutral, queryable placeholder and do not queue latent motion.
|
|
if (!o.Visible) return;
|
|
o.ScaleDelayMs = delayMs; o.ScaleDurationMs = durationMs;
|
|
o.ScaleTarget = (percent.X / 100.0, percent.Y / 100.0, percent.Z / 100.0);
|
|
o.ScaleEnabled = durationMs > 0; o.OneShotStartMs = -1;
|
|
}
|
|
}
|
|
|
|
/// <summary>Op 0x220: absolute translation target, with independent delay/duration.</summary>
|
|
public void SetTranslationChannel(long handle, long delayMs, long durationMs, (long X, long Y, long Z) target)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
var o = GetOrCreate(handle);
|
|
if (!o.Visible) return;
|
|
o.TranslationDelayMs = delayMs; o.TranslationDurationMs = durationMs;
|
|
o.TranslationTarget = target;
|
|
o.TranslationEnabled = durationMs > 0; o.OneShotStartMs = -1;
|
|
}
|
|
}
|
|
|
|
/// <summary>Op 0x228: query the translation target decomposed from the native target matrix at
|
|
/// obj+0x17c (translation obj+0x1ac/+0x1b0/+0x1b4). This is independent of draw/base position V24.</summary>
|
|
public bool TryQueryTranslationTarget(long handle, out (double X, double Y, double Z) target)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
if (_objects.TryGetValue(handle, out var o))
|
|
{
|
|
target = o.TranslationTarget;
|
|
return true;
|
|
}
|
|
target = default;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>Op 0x21f: delayed one-shot axis-angle rotation target, sharing obj+0x34's start timestamp.</summary>
|
|
public void SetRotationChannel(long handle, long delayMs, long durationMs,
|
|
(long X, long Y, long Z) axis, long angleDegrees)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
var o = GetOrCreate(handle);
|
|
if (!o.Visible) return;
|
|
o.RotationDelayMs = delayMs; o.RotationDurationMs = durationMs;
|
|
o.RotationTarget = (axis.X, axis.Y, axis.Z, angleDegrees);
|
|
o.RotationChannelEnabled = durationMs > 0; o.OneShotStartMs = -1;
|
|
}
|
|
}
|
|
|
|
/// <summary>Op 0x234: retain the cyclic rotation period and axis separately. Native interpolation uses
|
|
/// frame-time retained-gfx owner+0xb550 (EngineCtx+0x51b64) and rotates through 360 degrees per period.</summary>
|
|
public void SetRotationCycle(long handle, long periodMs, (long X, long Y, long Z) axis)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
var o = GetOrCreate(handle);
|
|
o.RotationPeriodMs = periodMs; o.RotationAxis = axis; o.RotationEnabled = periodMs > 0;
|
|
o.RotationStartMs = -1;
|
|
}
|
|
}
|
|
|
|
/// <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 0x230: stop every cyclic object channel without changing its base/current state,
|
|
/// one-shot channels, or retained cyclic targets. Native clears flag bit 2 and the five start/period
|
|
/// pairs at obj+0x20c..0x230, including one secondary matrix channel not otherwise modeled here.</summary>
|
|
public void ResetCyclicAnimationChannels(long handle)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
var o = GetOrCreate(handle);
|
|
o.ColorStart = -1;
|
|
o.ScaleCycleStartMs = -1;
|
|
o.RotationStartMs = -1;
|
|
o.SrcStart = -1;
|
|
o.ColorPeriod = 0;
|
|
o.ScaleCyclePeriodMs = 0;
|
|
o.RotationPeriodMs = 0;
|
|
o.SrcPeriod = 0;
|
|
o.ColorAnim = false;
|
|
o.ScaleCycleEnabled = false;
|
|
o.RotationEnabled = false;
|
|
o.SrcAnim = false;
|
|
|
|
// Preserve drop-in numbered-save compatibility for the unmodeled secondary cyclic matrix
|
|
// as well as the modeled channels. The ten native timing dwords form one contiguous range.
|
|
if (o.NativePersistenceRecord is { Length: >= 0x234 } raw)
|
|
{
|
|
raw[0] &= 0xfb;
|
|
System.Array.Clear(raw, 0x20c, 0x28);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Op 0x238: set its separate global animation-service duration and reset marker.</summary>
|
|
public void SetAnimClock(long durationTicks)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
AnimClockDurationTicks = durationTicks;
|
|
AnimClockGeneration++;
|
|
MarkRetainedMutation();
|
|
}
|
|
}
|
|
|
|
public void SetAnimationServiceFlags(long flags)
|
|
{
|
|
lock (_lock) AnimationServiceFlags = unchecked((uint)flags);
|
|
}
|
|
|
|
public void SampleFrameTime(long nowMilliseconds)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
_previousFrameTimeMs = _currentFrameTimeMs;
|
|
_currentFrameTimeMs = nowMilliseconds;
|
|
PreviousFrameTimeMilliseconds = CurrentFrameTimeMilliseconds;
|
|
CurrentFrameTimeMilliseconds = unchecked((uint)nowMilliseconds);
|
|
}
|
|
}
|
|
|
|
/// <summary>Op 0x243: force ordinary finite channels to their endpoints and reset the separate
|
|
/// global animation-service clock. Op-0x242-detached objects ignore the completion request.</summary>
|
|
public void ResetAnimClock()
|
|
{
|
|
lock (_lock)
|
|
{
|
|
if ((AnimationServiceFlags & 2) != 0) return;
|
|
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.ScaleCycleEnabled && o.ScaleCyclePeriodMs > 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);
|
|
|
|
/// <summary>Visible objects in ascending-handle order (= z-order), each with its source surface resolved
|
|
/// and its active channels interpolated at <paramref name="nowMs"/>. Position is the base V24 (a direct
|
|
/// transform (op 0x22f); scale and translation are independent one-shot matrix channels. Ops
|
|
/// 0x229-0x22e contribute a second sampled matrix only to their selected handle range. The
|
|
/// src-rect channel (0x239/0x231) selects the spritesheet cell; the color channel (0x232) ping-pongs the
|
|
/// 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)
|
|
{
|
|
list.Clear();
|
|
Affine2D? rangeAffine = null;
|
|
bool rangeTimeVarying = false;
|
|
if (_rangeTransformCount > 0)
|
|
{
|
|
var r = _rangeTransform;
|
|
bool hadRangeOneShot = r.ScaleEnabled || r.RotationChannelEnabled || r.TranslationEnabled;
|
|
if (hadRangeOneShot && r.OneShotStartMs < 0) r.OneShotStartMs = nowMs;
|
|
var rangeScale = SampleMatrixChannel(ref r.ScaleCurrent, r.ScaleTarget, r.ScaleDelayMs,
|
|
r.ScaleDurationMs, r.OneShotStartMs, ref r.ScaleEnabled, nowMs);
|
|
var rangeRotation = SampleRotationChannel(ref r.RotationCurrent, r.RotationTarget,
|
|
r.RotationDelayMs, r.RotationDurationMs, r.OneShotStartMs,
|
|
ref r.RotationChannelEnabled, nowMs);
|
|
var rangeTranslation = SampleMatrixChannel(ref r.TranslationCurrent, r.TranslationTarget,
|
|
r.TranslationDelayMs, r.TranslationDurationMs, r.OneShotStartMs,
|
|
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 (long handle in _orderedObjectHandles)
|
|
{
|
|
var o = _objects[handle];
|
|
if (!o.Visible) continue;
|
|
bool hadOneShot = o.OneShotColorEnabled || o.ScaleEnabled ||
|
|
o.RotationChannelEnabled || o.TranslationEnabled;
|
|
// Created/mutable surfaces have no file resource id but remain a distinct surface class.
|
|
// Zero is an active "key black" value, so created and absent slots both default to -1.
|
|
var (resId, ck) = _surfaces.TryGetValue(o.SourceSlot, out var s) ? s : (0L, -1L);
|
|
bool createdSurface = _createdSurfaces.Contains(o.SourceSlot);
|
|
|
|
// ---- packed color: a static mode 0 treats alpha as tint/fill strength. Once op 0x202 has
|
|
// armed the one-shot channel, its current/target ARGB instead supplies opacity and D3D-style
|
|
// multiplicative RGB modulation (including after target commit). Mode 1 uses the same blend.
|
|
// Op 0x232 modifies this temporary packed color before the unchanged mode consumes it. ----
|
|
int alpha = 255; long tint = 0xFFFFFF; int strength = 0; var blend = BlendKind.Opaque;
|
|
bool multiplyTint = false;
|
|
long sampledColor = o.HasColor ? o.Color : 0xffffffff;
|
|
ColorTransitionState? colorTransition = null;
|
|
if (o.OneShotColorEnabled)
|
|
{
|
|
if (o.OneShotStartMs < 0) o.OneShotStartMs = nowMs;
|
|
(sampledColor, colorTransition) = SampleOneShotColor(o, nowMs);
|
|
}
|
|
if (o.ColorAnim)
|
|
{
|
|
if (o.ColorStart < 0) o.ColorStart = nowMs;
|
|
sampledColor = InterpolatePackedColor(sampledColor, o.ColorTarget,
|
|
PingPongWeight(nowMs, o.ColorStart, o.ColorPeriod));
|
|
}
|
|
if (o.HasColor || o.ColorAnim)
|
|
{
|
|
var (a, r, g, b) = BlendMath.UnpackArgb(sampledColor);
|
|
tint = ((long)r << 16) | ((long)g << 8) | (long)b;
|
|
if (o.StaticColorMode == 1)
|
|
{
|
|
// Native mode 1 enables SRCALPHA/ONE additive blending and passes packed ARGB as
|
|
// D3D modulation. Black therefore contributes nothing (TITLE's SO022 flames),
|
|
// while the high byte scales the additive source contribution.
|
|
alpha = a; strength = 0; blend = BlendKind.Additive; multiplyTint = true;
|
|
}
|
|
else if (o.StaticColorMode == 0 && o.OneShotColorBlend)
|
|
{
|
|
alpha = a; strength = 0; blend = BlendKind.Alpha; multiplyTint = true;
|
|
}
|
|
else if (o.StaticColorMode == 0 && o.PostBindStaticColorBlend)
|
|
{
|
|
// FIELD suppresses a still-bound idle unit while its clone moves; CALLBACK_SETTING
|
|
// rebuilds the erased ADV backing and writes the configured opacity after binding it.
|
|
// Pre-bind static alpha-zero CG initialization remains the opaque mode-0 path.
|
|
alpha = a; strength = 0; blend = BlendKind.Alpha; multiplyTint = true;
|
|
}
|
|
else if (o.StaticColorMode == 2)
|
|
{
|
|
// Native transition-source mode: 0xffffffff is opaque identity modulation,
|
|
// not a request to replace every texel with white.
|
|
alpha = a; strength = 0; blend = BlendKind.Alpha;
|
|
}
|
|
else if (createdSurface)
|
|
{
|
|
// Native created/render-target surfaces use packed alpha as object opacity.
|
|
// SYSTEM4/BUNKI relies on this for translucent panels; FIELD uses the same surface
|
|
// at alpha 0x40 beneath the minimap so the paper chrome remains visible.
|
|
alpha = a; strength = 0; blend = BlendKind.Alpha; multiplyTint = true;
|
|
}
|
|
else if (resId != 0)
|
|
{
|
|
// Native mode 0 is the opaque textured path. RGB modulates the source and the
|
|
// packed alpha byte does not become tint strength; in particular 0xffffffff is
|
|
// identity after the foreground-transition cleanup write.
|
|
alpha = 255; strength = 0; blend = BlendKind.Alpha; multiplyTint = true;
|
|
}
|
|
else
|
|
{
|
|
// A surfaceless mode-0 object is a solid fill, whose high byte remains fill strength.
|
|
strength = a; blend = BlendKind.Alpha;
|
|
}
|
|
}
|
|
// ---- src-rect: preserve cell dimensions and offset it row-major through the sheet ----
|
|
int srcX = o.SrcRect.X, srcY = o.SrcRect.Y, w = o.SrcRect.W, h = o.SrcRect.H;
|
|
if (o.SrcAnim && o.SrcFrameCount >= 1)
|
|
{
|
|
long cell = o.SrcCell;
|
|
if (o.SrcPeriod > 0)
|
|
{
|
|
if (o.SrcStart < 0) o.SrcStart = nowMs;
|
|
cell = ((nowMs - o.SrcStart) / o.SrcPeriod) % o.SrcFrameCount;
|
|
}
|
|
srcX = o.SrcRect.X + (int)(cell % o.SrcColumns) * o.SrcRect.W;
|
|
srcY = o.SrcRect.Y + (int)(cell / o.SrcColumns) * o.SrcRect.H;
|
|
}
|
|
|
|
// One-shot matrix channels: hold current through delay, then linearly sample current -> target.
|
|
if (hadOneShot
|
|
&& o.OneShotStartMs < 0)
|
|
o.OneShotStartMs = nowMs;
|
|
var scale = SampleMatrixChannel(ref o.ScaleCurrent, o.ScaleTarget, o.ScaleDelayMs,
|
|
o.ScaleDurationMs, o.OneShotStartMs, ref o.ScaleEnabled, nowMs);
|
|
var rotation = SampleRotationChannel(ref o.RotationCurrent, o.RotationTarget,
|
|
o.RotationDelayMs, o.RotationDurationMs,
|
|
o.OneShotStartMs, ref o.RotationChannelEnabled, nowMs);
|
|
var translation = SampleMatrixChannel(ref o.TranslationCurrent, o.TranslationTarget,
|
|
o.TranslationDelayMs, o.TranslationDurationMs,
|
|
o.OneShotStartMs, ref o.TranslationEnabled, nowMs);
|
|
if (!o.OneShotColorEnabled && !o.ScaleEnabled && !o.RotationChannelEnabled && !o.TranslationEnabled)
|
|
{
|
|
if (hadOneShot) o.OneShotAnimationControlFlags &= ~1L;
|
|
o.OneShotStartMs = -1;
|
|
}
|
|
|
|
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;
|
|
long elapsed = System.Math.Max(0, nowMs - o.RotationStartMs);
|
|
cycleAngle = ((elapsed % o.RotationPeriodMs) * 360) / o.RotationPeriodMs;
|
|
}
|
|
|
|
SurfaceTransitionState? transition = _surfaceTransitions.TryGetValue(o.SourceSlot, out var st)
|
|
? SampleTransition(st, nowMs) : null;
|
|
Affine2D? objectRangeTransform = rangeAffine is { } ra &&
|
|
handle >= _rangeTransformFirst && handle - _rangeTransformFirst < _rangeTransformCount
|
|
? ra : null;
|
|
bool timeVarying =
|
|
o.OneShotColorEnabled || o.ScaleEnabled || o.RotationChannelEnabled ||
|
|
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);
|
|
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,
|
|
o.V18.X, o.V18.Y, o.V18.Z,
|
|
rotation.X, rotation.Y, rotation.Z, rotation.Angle),
|
|
new RotationCycleState(o.RotationEnabled, o.RotationPeriodMs,
|
|
o.RotationAxis.X, o.RotationAxis.Y,
|
|
o.RotationAxis.Z, cycleAngle),
|
|
alpha, tint, strength, blend, multiplyTint, transition,
|
|
colorTransition, objectRangeTransform, timeVarying,
|
|
new ScaleCycleState(o.ScaleCycleEnabled, o.ScaleCyclePeriodMs,
|
|
cycleScaleX, cycleScaleY, cycleScaleZ)));
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
long target = o.OneShotColorTarget & 0xffffffff;
|
|
long start = o.OneShotStartMs;
|
|
long elapsed = nowMs - start - o.ColorDelayMs;
|
|
if (o.ColorDurationMs <= 0 || elapsed >= o.ColorDurationMs)
|
|
{
|
|
long delay = o.ColorDelayMs, duration = o.ColorDurationMs;
|
|
o.Color = target;
|
|
o.ColorDelayMs = 0;
|
|
o.ColorDurationMs = 0;
|
|
o.OneShotColorTarget = -1;
|
|
o.OneShotColorEnabled = false;
|
|
return (target, new ColorTransitionState(current, target, delay, duration, start, 1.0, false));
|
|
}
|
|
if (elapsed <= 0)
|
|
return (current, new ColorTransitionState(current, target, o.ColorDelayMs,
|
|
o.ColorDurationMs, start, 0.0, true));
|
|
|
|
long packed = 0;
|
|
long remaining = o.ColorDurationMs - elapsed;
|
|
for (int shift = 0; shift <= 24; shift += 8)
|
|
{
|
|
long c = (current >> shift) & 0xff;
|
|
long t = (target >> shift) & 0xff;
|
|
packed |= ((c * remaining + t * elapsed) / o.ColorDurationMs & 0xff) << shift;
|
|
}
|
|
return (packed, new ColorTransitionState(current, target, o.ColorDelayMs,
|
|
o.ColorDurationMs, start, elapsed / (double)o.ColorDurationMs, true));
|
|
}
|
|
|
|
private static (double X, double Y, double Z) SampleMatrixChannel(
|
|
ref (double X, double Y, double Z) current,
|
|
(double X, double Y, double Z) target,
|
|
long delayMs, long durationMs, long startMs, ref bool enabled, long nowMs)
|
|
{
|
|
if (!enabled || durationMs <= 0 || startMs < 0) return current;
|
|
long elapsed = nowMs - startMs - delayMs;
|
|
if (elapsed <= 0) return current;
|
|
if (elapsed >= durationMs)
|
|
{
|
|
current = target;
|
|
enabled = false;
|
|
return current;
|
|
}
|
|
double t = (double)elapsed / durationMs;
|
|
return (current.X + (target.X - current.X) * t,
|
|
current.Y + (target.Y - current.Y) * t,
|
|
current.Z + (target.Z - current.Z) * t);
|
|
}
|
|
|
|
private static (double X, double Y, double Z, double Angle) SampleRotationChannel(
|
|
ref (double X, double Y, double Z, double Angle) current,
|
|
(double X, double Y, double Z, double Angle) target,
|
|
long delayMs, long durationMs, long startMs, ref bool enabled, long nowMs)
|
|
{
|
|
if (!enabled || durationMs <= 0 || startMs < 0) return current;
|
|
long elapsed = nowMs - startMs - delayMs;
|
|
if (elapsed <= 0) return current;
|
|
if (elapsed >= durationMs) { current = target; enabled = false; return current; }
|
|
double t = (double)elapsed / durationMs;
|
|
return (current.X + (target.X - current.X) * t,
|
|
current.Y + (target.Y - current.Y) * t,
|
|
current.Z + (target.Z - current.Z) * t,
|
|
current.Angle + (target.Angle - current.Angle) * t);
|
|
}
|
|
|
|
private static long InterpolatePackedColor(long current, long target, double t)
|
|
{
|
|
t = System.Math.Clamp(t, 0.0, 1.0);
|
|
long packed = 0;
|
|
for (int shift = 0; shift <= 24; shift += 8)
|
|
{
|
|
long c = (current >> shift) & 0xff;
|
|
long v = (target >> shift) & 0xff;
|
|
packed |= ((long)(c + (v - c) * t) & 0xff) << shift;
|
|
}
|
|
return packed;
|
|
}
|
|
|
|
/// <summary>Ping-pong interpolation weight in [0,1] toward the target: 0 at cycle start, 1 at half-period.</summary>
|
|
private static double PingPongWeight(long now, long start, long period)
|
|
{
|
|
if (period <= 0) return 0;
|
|
long half = period / 2; if (half <= 0) return 0;
|
|
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)
|
|
=> ((alpha & 0xff) << 24) | (color & 0xffffff);
|
|
}
|