Fix retained background lifecycle
This commit is contained in:
@@ -373,8 +373,7 @@ sealed class GfxTraceHost : IHost
|
||||
private readonly ResourceMap _res;
|
||||
private readonly string _scene;
|
||||
private readonly Dictionary<int, string?> _slotAsset = new(); // slot -> resolved AGF name (or null)
|
||||
// slot -> dims. Slot 0 is the primary/screen surface (800x600), normally created at engine boot which
|
||||
// the single-scene harness skips; seed it so the first CG's anchor math stays correct (not 0x0).
|
||||
// slot -> dimensions of the currently allocated surface. Slot 0 starts as the engine's primary surface.
|
||||
private readonly Dictionary<int, (int W, int H)> _slotDims = new() { { 0, (800, 600) } };
|
||||
public List<string> Events { get; } = new();
|
||||
public GfxTraceHost(ResourceMap res, string scene) { _res = res; _scene = scene; }
|
||||
@@ -408,6 +407,12 @@ sealed class GfxTraceHost : IHost
|
||||
_slotDims[slot] = (width, height);
|
||||
Events.Add($"create-texture slot={slot} {width}x{height}");
|
||||
}
|
||||
public void ReleaseSurface(int slot)
|
||||
{
|
||||
_slotAsset.Remove(slot);
|
||||
_slotDims.Remove(slot);
|
||||
Events.Add($"release-surface slot={slot}");
|
||||
}
|
||||
public void ShowText(int offset, string text) { }
|
||||
public void WaitForInput() { }
|
||||
public void Sleep(long duration) { }
|
||||
|
||||
@@ -20,6 +20,7 @@ public class AnimChannelTests
|
||||
{
|
||||
(0x55, new[]{G(1), I(0xcf3a)}), (0x55, new[]{G(2), I(0)}),
|
||||
(0x55, new[]{G(3), I(360)}), (0x55, new[]{G(4), I(20)}), (0x55, new[]{G(5), I(0)}),
|
||||
(0x1fb, new[]{G(1), I(0), I(0), I(0), I(1), I(1), I(0), I(0)}),
|
||||
(0x22f, new[]{G(1), G(2), G(3), G(4), G(5)}),
|
||||
(0x55, new[]{G(6), I(300)}), (0x55, new[]{G(7), I(40)}),
|
||||
(0x55, new[]{G(8), I(20)}), (0x51, new[]{G(8), I(0), G(8)}),
|
||||
@@ -62,6 +63,7 @@ public class AnimChannelTests
|
||||
(0x55, new[]{G(4), I(360)}), (0x55, new[]{G(5), I(20)}), (0x55, new[]{G(6), I(0)}),
|
||||
(0x55, new[]{G(40), I(20)}), (0x51, new[]{G(40), I(0), G(40)}),
|
||||
(0x55, new[]{G(41), I(60)}), (0x51, new[]{G(41), I(0), G(41)}),
|
||||
(0x1fb, new[]{G(1), I(0), I(0), I(0), I(1), I(1), I(0), I(0)}),
|
||||
(0x22f, new[]{G(1), G(2), G(4), G(5), G(6)}),
|
||||
|
||||
(0x228, new[]{G(10), G(1), G(11), G(12), G(13)}),
|
||||
|
||||
@@ -13,6 +13,7 @@ public class GfxAnimationTests
|
||||
public void ScaleAndTranslationChannels_AreIndependent()
|
||||
{
|
||||
var g = new GfxState();
|
||||
g.BindDraw(0x1000, 1, 0, 0, 1, 1, 0, 0);
|
||||
g.SetScaleChannel(0x1000, delayMs: 7, durationMs: 900, percent: (200, 50, 100));
|
||||
g.SetTranslationChannel(0x1000, delayMs: 9, durationMs: 1200, target: (80, -25, 6));
|
||||
var o = g.TryGet(0x1000)!;
|
||||
@@ -44,6 +45,7 @@ public class GfxAnimationTests
|
||||
public void RotationCycle_DoesNotOverwriteMatrixChannels()
|
||||
{
|
||||
var g = new GfxState();
|
||||
g.BindDraw(0x1000, 1, 0, 0, 1, 1, 0, 0);
|
||||
g.SetScaleChannel(0x1000, 0, 100, (150, 150, 100));
|
||||
g.SetTranslationChannel(0x1000, 0, 100, (10, 20, 0));
|
||||
g.SetRotationCycle(0x1000, periodMs: 30, axis: (0, 0, 5));
|
||||
@@ -55,6 +57,30 @@ public class GfxAnimationTests
|
||||
Assert.True(o.RotationEnabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreBindMatrixChannels_CreateNeutralPlaceholderWithoutLatentMotion()
|
||||
{
|
||||
// SC0000 calls op 0x220 on background handle 0xcb20 before BG001A is bound. Native creates the
|
||||
// object (so op 0x215 returns slot 0) but ignores all three matrix setters while visible bit 0 is clear.
|
||||
var g = new GfxState();
|
||||
g.SetScaleChannel(0xcb20, 0, 500, (200, 200, 100));
|
||||
g.SetRotationChannel(0xcb20, 0, 500, (0, 0, 1), 90);
|
||||
g.SetTranslationChannel(0xcb20, 0, 500, (0, 600, 0));
|
||||
|
||||
var placeholder = g.TryGet(0xcb20)!;
|
||||
Assert.Equal(0, g.QuerySlot(0xcb20));
|
||||
Assert.False(placeholder.Visible);
|
||||
Assert.False(placeholder.ScaleEnabled);
|
||||
Assert.False(placeholder.RotationChannelEnabled);
|
||||
Assert.False(placeholder.TranslationEnabled);
|
||||
Assert.Equal((0.0, 0.0, 0.0), placeholder.TranslationTarget);
|
||||
|
||||
g.BindDraw(0xcb20, 4, 0, 0, 800, 500, 0, -500);
|
||||
g.SetTranslationChannel(0xcb20, 0, 500, (0, 600, 0));
|
||||
Assert.True(placeholder.TranslationEnabled);
|
||||
Assert.Equal((0.0, 600.0, 0.0), placeholder.TranslationTarget);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ActiveVisualPresentation_ExcludesStaticWaits_ButIncludesAmbientChannels()
|
||||
{
|
||||
@@ -159,6 +185,7 @@ public class GfxAnimationTests
|
||||
var scene = ScriptAssembler.Assemble(t, "ANIM", new List<(int, Operand[])>
|
||||
{
|
||||
MovGI(1, 0x1000), MovGI(2, 7), MovGI(3, 9), MovGI(4, 800), MovGI(5, 500), MovGI(6, 0),
|
||||
(0x1fb, new[] { G(1), I(0), I(0), I(0), I(1), I(1), I(0), I(0) }),
|
||||
(0x220, new[] { G(1), G(2), G(3), G(4), G(5), G(6) }),
|
||||
MovGI(4, 200), MovGI(5, 50), MovGI(6, 100),
|
||||
(0x21e, new[] { G(1), G(2), G(3), G(4), G(5), G(6) }),
|
||||
@@ -215,6 +242,23 @@ public class GfxAnimationTests
|
||||
Assert.Equal(1, vm.Gfx.AnimClockGeneration);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CurrentTranslationSetter_ReplacesTheLiveMatrixImmediately()
|
||||
{
|
||||
var t = T();
|
||||
var scene = ScriptAssembler.Assemble(t, "CURRENTTRANSLATION", new List<(int, Operand[])>
|
||||
{
|
||||
MovGI(1, 0xcb20), MovGI(2, 12), MovGI(3, 34), MovGI(4, 5),
|
||||
(0x1ff, new[] { G(1), G(2), G(3), G(4) }),
|
||||
Exit(),
|
||||
}, System.Array.Empty<string>());
|
||||
var vm = new VirtualMachine(scene, t, new RecordingHost());
|
||||
vm.Run();
|
||||
var o = vm.Gfx.TryGet(0xcb20)!;
|
||||
Assert.Equal((12.0, 34.0, 5.0), o.TranslationCurrent);
|
||||
Assert.Equal((12L, 34L, 5L), o.V16c);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResetAnimClock_DispatchClearsOnlyGlobalServiceClock()
|
||||
{
|
||||
|
||||
@@ -39,10 +39,10 @@ public class GfxCommandBufferTests
|
||||
private static (int, Operand[]) Register(int handle) => (0x1a2, new[] { G(handle) });
|
||||
|
||||
[Fact]
|
||||
public void QueryReturnsMinusOneUntilDrawBound_ThenSourceSlot()
|
||||
public void QueryReturnsZeroForCreatedUnboundObject_ThenBoundSourceSlot()
|
||||
{
|
||||
// Op 0x215 returns obj+4 from the retained gfx object. Geometry creates the object but leaves it unbound;
|
||||
// op 0x1a2's descriptor registry is unrelated. Draw-texture binds the source slot returned by the query.
|
||||
// Op 0x215 returns obj+4 from the retained gfx object. Native initialization leaves an unbound object's
|
||||
// field at zero; op 0x1a2 is unrelated. Draw-texture replaces it with the bound source slot.
|
||||
var t = T();
|
||||
var scene = ScriptAssembler.Assemble(t, "GFX", new List<(int, Operand[])>
|
||||
{
|
||||
@@ -55,7 +55,7 @@ public class GfxCommandBufferTests
|
||||
}, System.Array.Empty<string>());
|
||||
var vm = new VirtualMachine(scene, t, new RecordingHost());
|
||||
vm.Run();
|
||||
Assert.Equal(-1, vm.Globals[10]);
|
||||
Assert.Equal(0, vm.Globals[10]);
|
||||
Assert.Equal(6, vm.Globals[11]);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,20 +6,41 @@ public class GfxStateTests
|
||||
[Fact]
|
||||
public void QueryReturnsBoundSourceSlot_NotOperandRegistryValue()
|
||||
{
|
||||
// Native op 0x215 queries the retained-object map and returns obj+4, the source slot set by draw-texture.
|
||||
// Geometry alone creates an object but leaves obj+4 at -1. Op 0x1a2 is a separate descriptor registry.
|
||||
// Native op 0x215 queries the retained-object map and returns obj+4. The default initializer zeroes
|
||||
// that field, while draw-texture replaces it with the bound slot. Op 0x1a2 is a separate registry.
|
||||
var g = new GfxState();
|
||||
g.GetOrCreate(0xcb2a).V18 = (400, 600, 0);
|
||||
Assert.Equal(-1, g.QuerySlot(0xcb2a));
|
||||
Assert.Equal(0, g.QuerySlot(0xcb2a));
|
||||
|
||||
g.Register(0xcb2a);
|
||||
Assert.Equal(-1, g.QuerySlot(0xcb2a));
|
||||
Assert.Equal(0, g.QuerySlot(0xcb2a));
|
||||
|
||||
g.BindDraw(0xcb2a, 6, 0, 0, 200, 200, 10, 20);
|
||||
Assert.Equal(6, g.QuerySlot(0xcb2a));
|
||||
Assert.Equal(-1, g.QuerySlot(0x9999));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnboundMatrixSetterCreatesQueryableObjectForCleanupBeforeReuse()
|
||||
{
|
||||
// Native matrix setters create a neutral placeholder even when they ignore the channel because it is
|
||||
// not draw-bound. obj+4 still defaults to slot 0, so the later query >= 0 cleanup erases the object.
|
||||
var g = new GfxState();
|
||||
g.SetTranslationChannel(0xcb2a, 0, 150, (-100, 0, 0));
|
||||
g.SetRotationChannel(0xcb2a, 0, 150, (0, 0, 1), -90);
|
||||
Assert.Equal(0, g.QuerySlot(0xcb2a));
|
||||
|
||||
if (g.QuerySlot(0xcb2a) >= 0)
|
||||
g.EraseRange(0xcb2a, 10);
|
||||
|
||||
g.SetSurface(5, 0x7a, 0);
|
||||
g.BindDraw(0xcb2a, 5, 0, 0, 800, 600, 0, 0);
|
||||
var reused = Assert.Single(g.SnapshotVisibleObjects(1000));
|
||||
Assert.Equal((0.0, 0.0, 0.0),
|
||||
(reused.Transform.TranslateX, reused.Transform.TranslateY, reused.Transform.RotationAngleDegrees));
|
||||
Assert.Equal(255, reused.Alpha);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VectorsRoundTripPerObject()
|
||||
{
|
||||
|
||||
@@ -81,7 +81,9 @@ public sealed class GfxState
|
||||
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.
|
||||
public int SourceSlot = -1;
|
||||
// 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;
|
||||
|
||||
@@ -441,12 +443,26 @@ public sealed class GfxState
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Op 0x1ff: immediately replace the object's current translation matrix at obj+0x16c.</summary>
|
||||
public void SetCurrentTranslation(long handle, (long X, long Y, long Z) translation)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var o = GetOrCreate(handle);
|
||||
o.V16c = translation;
|
||||
o.TranslationCurrent = translation;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Op 0x21e: normalized scale target (100 = identity), with independent delay/duration.</summary>
|
||||
public void SetScaleChannel(long handle, long delayMs, long durationMs, (long X, long Y, long Z) percent)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var o = GetOrCreate(handle);
|
||||
// Native setters get-or-create first, then require object flag bit 0 (draw-bound/visible).
|
||||
// Pre-bind calls leave a neutral, queryable placeholder and do not queue latent motion.
|
||||
if (!o.Visible) return;
|
||||
o.ScaleDelayMs = delayMs; o.ScaleDurationMs = durationMs;
|
||||
o.ScaleTarget = (percent.X / 100.0, percent.Y / 100.0, percent.Z / 100.0);
|
||||
o.ScaleEnabled = durationMs > 0; o.OneShotStartMs = -1;
|
||||
@@ -459,6 +475,7 @@ public sealed class GfxState
|
||||
lock (_lock)
|
||||
{
|
||||
var o = GetOrCreate(handle);
|
||||
if (!o.Visible) return;
|
||||
o.TranslationDelayMs = delayMs; o.TranslationDurationMs = durationMs;
|
||||
o.TranslationTarget = target;
|
||||
o.TranslationEnabled = durationMs > 0; o.OneShotStartMs = -1;
|
||||
@@ -488,6 +505,7 @@ public sealed class GfxState
|
||||
lock (_lock)
|
||||
{
|
||||
var o = GetOrCreate(handle);
|
||||
if (!o.Visible) return;
|
||||
o.RotationDelayMs = delayMs; o.RotationDurationMs = durationMs;
|
||||
o.RotationTarget = (axis.X, axis.Y, axis.Z, angleDegrees);
|
||||
o.RotationChannelEnabled = durationMs > 0; o.OneShotStartMs = -1;
|
||||
|
||||
@@ -411,8 +411,8 @@ public sealed class VirtualMachine
|
||||
}
|
||||
case "u00422930": // 0x23f query-object: (out)(handle) <- 0 if the object exists, else -1
|
||||
Write(a[0], Gfx.TryGet(Read(a[1])) != null ? 0 : -1); return pc + 1;
|
||||
case "set-gfx-geom3-c": // 0x1ff (handle)(a)(b)(c) -> V16c
|
||||
Gfx.GetOrCreate(Read(a[0])).V16c = (Read(a[1]), Read(a[2]), Read(a[3])); return pc + 1;
|
||||
case "set-gfx-geom3-c": // 0x1ff: set current translation matrix
|
||||
Gfx.SetCurrentTranslation(Read(a[0]), (Read(a[1]), Read(a[2]), Read(a[3]))); return pc + 1;
|
||||
case "u00420620": // upstream ABI label
|
||||
case "gfx-set-scale-current": // 0x1fd (handle)(sx%)(sy%)(sz%) -> current scale matrix
|
||||
Gfx.SetCurrentScale(Read(a[0]), (Read(a[1]), Read(a[2]), Read(a[3]))); return pc + 1;
|
||||
|
||||
Reference in New Issue
Block a user