Implement retained graphics lifecycle ops
This commit is contained in:
82
engine/Age.Engine.Tests/GfxLifecycleOpsTests.cs
Normal file
82
engine/Age.Engine.Tests/GfxLifecycleOpsTests.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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) { }
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user