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, `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 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 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 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 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 `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. 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 **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 deterministic, and the corresponding engine, Python, Godot, and corpus validations remain green after
each domain move. each domain move.
@@ -978,7 +983,7 @@ layer's rendering diverges from ADV; save layout.
## 8. Immediate next step ## 8. Immediate next step
Continue step 2 of the **codebase consolidation** maintenance slice: behavior-neutral physical splits backed 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 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 isolated and the first three `GfxState` domains separated, move animation ownership next, preserving public types,
types, commands, and generated output. commands, and generated output.
Concrete playthrough blockers may still preempt this bounded maintenance work; the consolidation effort does 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. 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 required MovieMaskTransitionRequest Request;
public bool Completed; 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). // ---- 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. ---- // 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++; 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 /// <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 /// 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(); 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, => new(t.CommandKey, t.TargetSlot, t.RangeAStart, t.RangeACount, t.RangeBStart, t.RangeBCount,
t.DelayMs, t.DurationMs, t.StartMs, TransitionProgress(t, nowMs), t.Forced); 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 /// <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> /// 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) private static (long Packed, ColorTransitionState State) SampleOneShotColor(GfxObject o, long nowMs)
{ {