Fix skip completion for visual transitions

This commit is contained in:
gamer147
2026-07-29 17:09:37 -04:00
parent 3378abdeca
commit 09d765d89d
12 changed files with 271 additions and 50 deletions

View File

@@ -92,4 +92,26 @@ public class CoreScalarAndScreenTransitionOpsTests
Assert.Equal(480, LegacyScreenTransitionTiming.DurationMilliseconds(30));
Assert.Equal(1024, LegacyScreenTransitionTiming.DurationMilliseconds(65));
}
[Fact]
public void ActiveAdvSkip_ForcesLegacyScreenTransitionEndpointsAtDispatch()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
var script = ScriptAssembler.Assemble(table, "SKIPPED_SCREEN_TRANSITIONS",
new List<(int, Operand[])>
{
(0x21, new[] { new Operand(Imm, 1), new Operand(Imm, 30) }),
(0x25, new[]
{
new Operand(Imm, 1), new Operand(Imm, 2), new Operand(Imm, 10),
}),
(0x2, Array.Empty<Operand>()),
}, Array.Empty<string>());
var host = new RecordingHost { MessageSkip = true };
new VirtualMachine(script, table, host).Run();
Assert.Equal(new[] { true }, host.SurfaceBlackFadeForceEndpoints);
Assert.Equal(new[] { true }, host.SurfaceCrossfadeForceEndpoints);
}
}

View File

@@ -93,4 +93,47 @@ public class ForegroundTransitionTests
Assert.False(gfx.HasActiveForegroundTransitions(100));
Assert.True(gfx.SnapshotMovieMaskTransitions().Single().Completed);
}
[Fact]
public void ClickCompletionFinishesSurfaceAndOrdinaryOneShotsButPreservesExcludedChannels()
{
var gfx = new GfxState();
gfx.SetSurface(4, 0x25, -1);
gfx.BindDraw(100, 4, 0, 0, 800, 600, 0, 0);
gfx.BindDraw(101, 4, 0, 0, 800, 600, 0, 0);
gfx.SetAnimatedObjectColorResolved(100, 0, 1000, 0, 0xffffff);
gfx.SetScaleChannel(100, 0, 1000, (200, 200, 100));
gfx.SetAnimatedObjectColorResolved(101, 0, 1000, 0, 0xffffff);
gfx.SetOneShotAnimationControl(101, 1);
gfx.SetRotationCycle(100, 1000, (0, 0, 1));
gfx.QueueSurfaceAlphaTransition(102, 6, 101, 1, 100, 1, 0, 1000);
gfx.StartForegroundTransitions(100);
gfx.QueueMovieMaskTransition(new MovieMaskTransitionRequest(
11, 45, 10, 1, -184, 0, 800, 600, 0, 0x325e, 0, 1000));
Assert.Equal(3, gfx.CompleteClickSkippableTimedPresentation(200));
Assert.Equal(1.0, gfx.SnapshotForegroundTransitions(200).Single().Progress);
Assert.False(gfx.TryGet(100)!.OneShotColorEnabled);
Assert.False(gfx.TryGet(100)!.ScaleEnabled);
Assert.True(gfx.TryGet(100)!.RotationEnabled);
Assert.True(gfx.TryGet(101)!.OneShotColorEnabled);
Assert.False(gfx.SnapshotMovieMaskTransitions().Single().Completed);
}
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
public void AnimationServiceFlagsSuppressClickCompletion(long flags)
{
var gfx = new GfxState();
gfx.SetSurface(4, 0x25, -1);
gfx.BindDraw(100, 4, 0, 0, 800, 600, 0, 0);
gfx.SetAnimatedObjectColorResolved(100, 0, 1000, 0, 0xffffff);
gfx.SetAnimationServiceFlags(flags);
Assert.Equal(0, gfx.CompleteClickSkippableTimedPresentation(100));
Assert.True(gfx.TryGet(100)!.OneShotColorEnabled);
}
}

View File

@@ -60,6 +60,8 @@ internal class RecordingHost : IHost
public readonly List<(int First, int Count)> ReleasedSurfaceRanges = new();
public readonly List<(int Surface, long Interval, SurfaceBlackFadeDirection Direction)> SurfaceBlackFades = new();
public readonly List<(int Source, int Target, long Interval)> SurfaceCrossfades = new();
public readonly List<bool> SurfaceBlackFadeForceEndpoints = new();
public readonly List<bool> SurfaceCrossfadeForceEndpoints = new();
public readonly List<(long Resource, int Slot)> Textures = new();
public readonly List<bool> MessageSkipChanges = new();
public readonly List<bool> PhysicalMessageSkipChanges = new();
@@ -175,10 +177,19 @@ internal class RecordingHost : IHost
gfx.CompleteForegroundTransitions(100);
}
public void FadeSurfaceWithBlack(
GfxState gfx, int surface, long intervalArgument, SurfaceBlackFadeDirection direction)
=> SurfaceBlackFades.Add((surface, intervalArgument, direction));
public void CrossfadeSurfaces(GfxState gfx, int sourceSurface, int targetSurface, long intervalArgument)
=> SurfaceCrossfades.Add((sourceSurface, targetSurface, intervalArgument));
GfxState gfx, int surface, long intervalArgument, SurfaceBlackFadeDirection direction,
bool forceEndpoint = false)
{
SurfaceBlackFades.Add((surface, intervalArgument, direction));
SurfaceBlackFadeForceEndpoints.Add(forceEndpoint);
}
public void CrossfadeSurfaces(
GfxState gfx, int sourceSurface, int targetSurface, long intervalArgument,
bool forceEndpoint = false)
{
SurfaceCrossfades.Add((sourceSurface, targetSurface, intervalArgument));
SurfaceCrossfadeForceEndpoints.Add(forceEndpoint);
}
public void CreateTexture(int slot, int w, int h) { }
public void SetTexture(long resId, int slot) => Textures.Add((resId, slot));
public void ClearRenderTarget(int surfaceSlot) => ClearedRenderTargets.Add(surfaceSlot);

