Implement movie surface stop-time queries
This commit is contained in:
@@ -100,6 +100,70 @@ public class MovieOpcodeTests
|
||||
Assert.Equal(0x33, vm.Gfx.SnapshotVisibleObjects().Single().SurfaceResId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QueryMovieStopTimeReturnsTheValueRetainedByPlayMovie()
|
||||
{
|
||||
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||
var script = ScriptAssembler.Assemble(table, "MOVIE-TIME", new List<(int, Operand[])>
|
||||
{
|
||||
(0x236, new[] { new Operand(0, 0x33), new Operand(0, 5), new Operand(0, 2), new Operand(0, 0) }),
|
||||
(0x23f, new[] { new Operand(3, 0x1234), new Operand(0, 5) }),
|
||||
(0x2, System.Array.Empty<Operand>()),
|
||||
}, System.Array.Empty<string>());
|
||||
var host = new RecordingHost { MovieStopTimeMs = 1876 };
|
||||
var vm = new VirtualMachine(script, table, host);
|
||||
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal(1876, vm.Globals[0x1234]);
|
||||
Assert.True(vm.Gfx.TryGetMovieStopTime(5, out long? retained));
|
||||
Assert.Equal(1876, retained);
|
||||
Assert.Empty(host.Warnings);
|
||||
|
||||
vm.Gfx.SetSurface(5, 0x34, 0); // replacing the native surface tears down its movie metadata
|
||||
Assert.False(vm.Gfx.TryGetMovieStopTime(5, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QueryMovieStopTimeReturnsMinusOneWithoutWarningForAnEmptyMovieSlot()
|
||||
{
|
||||
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||
var script = ScriptAssembler.Assemble(table, "EMPTY-MOVIE-TIME", new List<(int, Operand[])>
|
||||
{
|
||||
(0x23f, new[] { new Operand(3, 0x1234), new Operand(0, 5) }),
|
||||
(0x2, System.Array.Empty<Operand>()),
|
||||
}, System.Array.Empty<string>());
|
||||
var host = new RecordingHost();
|
||||
var vm = new VirtualMachine(script, table, host);
|
||||
vm.Gfx.GetOrCreate(5); // retained-object existence is unrelated to the native movie-object slot
|
||||
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal(-1, vm.Globals[0x1234]);
|
||||
Assert.Empty(host.Warnings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QueryMovieStopTimeWarnsAndReturnsMinusOneWhenMetadataIsUnavailable()
|
||||
{
|
||||
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||
var script = ScriptAssembler.Assemble(table, "MISSING-MOVIE-TIME", new List<(int, Operand[])>
|
||||
{
|
||||
(0x236, new[] { new Operand(0, 0x33), new Operand(0, 5), new Operand(0, 2), new Operand(0, 0) }),
|
||||
(0x23f, new[] { new Operand(3, 0x1234), new Operand(0, 5) }),
|
||||
(0x2, System.Array.Empty<Operand>()),
|
||||
}, System.Array.Empty<string>());
|
||||
var host = new RecordingHost();
|
||||
var vm = new VirtualMachine(script, table, host);
|
||||
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal(-1, vm.Globals[0x1234]);
|
||||
string warning = Assert.Single(host.Warnings);
|
||||
Assert.Contains("movie stop-time unavailable MISSING-MOVIE-TIME@", warning);
|
||||
Assert.Contains("surface=5; returning -1", warning);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sc0000MoviePayloadReadsFromArchiveVfsAndIsMpegProgramStream()
|
||||
{
|
||||
@@ -136,6 +200,8 @@ public class MovieOpcodeTests
|
||||
var entry = resources.Resolve("SC0000", 0x33)!;
|
||||
using var decoder = new DirectShowMovieDecoder(resources.ReadMovie(entry));
|
||||
|
||||
Assert.True(decoder.StopTimeMs > 0, "DirectShow should expose a positive IMediaPosition stop time");
|
||||
|
||||
var deadline = DateTime.UtcNow.AddSeconds(10);
|
||||
RgbaImage? frame = null;
|
||||
while (DateTime.UtcNow < deadline && !decoder.TryTakeFrame(out frame))
|
||||
|
||||
@@ -38,6 +38,7 @@ 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 long? MovieStopTimeMs;
|
||||
public readonly List<(long Resource, int Surface, long Flags)> ModalMovies = new();
|
||||
public readonly List<int> ClearedRenderTargets = new();
|
||||
public readonly List<(int First, int Count)> ReleasedSurfaceRanges = new();
|
||||
@@ -45,11 +46,13 @@ internal class RecordingHost : IHost
|
||||
public readonly List<(long Resource, int Slot)> Textures = new();
|
||||
public readonly List<bool> MessageSkipChanges = new();
|
||||
public readonly List<bool> PhysicalMessageSkipChanges = new();
|
||||
public readonly List<string> Warnings = new();
|
||||
public readonly List<long> CursorResources = new();
|
||||
public readonly List<bool> AdvPagePresentationSuspended = new();
|
||||
public int CursorClearCount;
|
||||
public int SceneContextResets;
|
||||
public long TextureResourceIdOffset;
|
||||
public void ReportWarning(string message) => Warnings.Add(message);
|
||||
public long ResolveTextureResourceId(long resourceId) => resourceId + TextureResourceIdOffset;
|
||||
public void ShowText(int offset, string text) => Lines.Add((offset, text));
|
||||
public void SetAdvTextCursor(int layoutSlot, int x, int y) => TextCursors.Add((layoutSlot, x, y));
|
||||
@@ -145,8 +148,11 @@ internal class RecordingHost : IHost
|
||||
=> ScheduledSfxStarts.Add((channel, startMode, delayMs));
|
||||
public void ReleaseSoundEffect(int channel) => SfxReleases.Add(channel);
|
||||
public void FadeBgm(int targetPercent, long durationMs) => BgmFades.Add((targetPercent, durationMs));
|
||||
public void PlayMovieToSurface(long resourceId, int surfaceSlot, long movieFlags, long syncMask)
|
||||
=> Movies.Add((resourceId, surfaceSlot, movieFlags, syncMask));
|
||||
public long? PlayMovieToSurface(long resourceId, int surfaceSlot, long movieFlags, long syncMask)
|
||||
{
|
||||
Movies.Add((resourceId, surfaceSlot, movieFlags, syncMask));
|
||||
return MovieStopTimeMs;
|
||||
}
|
||||
public void PlayModalMovieToSurface(long rawResourceId, int surfaceSlot, long movieFlags)
|
||||
=> ModalMovies.Add((rawResourceId, surfaceSlot, movieFlags));
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ public readonly record struct SurfaceRectFill(
|
||||
|
||||
public interface IHost
|
||||
{
|
||||
/// <summary>Report a recoverable runtime discrepancy while allowing script execution to continue.</summary>
|
||||
void ReportWarning(string message) => System.Console.Error.WriteLine(message);
|
||||
// Script-local resource ids resolve against the currently executing frame's SYS4INI section.
|
||||
// Interactive hosts track this stack; headless hosts may keep the no-op/default identity behavior.
|
||||
void EnterScriptContext(string scriptName) { }
|
||||
@@ -111,7 +113,10 @@ public interface IHost
|
||||
void FadeBgm(int targetPercent, long durationMs) { }
|
||||
// Native op 0x236 binds a DirectShow movie decoder to an existing retained texture surface.
|
||||
// Playback is non-modal: the VM advances to the following instruction while the host publishes frames.
|
||||
void PlayMovieToSurface(long resourceId, int surfaceSlot, long movieFlags, long syncMask) { }
|
||||
/// <returns>The initialized movie graph's stop position in truncated integer milliseconds, or null
|
||||
/// when the host could not obtain usable timing metadata. Native op 0x23f queries this state
|
||||
/// immediately after 0x236 returns.</returns>
|
||||
long? PlayMovieToSurface(long resourceId, int surfaceSlot, long movieFlags, long syncMask) => null;
|
||||
// Native op 0x20f uses a universal raw-catalog id and parks script execution until the movie
|
||||
// reaches EOF or the player cancels it. The decoder remains asynchronous; the interactive host
|
||||
// owns the modal wait so its render loop can continue publishing frames.
|
||||
|
||||
@@ -299,6 +299,7 @@ public sealed class GfxState
|
||||
_objects.Clear();
|
||||
_fieldTable.Clear();
|
||||
_surfaces.Clear();
|
||||
_movieStopTimesMs.Clear();
|
||||
_surfaceTransitions.Clear();
|
||||
CurrentObject = 0;
|
||||
CurrentRenderTargetSlot = -1;
|
||||
@@ -314,8 +315,32 @@ public sealed class GfxState
|
||||
|
||||
// ---- surfaces (image buffers per slot): ctx+0x52bd4[slot], from create/set-texture ----
|
||||
private readonly Dictionary<int, (long ResId, long ColorKey)> _surfaces = new();
|
||||
// A separate entry models the native CMovieToTexture object attached to a surface. A null value means
|
||||
// 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();
|
||||
public void SetSurface(int slot, long resId, long colorKey) { lock (_lock) { _surfaces[slot] = (resId, colorKey); } }
|
||||
public void SetSurface(int slot, long resId, long colorKey)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_surfaces[slot] = (resId, colorKey);
|
||||
_movieStopTimesMs.Remove(slot);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Op 0x236 handoff: retain the initialized movie graph's IMediaPosition stop time. Null
|
||||
/// deliberately distinguishes a movie surface with unavailable metadata from an empty movie slot.</summary>
|
||||
public void SetMovieStopTime(int slot, long? stopTimeMs)
|
||||
{
|
||||
lock (_lock) _movieStopTimesMs[slot] = stopTimeMs;
|
||||
}
|
||||
|
||||
/// <summary>Op 0x23f query. False means no movie object occupies the slot; true with a null value
|
||||
/// means the movie exists but its stop-time query failed or returned unusable metadata.</summary>
|
||||
public bool TryGetMovieStopTime(int slot, out long? stopTimeMs)
|
||||
{
|
||||
lock (_lock) return _movieStopTimesMs.TryGetValue(slot, out stopTimeMs);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
@@ -333,6 +358,7 @@ public sealed class GfxState
|
||||
for (int slot = firstSlot; slot < end; slot++)
|
||||
{
|
||||
_surfaces.Remove(slot);
|
||||
_movieStopTimesMs.Remove(slot);
|
||||
_surfaceTransitions.Remove(slot);
|
||||
}
|
||||
if (CurrentRenderTargetSlot >= firstSlot && CurrentRenderTargetSlot < end)
|
||||
@@ -429,7 +455,14 @@ public sealed class GfxState
|
||||
o.ColorAnim = true;
|
||||
}
|
||||
}
|
||||
public void ClearSurface(int slot) { lock (_lock) { _surfaces[slot] = (0, 0); } } // create-texture (blank)
|
||||
public void ClearSurface(int slot)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_surfaces[slot] = (0, 0); // create-texture (blank)
|
||||
_movieStopTimesMs.Remove(slot);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Op 0x223: queue a type-0 timed alpha transition into a target surface slot.</summary>
|
||||
public void QueueSurfaceAlphaTransition(long commandKey, int targetSlot,
|
||||
|
||||
@@ -1425,7 +1425,8 @@ public sealed class VirtualMachine
|
||||
// The native CMovieToTexture renderer replaces the pixels of the already-created surface.
|
||||
// Retain the same resource binding so the compositor resolves live movie frames for its objects.
|
||||
Gfx.SetSurface(surfaceSlot, resourceId, 0);
|
||||
_host.PlayMovieToSurface(resourceId, surfaceSlot, Read(a[2]), Read(a[3]));
|
||||
long? stopTimeMs = _host.PlayMovieToSurface(resourceId, surfaceSlot, Read(a[2]), Read(a[3]));
|
||||
Gfx.SetMovieStopTime(surfaceSlot, stopTimeMs);
|
||||
return pc + 1; // native cmd size 9 resumes at the next instruction; playback is asynchronous
|
||||
}
|
||||
// ---- gfx command-buffer ops (VM-internal GfxState; docs/engine-re.md op-contract table) ----
|
||||
@@ -1490,8 +1491,26 @@ public sealed class VirtualMachine
|
||||
else Write(a[0], 1); // native missing-object path leaves output operands untouched
|
||||
return pc + 1;
|
||||
}
|
||||
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 "u00422930": // pre-reference compatibility
|
||||
case "query-surface-stop-time-ms": // 0x23f (out_stop_time_ms)(surface_slot)
|
||||
{
|
||||
int surfaceSlot = (int)Read(a[1]);
|
||||
if (!Gfx.TryGetMovieStopTime(surfaceSlot, out long? stopTimeMs))
|
||||
{
|
||||
Write(a[0], -1); // native null CMovieToTexture slot
|
||||
return pc + 1;
|
||||
}
|
||||
if (!stopTimeMs.HasValue)
|
||||
{
|
||||
_host.ReportWarning(
|
||||
$"movie stop-time unavailable {_cur.Script.Name}@0x{ins.Offset:x} " +
|
||||
$"surface={surfaceSlot}; returning -1");
|
||||
Write(a[0], -1);
|
||||
return pc + 1;
|
||||
}
|
||||
Write(a[0], stopTimeMs.Value);
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user