Implement retained graphics lifecycle ops

This commit is contained in:
gamer147
2026-07-20 14:29:41 -04:00
parent 458b65aee6
commit d740c27b4a
12 changed files with 423 additions and 102 deletions

View File

@@ -0,0 +1,82 @@
using System.Collections.Generic;
using Age.Engine.Model;
using Age.Engine.Sys4;
using Age.Engine.Vm;
using Xunit;
public class GfxLifecycleOpsTests
{
private static OpcodeTable T() => OpcodeTableJson.Load(Paths.OpcodesJson);
private static Operand G(int address) => new(3, address);
private static Operand I(long value) => new(0, value);
private static (int, Operand[]) Mov(int address, long value) => (0x55, new[] { G(address), I(value) });
private static (int, Operand[]) Exit() => (0x2, System.Array.Empty<Operand>());
[Fact]
public void ClearRetainedObjectsPreservesSurfaceResourcesForLaterRebind()
{
var table = T();
var scene = ScriptAssembler.Assemble(table, "GFX-LIFECYCLE", new List<(int, Operand[])>
{
Mov(1, 0x40), Mov(2, 7), Mov(3, 0x33),
(0x1f9, new[] { G(3), G(2), I(0) }),
(0x1fb, new[] { G(1), G(2), I(0), I(0), I(16), I(16), I(0), I(0) }),
(0x1f6, System.Array.Empty<Operand>()),
(0x1fb, new[] { G(1), G(2), I(0), I(0), I(16), I(16), I(0), I(0) }),
Exit(),
}, System.Array.Empty<string>());
var vm = new VirtualMachine(scene, table, new RecordingHost());
vm.Run();
var visible = Assert.Single(vm.Gfx.SnapshotVisibleObjects());
Assert.Equal(0x40, visible.Handle);
Assert.Equal(0x33, visible.SurfaceResId);
}
[Fact]
public void RenderTargetSelectionAndClearReachTheHostIncludingBackbufferRestore()
{
var table = T();
var host = new RecordingHost();
var scene = ScriptAssembler.Assemble(table, "GFX-TARGET", new List<(int, Operand[])>
{
(0x20d, new[] { I(7) }),
(0x20e, System.Array.Empty<Operand>()),
(0x20d, new[] { I(1000) }),
(0x20e, System.Array.Empty<Operand>()),
Exit(),
}, System.Array.Empty<string>());
var vm = new VirtualMachine(scene, table, host);
vm.Run();
Assert.Equal(new[] { 7, -1 }, host.ClearedRenderTargets);
Assert.Equal(-1, vm.Gfx.CurrentRenderTargetSlot);
}
[Fact]
public void BulkReleaseDropsOnlyTransientSurfaceRange()
{
var table = T();
var host = new RecordingHost();
var scene = ScriptAssembler.Assemble(table, "GFX-RELEASE", new List<(int, Operand[])>
{
Mov(1, 1), Mov(2, 42), Mov(3, 0x11), Mov(4, 0x22),
(0x1f9, new[] { G(3), G(1), I(0) }),
(0x1f9, new[] { G(4), G(2), I(0) }),
(0x1fb, new[] { I(0x100), G(1), I(0), I(0), I(16), I(16), I(0), I(0) }),
(0x1fb, new[] { I(0x101), G(2), I(0), I(0), I(16), I(16), I(0), I(0) }),
(0x23d, System.Array.Empty<Operand>()),
Exit(),
}, System.Array.Empty<string>());
var vm = new VirtualMachine(scene, table, host);
vm.Run();
var visible = vm.Gfx.SnapshotVisibleObjects();
Assert.Equal(0x11, Assert.Single(visible, item => item.Handle == 0x100).SurfaceResId);
Assert.Equal(0, Assert.Single(visible, item => item.Handle == 0x101).SurfaceResId);
Assert.Equal((42, 958), Assert.Single(host.ReleasedSurfaceRanges));
}
}

View File

@@ -36,6 +36,8 @@ internal class RecordingHost : IHost
public readonly List<int> SfxReleases = new();
public readonly List<(int Target, long Duration)> BgmFades = new();
public readonly List<(long Resource, int Surface, long Flags, long SyncMask)> Movies = new();
public readonly List<int> ClearedRenderTargets = new();
public readonly List<(int First, int Count)> ReleasedSurfaceRanges = new();
public readonly List<bool> MessageSkipChanges = new();
public readonly List<long> CursorResources = new();
public readonly List<bool> AdvPagePresentationSuspended = new();
@@ -106,6 +108,8 @@ internal class RecordingHost : IHost
}
public void CreateTexture(int slot, int w, int h) { }
public void SetTexture(long resId, int slot) { }
public void ClearRenderTarget(int surfaceSlot) => ClearedRenderTargets.Add(surfaceSlot);
public void ReleaseSurfaceRange(int firstSlot, int count) => ReleasedSurfaceRanges.Add((firstSlot, count));
public void DrawTexture(int slot, int sx, int sy, int w, int h, int dx, int dy) { }
public (int Width, int Height) GetTextureSize(int slot) => (0, 0);
public void PlayBgm(long id) { }

View File