View File

@@ -131,9 +131,13 @@ public interface IHost
void PresentFrame(GfxState gfx) { }
// Legacy SYS4 screen-transition family (ops 0x21, 0x22, and 0x25): scripts render complete
// frames into numbered surfaces, then block while the engine alpha-composites an endpoint.
// Native bypasses the timed service when ADV fast-forward is already active at opcode dispatch.
void FadeSurfaceWithBlack(
GfxState gfx, int surface, long intervalArgument, SurfaceBlackFadeDirection direction) { }
void CrossfadeSurfaces(GfxState gfx, int sourceSurface, int targetSurface, long intervalArgument) { }
GfxState gfx, int surface, long intervalArgument, SurfaceBlackFadeDirection direction,
bool forceEndpoint = false) { }
void CrossfadeSurfaces(
GfxState gfx, int sourceSurface, int targetSurface, long intervalArgument,
bool forceEndpoint = false) { }
void CreateTexture(int slot, int width, int height);
/// <summary>Return a stable RGBA snapshot of one numbered surface, or null when unavailable.</summary>
RgbaImage? CaptureSurfacePixels(int slot) => null;

View File

@@ -934,7 +934,49 @@ public sealed class GfxState
(o.RotationEnabled && o.RotationPeriodMs > 0)));
}
/// <summary>Click completion affects only type-0 foreground transitions, never ambient object channels.</summary>
/// <summary>Force the finite retained presentation serviced by native run-state bit 0x400 to its
/// endpoint. This is the EffectSkipOnClick path: type-0 surface commands and ordinary finite object
/// channels complete together, while movie masks, ambient cycles, and op-0x242-detached channels remain
/// active. Op 0x24e bit 0 disables click skipping; bit 1 suppresses the shared force-complete request.</summary>
public int CompleteClickSkippableTimedPresentation(long nowMs)
{
lock (_lock)
{
if ((AnimationServiceFlags & 3) != 0) return 0;
int completed = 0;
foreach (var t in _surfaceTransitions.Values)
{
if (t.Forced || TransitionProgress(t, nowMs) >= 1.0) continue;
t.Forced = true;
completed++;
}
completed += CountOneShotChannels(_rangeTransform);
foreach (var o in _objects.Values)
{
if ((o.OneShotAnimationControlFlags & 1) != 0) continue;
completed += CountOneShotChannels(o);
}
ForceCompleteOneShotChannels();
if (AnimClockDurationTicks != 0) completed++;
AnimClockDurationTicks = 0;
AnimClockGeneration++;
if (completed > 0) MarkRetainedMutation();
return completed;
}
}
private static int CountOneShotChannels(GfxObject o)
=> (o.OneShotColorEnabled ? 1 : 0)
+ (o.ScaleEnabled ? 1 : 0)
+ (o.RotationChannelEnabled ? 1 : 0)
+ (o.TranslationEnabled ? 1 : 0);
/// <summary>Force only queued type-0 foreground transitions. PresentFrame uses this to publish a
/// command endpoint immediately; interactive run-state-0x400 skipping uses the broader method above.</summary>
public int CompleteForegroundTransitions(long nowMs)
{
lock (_lock)

View File

@@ -2862,16 +2862,20 @@ public sealed class VirtualMachine
case "fade-surface-in-from-black": // 0x21: blocking black -> captured full-frame surface
case "u00418860":
_host.FadeSurfaceWithBlack(
Gfx, (int)Read(a[0]), Read(a[1]), SurfaceBlackFadeDirection.FromBlack);
Gfx, (int)Read(a[0]), Read(a[1]), SurfaceBlackFadeDirection.FromBlack,
_messageSkipServiceActive || _host.IsMessageSkipActive);
return pc + 1;
case "fade-surface-out-to-black": // 0x22: blocking captured full-frame surface -> black
case "u00418920":
_host.FadeSurfaceWithBlack(
Gfx, (int)Read(a[0]), Read(a[1]), SurfaceBlackFadeDirection.ToBlack);
Gfx, (int)Read(a[0]), Read(a[1]), SurfaceBlackFadeDirection.ToBlack,
_messageSkipServiceActive || _host.IsMessageSkipActive);
return pc + 1;
case "crossfade-surfaces": // 0x25: legacy full-frame surface alpha transition
case "u00418B40":
_host.CrossfadeSurfaces(Gfx, (int)Read(a[0]), (int)Read(a[1]), Read(a[2]));
_host.CrossfadeSurfaces(
Gfx, (int)Read(a[0]), (int)Read(a[1]), Read(a[2]),
_messageSkipServiceActive || _host.IsMessageSkipActive);
return pc + 1;
case "mark-frame-yield": // 0x21c: normal foreground-transition scheduler/resume boundary
_host.WaitForForegroundTransition(Gfx); return pc + 1;