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));
|
||||
|
||||
@@ -28,6 +28,13 @@ public readonly record struct SurfaceRectCopy(
|
||||
int SourceSurface, int DestinationSurface, int SourceX, int SourceY,
|
||||
int Width, int Height, int DestinationX, int DestinationY);
|
||||
|
||||
/// <summary>Opcode 0x24d's captured-range movie-mask transition. The movie's decoded green channel
|
||||
/// becomes a byte-per-pixel alpha mask over SourceRange in the scratch surface.</summary>
|
||||
public readonly record struct MovieMaskTransitionRequest(
|
||||
long CommandKey, int SurfaceSlot, long SourceRangeStart, int SourceRangeCount,
|
||||
int X, int Y, int Width, int Height, long Mode, long ResourceId,
|
||||
long StartDelayMs, long DurationMs);
|
||||
|
||||
/// <summary>A synchronous AGE-owned diagnostic prompt after native body/context formatting.</summary>
|
||||
public readonly record struct DiagnosticMessage(string Caption, string Text);
|
||||
|
||||
@@ -181,6 +188,14 @@ public interface IHost
|
||||
long? PlayMovieToSurfaceAtPosition(
|
||||
long resourceId, int surfaceSlot, long movieFlags, long syncMask, long positionMs)
|
||||
=> PlayMovieToSurface(resourceId, surfaceSlot, movieFlags, syncMask);
|
||||
// Native op 0x24d captures a retained range into the scratch surface, then publishes each
|
||||
// decoded movie frame's green channel as an exact packed-alpha mask. A nonvisual host completes
|
||||
// the lifecycle immediately so a following 0x21c can never strand script execution.
|
||||
void PlayMovieMaskTransition(GfxState gfx, MovieMaskTransitionRequest request)
|
||||
{
|
||||
gfx.QueueMovieMaskTransition(request);
|
||||
gfx.CompleteMovieMaskTransition(request.SurfaceSlot);
|
||||
}
|
||||
bool IsMovieSurfaceActive(int surfaceSlot) => false;
|
||||
// Native op 0x20f uses a universal packed id and parks script execution until the movie
|
||||
// reaches EOF or the player cancels it. The decoder remains asynchronous; the interactive host
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using Age.Engine.Hosting;
|
||||
|
||||
namespace Age.Engine.Model;
|
||||
|
||||
@@ -32,6 +33,9 @@ public readonly record struct SurfaceTransitionState(long CommandKey, int Target
|
||||
long RangeAStart, int RangeACount, long RangeBStart, int RangeBCount,
|
||||
long DelayMs, long DurationMs, long StartMs, double Progress, bool Forced);
|
||||
|
||||
public readonly record struct MovieMaskTransitionState(
|
||||
MovieMaskTransitionRequest Request, bool Completed);
|
||||
|
||||
/// <summary>One synchronized sample of op 0x202's native one-shot packed-color channel.</summary>
|
||||
public readonly record struct ColorTransitionState(long Current, long Target,
|
||||
long DelayMs, long DurationMs, long StartMs, double Progress, bool Active);
|
||||
@@ -112,6 +116,11 @@ public sealed class GfxState
|
||||
public long StartMs = -1;
|
||||
public bool Forced;
|
||||
}
|
||||
private sealed class MovieMaskTransition
|
||||
{
|
||||
public required MovieMaskTransitionRequest Request;
|
||||
public bool Completed;
|
||||
}
|
||||
public sealed class GfxObject
|
||||
{
|
||||
// The native numbered-save record contains several full transform matrices and reserved fields
|
||||
@@ -410,6 +419,7 @@ public sealed class GfxState
|
||||
_reloadableSurfaces.Clear();
|
||||
_movieStopTimesMs.Clear();
|
||||
_surfaceTransitions.Clear();
|
||||
_movieMaskTransitions.Clear();
|
||||
CurrentObject = 0;
|
||||
CurrentRenderTargetSlot = -1;
|
||||
_rangeTransformFirst = 0;
|
||||
@@ -525,6 +535,7 @@ public sealed class GfxState
|
||||
// the movie object exists but its host decoder supplied no usable IMediaPosition stop time.
|
||||
private readonly Dictionary<int, long?> _movieStopTimesMs = new();
|
||||
private readonly Dictionary<int, SurfaceTransition> _surfaceTransitions = new();
|
||||
private readonly Dictionary<int, MovieMaskTransition> _movieMaskTransitions = new();
|
||||
public void SetSurface(int slot, long resId, long colorKey)
|
||||
{
|
||||
lock (_lock)
|
||||
@@ -587,6 +598,7 @@ public sealed class GfxState
|
||||
_createdSurfaces.Remove(slot);
|
||||
_movieStopTimesMs.Remove(slot);
|
||||
_surfaceTransitions.Remove(slot);
|
||||
_movieMaskTransitions.Remove(slot);
|
||||
}
|
||||
if (CurrentRenderTargetSlot >= firstSlot && CurrentRenderTargetSlot < end)
|
||||
CurrentRenderTargetSlot = -1;
|
||||
@@ -707,6 +719,7 @@ public sealed class GfxState
|
||||
_createdSurfaces.Remove(slot);
|
||||
_movieStopTimesMs.Remove(slot);
|
||||
_surfaceTransitions.Remove(slot);
|
||||
_movieMaskTransitions.Remove(slot);
|
||||
MarkRetainedMutation();
|
||||
}
|
||||
}
|
||||
@@ -729,6 +742,45 @@ public sealed class GfxState
|
||||
}
|
||||
}
|
||||
|
||||
public void QueueMovieMaskTransition(MovieMaskTransitionRequest request)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_movieMaskTransitions[request.SurfaceSlot] = new MovieMaskTransition
|
||||
{
|
||||
Request = request with
|
||||
{
|
||||
SourceRangeCount = System.Math.Max(0, request.SourceRangeCount),
|
||||
Width = System.Math.Max(0, request.Width),
|
||||
Height = System.Math.Max(0, request.Height),
|
||||
StartDelayMs = System.Math.Max(0, request.StartDelayMs),
|
||||
DurationMs = System.Math.Max(0, request.DurationMs),
|
||||
},
|
||||
};
|
||||
MarkRetainedMutation();
|
||||
}
|
||||
}
|
||||
|
||||
public bool CompleteMovieMaskTransition(int surfaceSlot)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_movieMaskTransitions.TryGetValue(surfaceSlot, out var transition)
|
||||
|| transition.Completed)
|
||||
return false;
|
||||
transition.Completed = true;
|
||||
MarkRetainedMutation();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<MovieMaskTransitionState> SnapshotMovieMaskTransitions()
|
||||
{
|
||||
lock (_lock)
|
||||
return _movieMaskTransitions.Values
|
||||
.Select(t => new MovieMaskTransitionState(t.Request, t.Completed)).ToList();
|
||||
}
|
||||
|
||||
/// <summary>Start every pending foreground transition at the native present boundary.</summary>
|
||||
public int StartForegroundTransitions(long nowMs)
|
||||
{
|
||||
@@ -744,7 +796,8 @@ public sealed class GfxState
|
||||
public bool HasActiveForegroundTransitions(long nowMs)
|
||||
{
|
||||
lock (_lock)
|
||||
return _surfaceTransitions.Values.Any(t => TransitionProgress(t, nowMs) < 1.0);
|
||||
return _surfaceTransitions.Values.Any(t => TransitionProgress(t, nowMs) < 1.0)
|
||||
|| _movieMaskTransitions.Values.Any(t => !t.Completed);
|
||||
}
|
||||
|
||||
/// <summary>Native op 0x21c keeps presenting until both queued surface commands and finite one-shot
|
||||
@@ -753,6 +806,7 @@ public sealed class GfxState
|
||||
{
|
||||
lock (_lock)
|
||||
return _surfaceTransitions.Values.Any(t => TransitionProgress(t, nowMs) < 1.0) ||
|
||||
_movieMaskTransitions.Values.Any(t => !t.Completed) ||
|
||||
_rangeTransform.ScaleEnabled || _rangeTransform.RotationChannelEnabled ||
|
||||
_rangeTransform.TranslationEnabled ||
|
||||
_objects.Values.Any(o => o.Visible &&
|
||||
@@ -780,10 +834,12 @@ public sealed class GfxState
|
||||
return new GfxDiagnosticSnapshot(
|
||||
nowMs,
|
||||
_surfaceTransitions.Values.Any(t => TransitionProgress(t, nowMs) < 1.0)
|
||||
|| _movieMaskTransitions.Values.Any(t => !t.Completed)
|
||||
|| range != null || objects.Length != 0,
|
||||
_objects.Count,
|
||||
_objects.Values.Count(o => o.Visible),
|
||||
_surfaceTransitions.Values.Count(t => TransitionProgress(t, nowMs) < 1.0),
|
||||
_surfaceTransitions.Values.Count(t => TransitionProgress(t, nowMs) < 1.0)
|
||||
+ _movieMaskTransitions.Values.Count(t => !t.Completed),
|
||||
AnimationServiceFlags,
|
||||
AnimClockDurationTicks,
|
||||
AnimClockGeneration,
|
||||
@@ -866,6 +922,7 @@ public sealed class GfxState
|
||||
{
|
||||
lock (_lock)
|
||||
return _surfaceTransitions.Values.Any(t => TransitionProgress(t, nowMs) < 1.0) ||
|
||||
_movieMaskTransitions.Values.Any(t => !t.Completed) ||
|
||||
_rangeTransform.ScaleEnabled || _rangeTransform.RotationChannelEnabled ||
|
||||
_rangeTransform.TranslationEnabled ||
|
||||
_objects.Values.Any(o => o.Visible &&
|
||||
|
||||
66
engine/Age.Engine/Sys4/MovieMaskSurface.cs
Normal file
66
engine/Age.Engine/Sys4/MovieMaskSurface.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using Age.Engine.Model;
|
||||
|
||||
namespace Age.Engine.Sys4;
|
||||
|
||||
/// <summary>Platform-neutral pixel oracle for opcode 0x24d's type-1 movie transition.</summary>
|
||||
public static class MovieMaskSurface
|
||||
{
|
||||
/// <summary>Extract logical top-down green samples into the requested native mask dimensions.
|
||||
/// Missing source pixels retain <paramref name="fill"/>.</summary>
|
||||
public static byte[] ExtractGreen(RgbaImage frame, int width, int height, byte fill)
|
||||
{
|
||||
int safeWidth = System.Math.Max(0, width);
|
||||
int safeHeight = System.Math.Max(0, height);
|
||||
var mask = new byte[checked(safeWidth * safeHeight)];
|
||||
if (fill != 0) System.Array.Fill(mask, fill);
|
||||
int copyWidth = System.Math.Min(safeWidth, frame.Width);
|
||||
int copyHeight = System.Math.Min(safeHeight, frame.Height);
|
||||
for (int row = 0; row < copyHeight; row++)
|
||||
{
|
||||
int source = checked(row * frame.Width * 4 + 1);
|
||||
int destination = checked(row * safeWidth);
|
||||
for (int column = 0; column < copyWidth; column++, source += 4)
|
||||
mask[destination + column] = frame.Pixels[source];
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
/// <summary>Apply AGE's exact packed-ARGB multiplication to a captured RGBA surface. RGB and
|
||||
/// pixels outside the requested rectangle are preserved. The multiplication deliberately includes
|
||||
/// carry from the packed red/green bytes; it is not conventional alpha*mask/255.</summary>
|
||||
public static RgbaImage Apply(
|
||||
RgbaImage captured, ReadOnlySpan<byte> mask, int maskWidth, int maskHeight, int x, int y)
|
||||
{
|
||||
if (maskWidth < 0 || maskHeight < 0
|
||||
|| mask.Length != checked(maskWidth * maskHeight))
|
||||
throw new System.ArgumentException("mask dimensions do not match its byte count", nameof(mask));
|
||||
var output = new RgbaImage(
|
||||
captured.Width, captured.Height, (byte[])captured.Pixels.Clone());
|
||||
long left = System.Math.Max(0L, x);
|
||||
long top = System.Math.Max(0L, y);
|
||||
long right = System.Math.Min((long)captured.Width, (long)x + maskWidth);
|
||||
long bottom = System.Math.Min((long)captured.Height, (long)y + maskHeight);
|
||||
if (right <= left || bottom <= top) return output;
|
||||
|
||||
for (int destinationY = (int)top; destinationY < (int)bottom; destinationY++)
|
||||
{
|
||||
int maskY = destinationY - y;
|
||||
for (int destinationX = (int)left; destinationX < (int)right; destinationX++)
|
||||
{
|
||||
int pixel = checked((destinationY * captured.Width + destinationX) * 4);
|
||||
byte red = captured.Pixels[pixel];
|
||||
byte green = captured.Pixels[pixel + 1];
|
||||
byte blue = captured.Pixels[pixel + 2];
|
||||
byte alpha = captured.Pixels[pixel + 3];
|
||||
uint argb = (uint)alpha << 24
|
||||
| (uint)red << 16
|
||||
| (uint)green << 8
|
||||
| blue;
|
||||
uint product = unchecked((argb >> 8)
|
||||
* mask[checked(maskY * maskWidth + destinationX - x)]);
|
||||
output.Pixels[pixel + 3] = (byte)(product >> 24);
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
}
|
||||
@@ -2846,6 +2846,14 @@ public sealed class VirtualMachine
|
||||
Gfx.ResetAnimClock(); return pc + 1;
|
||||
case "set-gfx-animation-service-flags": // 0x24e: bit 1 suppresses op 0x243
|
||||
Gfx.SetAnimationServiceFlags(Read(a[0])); return pc + 1;
|
||||
case "play-movie-mask-transition": // 0x24d: captured retained range + movie green-channel mask
|
||||
_host.PlayMovieMaskTransition(Gfx, new MovieMaskTransitionRequest(
|
||||
Read(a[0]), unchecked((int)Read(a[1])),
|
||||
Read(a[2]), unchecked((int)Read(a[3])),
|
||||
unchecked((int)Read(a[4])), unchecked((int)Read(a[5])),
|
||||
unchecked((int)Read(a[6])), unchecked((int)Read(a[7])),
|
||||
Read(a[8]), Read(a[9]), Read(a[10]), Read(a[11])));
|
||||
return pc + 1;
|
||||
case "queue-surface-alpha-transition": // 0x223: target surface crossfade over two object ranges
|
||||
Gfx.QueueSurfaceAlphaTransition(Read(a[0]), (int)Read(a[1]), Read(a[2]), (int)Read(a[3]),
|
||||
Read(a[4]), (int)Read(a[5]), Read(a[6]), Read(a[7])); return pc + 1;
|
||||
|
||||
Reference in New Issue
Block a user