@@ -72,6 +72,12 @@ public interface IHost
void CreateTexture(int slot, int width, int height);
void SetTexture(long resourceId, int slot);
void ReleaseSurface(int slot) { }
/// <summary>Clear the selected target's pixels; -1 denotes the main backbuffer.</summary>
void ClearRenderTarget(int surfaceSlot) { }
void ReleaseSurfaceRange(int firstSlot, int count)
{
for (int slot = firstSlot; slot < firstSlot + count; slot++) ReleaseSurface(slot);
}
void DrawTexture(int slot, int srcX, int srcY, int width, int height, int dstX, int dstY);
(int Width, int Height) GetTextureSize(int slot);
void PlayBgm(long id);

View File

@@ -42,7 +42,7 @@ public readonly record struct RenderObject(long Handle, long SurfaceResId, long
/// <summary>Host-agnostic model of the AGE native gfx command-buffer (reversed in
/// docs/engine-re.md, gfx op-contract table). One registry maps an object handle to a GfxObject — the
/// native ctx+0x408 map that op 0x215 queries and the geometry get/set ops share. Each object carries a
/// native retained-gfx owner+0x408 map (EngineCtx+0x46a1c) that op 0x215 queries and the geometry get/set ops share. Each object carries a
/// slot (returned by 0x215) and three 3-vectors: V18 (set 0x217 / get 0x218, anchor), V24 (set 0x219 /
/// get 0x21a, position), V16c (set 0x1ff). The native DirectDraw workers are NOT modelled — only the data
/// the query ops read back, which is all the bytecode geometry math needs.</summary>
@@ -100,7 +100,8 @@ public sealed class GfxState
public (double X, double Y, double Z, double Angle) RotationTarget;
public long RotationDelayMs, RotationDurationMs;
public bool RotationChannelEnabled;
// Shared matrix-channel start timestamp obj+0x34, seeded from frame-time ctx+0xb550.
// 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).
@@ -117,6 +118,8 @@ public sealed class GfxState
private readonly Dictionary<long, long> _fieldTable = new(); // ctx+0x46d14 (0x216); no family writer -> default 0
public long CurrentObject { get; private set; }
/// <summary>The D3D render target selected by op 0x20d. -1 denotes the main backbuffer.</summary>
public int CurrentRenderTargetSlot { get; private set; } = -1;
// ---- 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. ----
@@ -219,6 +222,16 @@ public sealed class GfxState
}
}
/// <summary>Op 0x1f6: clear every retained gfx-object record without releasing surface resources.</summary>
public void ClearRetainedObjects()
{
lock (_lock)
{
_objects.Clear();
CurrentObject = 0;
}
}
private readonly object _lock = new();
// ---- surfaces (image buffers per slot): ctx+0x52bd4[slot], from create/set-texture ----
@@ -226,6 +239,29 @@ public sealed class GfxState
private readonly Dictionary<int, SurfaceTransition> _surfaceTransitions = new();
public void SetSurface(int slot, long resId, long colorKey) { lock (_lock) { _surfaces[slot] = (resId, colorKey); } }
/// <summary>Op 0x20d: select a surface as the D3D render target; values at or above 1000 restore the
/// device backbuffer in the native engine.</summary>
public void SelectRenderTarget(long slot)
{
lock (_lock) CurrentRenderTargetSlot = slot is >= 0 and < 1000 ? (int)slot : -1;
}
/// <summary>Op 0x23d: release the transient surface range while retaining system-owned low slots.</summary>
public void ReleaseSurfaceRange(int firstSlot, int count)
{
lock (_lock)
{
int end = checked(firstSlot + count);
for (int slot = firstSlot; slot < end; slot++)
{
_surfaces.Remove(slot);
_surfaceTransitions.Remove(slot);
}
if (CurrentRenderTargetSlot >= firstSlot && CurrentRenderTargetSlot < end)
CurrentRenderTargetSlot = -1;
}
}
/// <summary>Ops 0x202/0x203: record a packed 0xAARRGGBB color/alpha modulation on the object and mark it
/// HasColor so the compositor applies alpha+tint (vs the opaque default).</summary>
public void SetObjectColor(long handle, long packed)
@@ -502,7 +538,7 @@ public sealed class GfxState
}
/// <summary>Op 0x234: retain the cyclic rotation period and axis separately. Native interpolation uses
/// frame-time ctx+0xb550 and rotates through 360 degrees per period; affine rendering is deferred.</summary>
/// frame-time retained-gfx owner+0xb550 (EngineCtx+0x51b64) and rotates through 360 degrees per period.</summary>
public void SetRotationCycle(long handle, long periodMs, (long X, long Y, long Z) axis)
{
lock (_lock)

View File

@@ -962,6 +962,15 @@ public sealed class VirtualMachine
(int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]), (int)Read(a[3]), (int)Read(a[4]),
(int)System.Math.Min(Read(a[5]), 255), Read(a[6]) & 0x00ff_ffff));
return pc + 1;
case "clear-retained-gfx-objects": // 0x1f6: erase object records, but preserve surfaces
Gfx.ClearRetainedObjects(); return pc + 1;
case "select-render-target": // 0x20d: slot <1000 selects a surface; >=1000 restores backbuffer
Gfx.SelectRenderTarget(Read(a[0])); return pc + 1;
case "clear-render-target": // 0x20e: clear color to black and depth to one
_host.ClearRenderTarget(Gfx.CurrentRenderTargetSlot); return pc + 1;
case "release-transient-surfaces": // 0x23d: native fixed range [42,1000)
Gfx.ReleaseSurfaceRange(42, 1000 - 42);
_host.ReleaseSurfaceRange(42, 1000 - 42); return pc + 1;
case "play-bgm": _host.PlayBgm(Read(a[0])); return pc + 1;
case "play-voice":
_autoVoicePending = true;