Implement ADV foreground transition lifecycle

This commit is contained in:
gamer147
2026-07-10 18:49:53 -04:00
parent 768863b48e
commit d6d1be984e
16 changed files with 543 additions and 54 deletions

View File

@@ -1,3 +1,5 @@
using Age.Engine.Model;
namespace Age.Engine.Hosting;
public interface IHost
{
@@ -5,6 +7,14 @@ public interface IHost
void WaitForInput();
void Sleep(long duration);
void FrameYield();
// Native 0x1c7/0x1cc query two distinct ADV skip channels. Headless and non-interactive
// hosts default to normal playback; the Godot host supplies the live interactive values.
bool IsMessageSkipActive => false;
bool IsAdvReadSkipActive => false;
// Normal playback reaches op 0x21c and parks until a queued 0x223 transition completes. The
// read/message-skip branch reaches op 0x20c and presents the completed endpoint immediately.
void WaitForForegroundTransition(GfxState gfx) { }
void PresentFrame(GfxState gfx) { }
void CreateTexture(int slot, int width, int height);
void SetTexture(long resourceId, int slot);
void DrawTexture(int slot, int srcX, int srcY, int width, int height, int dstX, int dstY);

View File

@@ -14,6 +14,12 @@ public readonly record struct RotationCycleState(bool Enabled, long PeriodMs,
double AxisX, double AxisY, double AxisZ,
double AngleDegrees = 0);
/// <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);
/// <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,
@@ -25,7 +31,8 @@ public readonly record struct RotationCycleState(bool Enabled, long PeriodMs,
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);
int Alpha, long Tint, int TintStrength, BlendKind Blend,
SurfaceTransitionState? SurfaceTransition = null);
/// <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
@@ -35,12 +42,20 @@ public readonly record struct RenderObject(long Handle, long SurfaceResId, long
/// 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;
}
public sealed class GfxObject
{
public (long X, long Y, long Z) V18, V24, V16c;
public long Field64, Field68, Field6c;
public long Color;
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
// ---- src-rect / spritesheet-cell channel (ops 0x239 static cell, 0x231 animate). Interpolator
// SRC-RECT SCROLL channel: period obj+0x230, start obj+0x21c, grid obj+0x238/0x23c. ----
@@ -115,6 +130,37 @@ public sealed class GfxState
}
}
/// <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;
_objects[destinationHandle] = new GfxObject
{
V18 = s.V18, V24 = s.V24, V16c = s.V16c,
Field64 = s.Field64, Field68 = s.Field68, Field6c = s.Field6c,
Color = s.Color, HasColor = s.HasColor, StaticColorMode = s.StaticColorMode,
SrcGridW = s.SrcGridW, SrcGridH = s.SrcGridH, 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, MatrixStartMs = s.MatrixStartMs,
RotationPeriodMs = s.RotationPeriodMs, RotationAxis = s.RotationAxis,
RotationEnabled = s.RotationEnabled, RotationStartMs = s.RotationStartMs,
};
CurrentObject = destinationHandle;
return true;
}
}
/// <summary>Op 0x1a2: retain the operand's current value in the separate descriptor registry. This does
/// not populate the retained-object map used by op 0x215.</summary>
public void Register(long handle) { lock (_lock) { _operandRegistry.Add(handle); } }
@@ -158,6 +204,7 @@ public sealed class GfxState
// ---- surfaces (image buffers per slot): ctx+0x52bd4[slot], from create/set-texture ----
private readonly Dictionary<int, (long ResId, long ColorKey)> _surfaces = new();
private readonly Dictionary<int, SurfaceTransition> _surfaceTransitions = new();
public void SetSurface(int slot, long resId, long colorKey) { lock (_lock) { _surfaces[slot] = (resId, colorKey); } }
/// <summary>Ops 0x202/0x203: record a packed 0xAARRGGBB color/alpha modulation on the object and mark it
@@ -167,6 +214,27 @@ public sealed class GfxState
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;
}
}
public void SetStaticObjectColorResolved(long handle, long mode, long alpha, long rgb)
{
SetObjectColorResolved(handle, alpha, rgb);
lock (_lock) GetOrCreate(handle).StaticColorMode = mode;
}
/// <summary>Ops 0x239 (static cell, period=0) / 0x231 (animate, period&gt;0): set the spritesheet grid +
/// the visible cell. When animated, the interpolator ping-pongs the cell across the grid over the period.</summary>
public void SetSrcRect(long handle, long gridW, long gridH, long cell, long period)
@@ -191,6 +259,73 @@ public sealed class GfxState
}
public void ClearSurface(int slot) { lock (_lock) { _surfaces[slot] = (0, 0); } } // create-texture (blank)
/// <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),
};
}
}
/// <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);
}
/// <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++; }
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)
@@ -289,7 +424,17 @@ public sealed class GfxState
if (o.HasColor)
{
var (a, r, g, b) = BlendMath.UnpackArgb(o.Color);
strength = a; tint = ((long)r << 16) | ((long)g << 8) | (long)b; blend = BlendKind.Alpha;
tint = ((long)r << 16) | ((long)g << 8) | (long)b;
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
{
strength = a; blend = BlendKind.Alpha;
}
}
if (o.ColorAnim)
{
@@ -342,6 +487,8 @@ public sealed class GfxState
cycleAngle = ((elapsed % o.RotationPeriodMs) * 360) / o.RotationPeriodMs;
}
SurfaceTransitionState? transition = _surfaceTransitions.TryGetValue(o.SourceSlot, out var st)
? SampleTransition(st, nowMs) : null;
list.Add(new RenderObject(kv.Key, resId, ck, srcX, srcY, w, h,
(int)o.V24.X, (int)o.V24.Y,
new TransformState(scale.X, scale.Y, scale.Z,
@@ -351,7 +498,7 @@ public sealed class GfxState
new RotationCycleState(o.RotationEnabled, o.RotationPeriodMs,
o.RotationAxis.X, o.RotationAxis.Y,
o.RotationAxis.Z, cycleAngle),
alpha, tint, strength, blend));
alpha, tint, strength, blend, transition));
}
return list;
}

