Split GfxState retained objects

This commit is contained in:
gamer147
2026-08-02 21:41:56 -04:00
parent 4498a1a974
commit 8c24db9212
4 changed files with 389 additions and 372 deletions

View File

@@ -164,7 +164,9 @@ of that state through the sealed partial class.
`engine/Age.Engine/Model/GfxState.Contracts.cs` owns its public render, transition, diagnostic, persistence,
animation, numeric-glyph, and handle-range contracts. `engine/Age.Engine/Model/GfxState.Surfaces.cs` owns surface
resource/color-key state, created/reloadable classification, movie stop-time metadata, render-target/tile
configuration, and surface lifecycle operations.
configuration, and surface lifecycle operations. `engine/Age.Engine/Model/GfxState.RetainedObjects.cs` owns the
retained-object record and registry/index, range-transform state, object creation/query/clone/erase and draw
binding, plus numeric-glyph object generation.
The disposable `build/page-map-<SCENE>.jsonl` files are produced by editor/development Godot runs and map
runtime ADV page ordinals to their authoritative script offsets for `tools/locate_page.py`. Packaged exports

View File

@@ -607,6 +607,11 @@ do not mix mechanical moves with semantic changes.
`engine/Age.Engine/Model/GfxState.Surfaces.cs`. Reset, persistence, retained-object sampling, and transition
teardown retain direct access through the partial class; runtime validation remains green.
The third bounded `GfxState` split moved the retained-object record and registry/index, range-transform
state and operations, object creation/query/clone/erase and draw binding, and numeric-glyph object generation
into `engine/Age.Engine/Model/GfxState.RetainedObjects.cs`. Cross-domain reset/persistence, animation, and
presentation sampling retain direct access through the sealed partial class; runtime validation remains green.
**Gate:** no externally visible behavior or command changes; generated artifacts are byte-identical where
deterministic, and the corresponding engine, Python, Godot, and corpus validations remain green after
each domain move.
@@ -978,7 +983,7 @@ layer's rendering diverges from ADV; save layout.
## 8. Immediate next step
Continue step 2 of the **codebase consolidation** maintenance slice: behavior-neutral physical splits backed
by the tracked launcher and layered validation driver. With the planned `Main` and `GodotAdvHost` domains
isolated and the first two `GfxState` domains separated, move retained-object ownership next, preserving public
types, commands, and generated output.
isolated and the first three `GfxState` domains separated, move animation ownership next, preserving public types,
commands, and generated output.
Concrete playthrough blockers may still preempt this bounded maintenance work; the consolidation effort does
not replace Phase B gameplay validation or the open cross-platform gates.

View File

@@ -0,0 +1,379 @@
namespace Age.Engine.Model;
public sealed partial class GfxState
{
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>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 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();
}
}
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,
};
/// <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;
}
}
private void InsertOrderedHandle(long handle)
{
int index = _orderedObjectHandles.BinarySearch(handle);
if (index < 0) _orderedObjectHandles.Insert(~index, handle);
}
}

View File

@@ -24,97 +24,7 @@ public sealed partial class GfxState
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; }
// ---- 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. ----
@@ -130,171 +40,6 @@ public sealed partial class GfxState
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 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
@@ -384,34 +129,6 @@ public sealed partial class GfxState
}
}
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();
@@ -801,87 +518,6 @@ public sealed partial class GfxState
=> 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>
@@ -1335,11 +971,6 @@ public sealed partial class GfxState
}
}
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)
{