Implement movie mask transition opcode
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Age.Engine.Model;
|
||||
using Age.Engine.Hosting;
|
||||
using Age.Engine.Sys4;
|
||||
using Age.Engine.Vm;
|
||||
using Xunit;
|
||||
@@ -74,4 +75,22 @@ public class ForegroundTransitionTests
|
||||
Assert.Equal(1, host.TransitionWaits);
|
||||
Assert.Equal(1.0, vm.Gfx.SnapshotForegroundTransitions(100).Single().Progress);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MovieMaskTransitionBlocksUntilMovieCompletionAndIgnoresClickCompletion()
|
||||
{
|
||||
var gfx = new GfxState();
|
||||
var request = new MovieMaskTransitionRequest(
|
||||
11, 45, 10, 1, -184, 0, 800, 600, 0, 0x325e, 0, 1000);
|
||||
|
||||
gfx.QueueMovieMaskTransition(request);
|
||||
|
||||
Assert.True(gfx.HasActiveForegroundTransitions(0));
|
||||
Assert.True(gfx.HasActiveTimedPresentation(0));
|
||||
Assert.Equal(0, gfx.CompleteForegroundTransitions(100));
|
||||
Assert.False(gfx.SnapshotMovieMaskTransitions().Single().Completed);
|
||||
Assert.True(gfx.CompleteMovieMaskTransition(45));
|
||||
Assert.False(gfx.HasActiveForegroundTransitions(100));
|
||||
Assert.True(gfx.SnapshotMovieMaskTransitions().Single().Completed);
|
||||
}
|
||||
}
|
||||
|
||||
55
engine/Age.Engine.Tests/MovieMaskSurfaceTests.cs
Normal file
55
engine/Age.Engine.Tests/MovieMaskSurfaceTests.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using Age.Engine.Model;
|
||||
using Age.Engine.Sys4;
|
||||
using Xunit;
|
||||
|
||||
public class MovieMaskSurfaceTests
|
||||
{
|
||||
[Fact]
|
||||
public void ExtractGreenUsesLogicalRowsAndRequestedMaskDimensions()
|
||||
{
|
||||
var frame = new RgbaImage(2, 2,
|
||||
[
|
||||
1, 10, 2, 255, 3, 20, 4, 255,
|
||||
5, 30, 6, 255, 7, 40, 8, 255,
|
||||
]);
|
||||
|
||||
Assert.Equal(new byte[] { 10, 20, 99, 30, 40, 99 },
|
||||
MovieMaskSurface.ExtractGreen(frame, 3, 2, 99));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyPreservesRgbAndUsesExactPackedCarryAlpha()
|
||||
{
|
||||
var captured = new RgbaImage(2, 1,
|
||||
[
|
||||
0, 0, 0, 128,
|
||||
255, 0, 0, 128,
|
||||
]);
|
||||
|
||||
RgbaImage result = MovieMaskSurface.Apply(captured, [255, 255], 2, 1, 0, 0);
|
||||
|
||||
Assert.Equal(new byte[]
|
||||
{
|
||||
0, 0, 0, 127, // packed 0x80000000: native differs from alpha*255/255
|
||||
255, 0, 0, 128, // packed red carry reaches the output alpha byte
|
||||
}, result.Pixels);
|
||||
Assert.Equal(new byte[] { 0, 0, 0, 128, 255, 0, 0, 128 }, captured.Pixels);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyClipsSignedRectangleAndLeavesOutsidePixelsUnchanged()
|
||||
{
|
||||
var captured = new RgbaImage(3, 1,
|
||||
[
|
||||
1, 2, 3, 255,
|
||||
4, 5, 6, 255,
|
||||
7, 8, 9, 255,
|
||||
]);
|
||||
|
||||
RgbaImage result = MovieMaskSurface.Apply(captured, [0, 64, 128], 3, 1, -1, 0);
|
||||
|
||||
Assert.Equal((byte)63, result.Pixels[3]); // mask column 1
|
||||
Assert.Equal((byte)127, result.Pixels[7]); // mask column 2
|
||||
Assert.Equal((byte)255, result.Pixels[11]);
|
||||
}
|
||||
}
|
||||
@@ -33,10 +33,13 @@ public class MovieOpcodeTests
|
||||
{
|
||||
public MoviePayload? OpenedPayload { get; private set; }
|
||||
public long OpenedInitialPositionMs { get; private set; }
|
||||
public IMovieDecoder Open(MoviePayload movie, long initialPositionMs = 0)
|
||||
public long? OpenedPresentationDurationMs { get; private set; }
|
||||
public IMovieDecoder Open(
|
||||
MoviePayload movie, long initialPositionMs = 0, long? presentationDurationMs = null)
|
||||
{
|
||||
OpenedPayload = movie;
|
||||
OpenedInitialPositionMs = initialPositionMs;
|
||||
OpenedPresentationDurationMs = presentationDurationMs;
|
||||
return decoder;
|
||||
}
|
||||
}
|
||||
@@ -202,6 +205,27 @@ public class MovieOpcodeTests
|
||||
Assert.Null(decoder.Failure);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FfmpegDecoderRetimeUsesRequestedDurationAndHoldsTerminalFrameToEndpoint()
|
||||
{
|
||||
var source = new FakeFfmpegFrameSource(
|
||||
new FfmpegMovieInfo(1, 1, 1000, 30, 1, false),
|
||||
SyntheticMovieFrame(1, 600), SyntheticMovieFrame(2, 900));
|
||||
using var clock = new ManualMoviePacingClock();
|
||||
using var decoder = new FfmpegMovieDecoder(
|
||||
source, clock, presentationDurationMs: 500);
|
||||
|
||||
Assert.True(SpinWait.SpinUntil(() => decoder.TryTakeFrame(out _), 1000));
|
||||
Assert.True(clock.WaitForDeadline(150)); // (900 - 600) * 500 / 1000
|
||||
clock.AdvanceTo(150);
|
||||
Assert.True(SpinWait.SpinUntil(() => decoder.TryTakeFrame(out _), 1000));
|
||||
Assert.True(clock.WaitForDeadline(500));
|
||||
clock.AdvanceTo(499);
|
||||
Assert.False(decoder.IsCompleted);
|
||||
clock.AdvanceTo(500);
|
||||
Assert.True(SpinWait.SpinUntil(() => decoder.IsCompleted, 1000));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FfmpegDecoderPublishesAndRetainsFirstDecodedFrameBeforePacing()
|
||||
{
|
||||
@@ -605,6 +629,37 @@ public class MovieOpcodeTests
|
||||
Assert.Equal(0x5678, vm.Globals[0x1235]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MovieMaskTransitionDispatchesAllTwelveOperandsAndResumes()
|
||||
{
|
||||
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||
var script = ScriptAssembler.Assemble(table, "MOVIE-MASK",
|
||||
[
|
||||
(0x24d,
|
||||
[
|
||||
new Operand(0, 11), new Operand(0, 45),
|
||||
new Operand(0, 10), new Operand(0, 1),
|
||||
new Operand(0, unchecked((uint)-184)), new Operand(0, 0),
|
||||
new Operand(0, 800), new Operand(0, 600),
|
||||
new Operand(0, 0), new Operand(0, 0x325e),
|
||||
new Operand(0, 0), new Operand(0, 1000),
|
||||
]),
|
||||
(0x55, [new Operand(3, 0x1234), new Operand(0, 0x5678)]),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
], []);
|
||||
var host = new RecordingHost();
|
||||
var vm = new VirtualMachine(script, table, host);
|
||||
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal("exit", vm.HaltReason);
|
||||
Assert.Equal(0x5678, vm.Globals[0x1234]);
|
||||
Assert.Equal(new MovieMaskTransitionRequest(
|
||||
11, 45, 10, 1, -184, 0, 800, 600, 0, 0x325e, 0, 1000),
|
||||
host.MovieMaskTransitions.Single());
|
||||
Assert.True(vm.Gfx.SnapshotMovieMaskTransitions().Single().Completed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlayMovieKeepsCreatedSurfaceBlankDuringSynchronousHostSetup()
|
||||
{
|
||||
@@ -727,6 +782,28 @@ public class MovieOpcodeTests
|
||||
Assert.Equal(new byte[] { 0, 0, 1, 0xba }, movie.Bytes[..4]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DebugTestMovieDecodesToExactGreenMaskDimensions()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows()) return;
|
||||
ConfigureFfmpegNativeProbe();
|
||||
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
|
||||
var resources = new ResourceMap(catalog, new Sys4AssetStore(catalog, Paths.GameDir));
|
||||
var payload = resources.ReadMovie(resources.ResolveMovie(0x325e)!);
|
||||
|
||||
using var movie = new FfmpegMovieSession(payload);
|
||||
Assert.Equal("TEST.AGF", payload.Name);
|
||||
Assert.Equal(800, movie.Info.Width);
|
||||
Assert.Equal(600, movie.Info.Height);
|
||||
Assert.Equal(1000, movie.Info.StopTimeMs);
|
||||
Assert.False(movie.Info.HasAudio);
|
||||
Assert.True(movie.TryDecodeNextVideoFrame(out var frame));
|
||||
|
||||
byte[] mask = MovieMaskSurface.ExtractGreen(frame.Image, 800, 600, 255);
|
||||
Assert.Equal(800 * 600, mask.Length);
|
||||
Assert.True(mask.Distinct().Skip(1).Any(), "TEST.AGF should publish a nonuniform green mask");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x335f, "LOGO.AGF")]
|
||||
[InlineData(0x3364, "OP.AGF")]
|
||||
|
||||
@@ -51,6 +51,7 @@ internal class RecordingHost : IHost
|
||||
public readonly List<(long Resource, int Surface, long Flags, long SyncMask)> Movies = new();
|
||||
public readonly List<(long Resource, int Surface, long Flags, long SyncMask, long PositionMs)>
|
||||
PositionedMovies = new();
|
||||
public readonly List<MovieMaskTransitionRequest> MovieMaskTransitions = new();
|
||||
public System.Action? OnPlayMovie;
|
||||
public long? MovieStopTimeMs;
|
||||
public readonly HashSet<int> ActiveMovieSurfaces = new();
|
||||
@@ -225,6 +226,12 @@ internal class RecordingHost : IHost
|
||||
OnPlayMovie?.Invoke();
|
||||
return MovieStopTimeMs;
|
||||
}
|
||||
public void PlayMovieMaskTransition(GfxState gfx, MovieMaskTransitionRequest request)
|
||||
{
|
||||
MovieMaskTransitions.Add(request);
|
||||
gfx.QueueMovieMaskTransition(request);
|
||||
gfx.CompleteMovieMaskTransition(request.SurfaceSlot);
|
||||
}
|
||||
public bool IsMovieSurfaceActive(int surfaceSlot) => ActiveMovieSurfaces.Contains(surfaceSlot);
|
||||
public void PlayModalMovieToSurface(long resourceId, int surfaceSlot, long movieFlags)
|
||||
=> ModalMovies.Add((resourceId, surfaceSlot, movieFlags));
|
||||
|
||||
Reference in New Issue
Block a user