diff --git a/docs/PROJECT-STRUCTURE.md b/docs/PROJECT-STRUCTURE.md
index a36e21d..477cf84 100644
--- a/docs/PROJECT-STRUCTURE.md
+++ b/docs/PROJECT-STRUCTURE.md
@@ -107,6 +107,8 @@ S:\Game Hacking\Eushully\Himegari\ ← workspace root (three siblings)
│ └── manifest.json, opcode-coverage.md (opcode-coverage.md GENERATED from opcodes.toml)
│
├── engine/ DELIVERABLE — the .NET VM core (AgeEngine.sln: Age.Engine / Age.Cli / tests)
+ │ ├── Age.Engine/Model/ retained graphics state plus separately navigable public
+ │ render, transition, diagnostic, and persistence contracts
│ ├── Age.Engine/Sys4/ runtime game-root selection, catalog parser, loose-first bounded
│ ALF asset store, script provider, AGF/LZSS and Windows CUR decoders,
│ and resource facade
diff --git a/docs/remake-architecture-and-roadmap.md b/docs/remake-architecture-and-roadmap.md
index d09ab85..c81a826 100644
--- a/docs/remake-architecture-and-roadmap.md
+++ b/docs/remake-architecture-and-roadmap.md
@@ -596,6 +596,11 @@ do not mix mechanical moves with semantic changes.
scene reset, and frame-pulse consumers retain direct access through the sealed partial class. The planned
host decomposition is complete; runtime validation remains green.
+ The first bounded `GfxState` split moved its public render, transition, diagnostic, persistence, animation,
+ numeric-glyph, and handle-range value contracts into `engine/Age.Engine/Model/GfxState.Contracts.cs`.
+ `GfxState` remains the same sealed runtime type, and every moved declaration is textually unchanged; 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.
@@ -967,7 +972,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, begin the `GfxState` decomposition with one existing ownership domain at a time, preserving public
-types, commands, and generated output.
+isolated and the `GfxState` public contract layer separated, move `GfxState` surface 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.
diff --git a/engine/Age.Engine/Model/GfxState.Contracts.cs b/engine/Age.Engine/Model/GfxState.Contracts.cs
new file mode 100644
index 0000000..7145f08
--- /dev/null
+++ b/engine/Age.Engine/Model/GfxState.Contracts.cs
@@ -0,0 +1,101 @@
+using Age.Engine.Hosting;
+
+namespace Age.Engine.Model;
+
+/// 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.
+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,
+}
+
+/// Sampled op-0x223 type-0 surface transition. Range A is already present in normal z-order;
+/// the compositor draws range B over it with to form the native crossfade.
+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);
+
+/// One synchronized sample of op 0x202's native one-shot packed-color channel.
+public readonly record struct ColorTransitionState(long Current, long Target,
+ long DelayMs, long DurationMs, long StartMs, double Progress, bool Active);
+
+/// One EngineCtx numeric-glyph style registered by opcode 0x13a.
+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 BlockingObjects);
+
+public readonly record struct GfxSurfacePersistenceState(
+ int Slot, long ResourceId, long ColorKey, bool Created, bool ReloadOnRestore);
+
+public sealed record GfxPersistenceSnapshot(
+ IReadOnlyList Surfaces,
+ IReadOnlyList<(long Handle, GfxState.GfxObject Object)> Objects,
+ long RangeFirst,
+ long RangeCount,
+ GfxState.GfxObject RangeTransform);
+
+/// 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 ; 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").
+/// The packed-color channel is mode-dependent. Textured mode 0 ignores packed alpha and
+/// multiplicatively modulates by ; surfaceless mode 0 uses .
+/// A mode-0 color which has passed through op 0x202, and mode 1, use as opacity.
+/// selects the latter compositor path.
+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);
+
+/// The retained handle interval selected by an op-0x222 backbuffer publication.
+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;
+}
diff --git a/engine/Age.Engine/Model/GfxState.cs b/engine/Age.Engine/Model/GfxState.cs
index 45774b9..c7a132d 100644
--- a/engine/Age.Engine/Model/GfxState.cs
+++ b/engine/Age.Engine/Model/GfxState.cs
@@ -3,103 +3,6 @@ using Age.Engine.Hosting;
namespace Age.Engine.Model;
-/// 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.
-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,
-}
-
-/// Sampled op-0x223 type-0 surface transition. Range A is already present in normal z-order;
-/// the compositor draws range B over it with to form the native crossfade.
-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);
-
-/// One synchronized sample of op 0x202's native one-shot packed-color channel.
-public readonly record struct ColorTransitionState(long Current, long Target,
- long DelayMs, long DurationMs, long StartMs, double Progress, bool Active);
-
-/// One EngineCtx numeric-glyph style registered by opcode 0x13a.
-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 BlockingObjects);
-
-public readonly record struct GfxSurfacePersistenceState(
- int Slot, long ResourceId, long ColorKey, bool Created, bool ReloadOnRestore);
-
-public sealed record GfxPersistenceSnapshot(
- IReadOnlyList Surfaces,
- IReadOnlyList<(long Handle, GfxState.GfxObject Object)> Objects,
- long RangeFirst,
- long RangeCount,
- GfxState.GfxObject RangeTransform);
-
-/// 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 ; 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").
-/// The packed-color channel is mode-dependent. Textured mode 0 ignores packed alpha and
-/// multiplicatively modulates by ; surfaceless mode 0 uses .
-/// A mode-0 color which has passed through op 0x202, and mode 1, use as opacity.
-/// selects the latter compositor path.
-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);
-
-/// The retained handle interval selected by an op-0x222 backbuffer publication.
-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;
-}
/// 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