View File

@@ -277,6 +277,11 @@ public sealed class VirtualMachine
_host.WaitForInput(); return pc + 1;
case "sleep": // 0xc8 (duration) — pause the host duration ms; headless hosts no-op (parity). Frame pacing.
_host.Sleep(Read(a[0])); return pc + 1;
case "get-message-skip": // 0x1c7: Ctrl/message fast-forward run-state bit
Write(a[0], _host.IsMessageSkipActive ? 1 : 0); return pc + 1;
case "get-adv-read-skip-state": // 0x1cc: per-message read/click skip service state
case "get-adv-service-state": // compatibility with pre-recovery generated tables
Write(a[0], _host.IsAdvReadSkipActive ? 1 : 0); return pc + 1;
case "end-text-line": case "set-font":
case "comment": case "display-furigana": case "dev_ukn":
return pc + 1;
@@ -362,10 +367,12 @@ public sealed class VirtualMachine
Gfx.EraseRange(Read(a[0]), Read(a[1])); return pc + 1;
case "gfx-elem-release": // 0x1fa (surface slot)
Gfx.ClearSurface((int)Read(a[0])); return pc + 1;
case "clone-gfx-object": // 0x21d (source handle)(destination handle)
Gfx.CloneObject(Read(a[0]), Read(a[1])); return pc + 1;
case "gfx-blit-color": // 0x202 (handle)(x)(y)(alpha)(color) — static alpha/tint (anim interp deferred)
Gfx.SetObjectColor(Read(a[0]), GfxState.PackColor(Read(a[3]), Read(a[4]))); return pc + 1;
Gfx.SetObjectColorResolved(Read(a[0]), Read(a[3]), Read(a[4])); return pc + 1;
case "gfx-draw-color": // 0x203 (handle)(v)(alpha)(color) — static alpha/tint
Gfx.SetObjectColor(Read(a[0]), GfxState.PackColor(Read(a[2]), Read(a[3]))); return pc + 1;
Gfx.SetStaticObjectColorResolved(Read(a[0]), Read(a[1]), Read(a[2]), Read(a[3])); return pc + 1;
// ---- sprite transform / animation cluster (docs/engine-re.md "0x21c-0x243 ... ANIMATION") ----
case "set-anim-transform-abs": // 0x220 (handle)(delay)(duration)(tx)(ty)(tz)
Gfx.SetTranslationChannel(Read(a[0]), Read(a[1]), Read(a[2]),
@@ -382,7 +389,13 @@ public sealed class VirtualMachine
Gfx.SetAnimClock(Read(a[0])); return pc + 1;
case "reset-anim-clock": // 0x243: reset the separate global animation-service clock
Gfx.ResetAnimClock(); return pc + 1;
case "mark-frame-yield": // 0x21c: host already yields after every completed opcode
case "queue-surface-alpha-transition": // 0x223: target surface crossfade over two object ranges
Gfx.QueueSurfaceAlphaTransition(Read(a[0]), (int)Read(a[1]), Read(a[2]), (int)Read(a[3]),
Read(a[4]), (int)Read(a[5]), Read(a[6]), Read(a[7])); return pc + 1;
case "present-frame": // 0x20c: read/message-skip path snaps a queued transition to its endpoint
_host.PresentFrame(Gfx); return pc + 1;
case "mark-frame-yield": // 0x21c: normal foreground-transition scheduler/resume boundary
_host.WaitForForegroundTransition(Gfx); return pc + 1;
case "clear-gfx-command-queue": // 0x224: retained compositor does not use this native queue
return pc + 1;
default: