[VideoOut] Add Bink2 support via FFMPEG bridge (#527)

* [VideoOut] Add Bink2 support via FFMPEG bridge

* [CMake] update commit

* [CMake] update commit
This commit is contained in:
Berk
2026-07-22 21:41:41 +03:00
committed by GitHub
parent d3600c9255
commit f704586a8d
22 changed files with 3087 additions and 310 deletions
@@ -0,0 +1,93 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.HLE;
using SharpEmu.Libs.Agc;
using Xunit;
namespace SharpEmu.Libs.Tests.Agc;
public sealed class AgcPredicationTests
{
private const ulong BaseAddress = 0x1_0000_0000;
private const ulong CommandBufferAddress = BaseAddress + 0x100;
private const ulong PacketAddress = BaseAddress + 0x400;
private const ulong PredicateAddress = BaseAddress + 0x800;
[Fact]
public void DcbSetPredication_EmitsGen5Packet()
{
var memory = new FakeCpuMemory(BaseAddress, 0x2000);
var ctx = new CpuContext(memory, Generation.Gen5);
WriteUInt64(memory, CommandBufferAddress + 0x10, PacketAddress);
WriteUInt64(memory, CommandBufferAddress + 0x18, PacketAddress + 0x100);
ctx[CpuRegister.Rdi] = CommandBufferAddress;
ctx[CpuRegister.Rsi] = 1;
ctx[CpuRegister.Rdx] = 3;
ctx[CpuRegister.Rcx] = 1;
ctx[CpuRegister.R8] = PredicateAddress + 7;
ctx[CpuRegister.R9] = 2;
var result = AgcExports.DcbSetPredication(ctx);
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, result);
Assert.Equal(PacketAddress, ctx[CpuRegister.Rax]);
Assert.Equal(0xC002_2000u, ReadUInt32(memory, PacketAddress));
Assert.Equal(0x0003_1100u, ReadUInt32(memory, PacketAddress + 4));
Assert.Equal(unchecked((uint)PredicateAddress), ReadUInt32(memory, PacketAddress + 8));
Assert.Equal((uint)(PredicateAddress >> 32), ReadUInt32(memory, PacketAddress + 12));
Assert.Equal(PacketAddress + 16, ReadUInt64(memory, CommandBufferAddress + 0x10));
}
[Fact]
public void SetPacketPredication_TogglesPacketHeaderBit()
{
var memory = new FakeCpuMemory(BaseAddress, 0x1000);
var ctx = new CpuContext(memory, Generation.Gen5);
const uint header = 0xC003_1500;
WriteUInt32(memory, PacketAddress, header);
ctx[CpuRegister.Rdi] = PacketAddress;
ctx[CpuRegister.Rsi] = 1;
Assert.Equal(
(int)OrbisGen2Result.ORBIS_GEN2_OK,
AgcExports.SetPacketPredication(ctx));
Assert.Equal(header | 1u, ReadUInt32(memory, PacketAddress));
ctx[CpuRegister.Rsi] = 0;
Assert.Equal(
(int)OrbisGen2Result.ORBIS_GEN2_OK,
AgcExports.SetPacketPredication(ctx));
Assert.Equal(header, ReadUInt32(memory, PacketAddress));
}
private static uint ReadUInt32(FakeCpuMemory memory, ulong address)
{
Span<byte> buffer = stackalloc byte[4];
Assert.True(memory.TryRead(address, buffer));
return BinaryPrimitives.ReadUInt32LittleEndian(buffer);
}
private static ulong ReadUInt64(FakeCpuMemory memory, ulong address)
{
Span<byte> buffer = stackalloc byte[8];
Assert.True(memory.TryRead(address, buffer));
return BinaryPrimitives.ReadUInt64LittleEndian(buffer);
}
private static void WriteUInt32(FakeCpuMemory memory, ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[4];
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
Assert.True(memory.TryWrite(address, buffer));
}
private static void WriteUInt64(FakeCpuMemory memory, ulong address, ulong value)
{
Span<byte> buffer = stackalloc byte[8];
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
Assert.True(memory.TryWrite(address, buffer));
}
}
@@ -0,0 +1,61 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.HLE;
using SharpEmu.Libs.Agc;
using Xunit;
namespace SharpEmu.Libs.Tests.Agc;
public sealed class AgcResourceOwnerTests
{
private const ulong BaseAddress = 0x1_0000_0000;
private const ulong OwnerAddress = BaseAddress + 0x100;
private const ulong NameAddress = BaseAddress + 0x200;
private const ulong RegistrationMemoryAddress = BaseAddress + 0x400;
[Fact]
public void RegisterOwner_DoesNotRequireOptionalResourceRegistryMemory()
{
var memory = new FakeCpuMemory(BaseAddress, 0x2000);
var ctx = new CpuContext(memory, Generation.Gen5);
memory.WriteCString(NameAddress, "GIRender");
ctx[CpuRegister.Rdi] = OwnerAddress;
ctx[CpuRegister.Rsi] = NameAddress;
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, AgcExports.DriverRegisterOwner(ctx));
Assert.NotEqual(0u, ReadUInt32(memory, OwnerAddress));
}
[Fact]
public void RegisterOwner_RespectsExplicitRegistryCapacity()
{
var memory = new FakeCpuMemory(BaseAddress, 0x2000);
var ctx = new CpuContext(memory, Generation.Gen5);
ctx[CpuRegister.Rdi] = RegistrationMemoryAddress;
ctx[CpuRegister.Rsi] = 0x1000;
ctx[CpuRegister.Rdx] = 1;
Assert.Equal(
(int)OrbisGen2Result.ORBIS_GEN2_OK,
AgcExports.DriverInitResourceRegistration(ctx));
memory.WriteCString(NameAddress, "First");
ctx[CpuRegister.Rdi] = OwnerAddress;
ctx[CpuRegister.Rsi] = NameAddress;
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, AgcExports.DriverRegisterOwner(ctx));
memory.WriteCString(NameAddress, "Second");
ctx[CpuRegister.Rdi] = OwnerAddress + 4;
Assert.Equal(
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT,
AgcExports.DriverRegisterOwner(ctx));
}
private static uint ReadUInt32(FakeCpuMemory memory, ulong address)
{
Span<byte> buffer = stackalloc byte[4];
Assert.True(memory.TryRead(address, buffer));
return BinaryPrimitives.ReadUInt32LittleEndian(buffer);
}
}
@@ -0,0 +1,133 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.HLE;
using SharpEmu.Libs.Agc;
using Xunit;
namespace SharpEmu.Libs.Tests.Agc;
public sealed class AgcWaitRegMemTests
{
private const ulong BaseAddress = 0x1_0000_0000;
private const ulong CommandBufferAddress = BaseAddress + 0x100;
private const ulong PacketAddress = BaseAddress + 0x400;
private const ulong StackAddress = BaseAddress + 0x800;
[Fact]
public void DcbWaitRegMem32_EmitsGen5PacketLayout()
{
var memory = CreateMemory(out var ctx);
var waitAddress = BaseAddress + 0xC03;
ctx[CpuRegister.Rdi] = CommandBufferAddress;
ctx[CpuRegister.Rsi] = 0;
ctx[CpuRegister.Rdx] = 3;
ctx[CpuRegister.Rcx] = 4;
ctx[CpuRegister.R8] = 2;
ctx[CpuRegister.R9] = waitAddress;
WriteUInt64(memory, StackAddress + 8, 0x1122_3344_5566_7788);
WriteUInt64(memory, StackAddress + 16, 0xAABB_CCDD_EEFF_0011);
WriteUInt32(memory, StackAddress + 24, 0x123456);
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, AgcExports.DcbWaitRegMem(ctx));
Assert.Equal(PacketAddress, ctx[CpuRegister.Rax]);
Assert.Equal(0xC005_1028u, ReadUInt32(memory, PacketAddress));
Assert.Equal(0x0000_0C00u, ReadUInt32(memory, PacketAddress + 4));
Assert.Equal(1u, ReadUInt32(memory, PacketAddress + 8));
Assert.Equal(0xEEFF_0011u, ReadUInt32(memory, PacketAddress + 12));
Assert.Equal(0x5566_7788u, ReadUInt32(memory, PacketAddress + 16));
Assert.Equal(0x0400_0053u, ReadUInt32(memory, PacketAddress + 20));
Assert.Equal(0xFFFFu, ReadUInt32(memory, PacketAddress + 24));
Assert.Equal(PacketAddress + 28, ReadUInt64(memory, CommandBufferAddress + 0x10));
}
[Fact]
public void DcbWaitRegMem64_EmitsGen5PacketLayout()
{
var memory = CreateMemory(out var ctx);
var waitAddress = BaseAddress + 0xC07;
ctx[CpuRegister.Rdi] = CommandBufferAddress;
ctx[CpuRegister.Rsi] = 1;
ctx[CpuRegister.Rdx] = 6;
ctx[CpuRegister.Rcx] = 3;
ctx[CpuRegister.R8] = 1;
ctx[CpuRegister.R9] = waitAddress;
WriteUInt64(memory, StackAddress + 8, 0x1122_3344_5566_7788);
WriteUInt64(memory, StackAddress + 16, 0xAABB_CCDD_EEFF_0011);
WriteUInt32(memory, StackAddress + 24, 0x320);
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, AgcExports.DcbWaitRegMem(ctx));
Assert.Equal(0xC007_1058u, ReadUInt32(memory, PacketAddress));
Assert.Equal(0x0000_0C00u, ReadUInt32(memory, PacketAddress + 4));
Assert.Equal(1u, ReadUInt32(memory, PacketAddress + 8));
Assert.Equal(0xEEFF_0011u, ReadUInt32(memory, PacketAddress + 12));
Assert.Equal(0xAABB_CCDDu, ReadUInt32(memory, PacketAddress + 16));
Assert.Equal(0x5566_7788u, ReadUInt32(memory, PacketAddress + 20));
Assert.Equal(0x1122_3344u, ReadUInt32(memory, PacketAddress + 24));
Assert.Equal(0x0200_0156u, ReadUInt32(memory, PacketAddress + 28));
Assert.Equal(0x32u, ReadUInt32(memory, PacketAddress + 32));
}
[Fact]
public void WaitRegMemPatchFunctions_UseGen5Fields()
{
var memory = CreateMemory(out var ctx);
WriteUInt32(memory, PacketAddress, 0xC005_1028);
WriteUInt32(memory, PacketAddress + 20, 0x0400_0153);
ctx[CpuRegister.Rdi] = PacketAddress;
ctx[CpuRegister.Rsi] = BaseAddress + 0xD07;
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, AgcExports.WaitRegMemPatchAddress(ctx));
Assert.Equal(0x0000_0D04u, ReadUInt32(memory, PacketAddress + 4));
Assert.Equal(1u, ReadUInt32(memory, PacketAddress + 8));
ctx[CpuRegister.Rsi] = 5;
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, AgcExports.WaitRegMemPatchCompareFunction(ctx));
Assert.Equal(0x0400_0155u, ReadUInt32(memory, PacketAddress + 20));
ctx[CpuRegister.Rsi] = 0xDEAD_BEEF;
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, AgcExports.WaitRegMemPatchReference(ctx));
Assert.Equal(0xDEAD_BEEFu, ReadUInt32(memory, PacketAddress + 16));
}
private static FakeCpuMemory CreateMemory(out CpuContext ctx)
{
var memory = new FakeCpuMemory(BaseAddress, 0x2000);
ctx = new CpuContext(memory, Generation.Gen5);
ctx[CpuRegister.Rsp] = StackAddress;
WriteUInt64(memory, CommandBufferAddress + 0x10, PacketAddress);
WriteUInt64(memory, CommandBufferAddress + 0x18, PacketAddress + 0x100);
return memory;
}
private static uint ReadUInt32(FakeCpuMemory memory, ulong address)
{
Span<byte> buffer = stackalloc byte[4];
Assert.True(memory.TryRead(address, buffer));
return BinaryPrimitives.ReadUInt32LittleEndian(buffer);
}
private static ulong ReadUInt64(FakeCpuMemory memory, ulong address)
{
Span<byte> buffer = stackalloc byte[8];
Assert.True(memory.TryRead(address, buffer));
return BinaryPrimitives.ReadUInt64LittleEndian(buffer);
}
private static void WriteUInt32(FakeCpuMemory memory, ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[4];
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
Assert.True(memory.TryWrite(address, buffer));
}
private static void WriteUInt64(FakeCpuMemory memory, ulong address, ulong value)
{
Span<byte> buffer = stackalloc byte[8];
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
Assert.True(memory.TryWrite(address, buffer));
}
}
@@ -37,8 +37,12 @@ public sealed class GnmTilingDetileTests
return offset;
}
[Fact]
public void TryDetile_ExactXorMode27_MatchesReferenceAddressEquation()
[Theory]
[InlineData(384, 200)]
[InlineData(768, 512)]
public void TryDetile_ExactXorMode27_MatchesReferenceAddressEquation(
int elementsWide,
int elementsHigh)
{
const uint swizzleMode = 27; // 64 KiB RB+ R_X
const int bytesPerElement = 2;
@@ -46,8 +50,6 @@ public sealed class GnmTilingDetileTests
// SquareBlockDimensions(32768 elements): 15 bits split 8/7, x favored.
const int blockWidth = 256;
const int blockHeight = 128;
const int elementsWide = 384; // spans two block columns and a partial third
const int elementsHigh = 200;
var blocksPerRow = (elementsWide + blockWidth - 1) / blockWidth;
var blocksPerColumn = (elementsHigh + blockHeight - 1) / blockHeight;
@@ -99,6 +99,25 @@ public sealed class AvPlayerPathTests : IDisposable
AvPlayerExports.GetFfprobePath(ffmpeg, isWindows));
}
[Theory]
[InlineData(false, "ffmpeg")]
[InlineData(true, "ffmpeg.exe")]
public void MediaToolLookupFindsPackagedBinary(bool isWindows, string executable)
{
var publishDirectory = Path.Combine(_tempRoot, "publish");
Directory.CreateDirectory(Path.Combine(publishDirectory, "ffmpeg"));
var ffmpeg = Path.Combine(publishDirectory, "ffmpeg", executable);
File.WriteAllBytes(ffmpeg, []);
Assert.Equal(
ffmpeg,
AvPlayerExports.FindFfmpeg(
configured: null,
searchPath: null,
isWindows,
publishDirectory));
}
[Fact]
public void RelativeFileUriCannotEscapeApp0()
{
@@ -0,0 +1,79 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.Libs.Bink;
using Xunit;
namespace SharpEmu.Libs.Tests.Bink;
public sealed class Bink2MovieBridgeTests : IDisposable
{
private readonly string _tempDirectory = Path.Combine(
Path.GetTempPath(),
$"sharpemu-bink-{Guid.NewGuid():N}");
public Bink2MovieBridgeTests()
{
Directory.CreateDirectory(_tempDirectory);
}
[Fact]
public void HeaderPreservesFractionalFrameRate()
{
var path = WriteHeader("KB2j"u8, 3840, 2160, 30_000, 1_001);
Assert.True(Bink2MovieBridge.TryReadBinkInfo(path, out var info));
Assert.Equal(3840u, info.Width);
Assert.Equal(2160u, info.Height);
Assert.Equal(30_000u, info.FramesPerSecondNumerator);
Assert.Equal(1_001u, info.FramesPerSecondDenominator);
}
[Theory]
[InlineData("KB2g")]
[InlineData("KB2i")]
[InlineData("KB2j")]
public void HeaderAcceptsBink2Revisions(string signature)
{
var path = WriteHeader(
System.Text.Encoding.ASCII.GetBytes(signature),
1920,
1080,
60,
1);
Assert.True(Bink2MovieBridge.TryReadBinkInfo(path, out _));
}
[Fact]
public void HeaderRejectsMissingFrameRateDenominator()
{
var path = WriteHeader("KB2j"u8, 1920, 1080, 60, 0);
Assert.False(Bink2MovieBridge.TryReadBinkInfo(path, out _));
}
private string WriteHeader(
ReadOnlySpan<byte> signature,
uint width,
uint height,
uint fpsNumerator,
uint fpsDenominator)
{
var header = new byte[36];
signature.CopyTo(header);
BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(0x14), width);
BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(0x18), height);
BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(0x1C), fpsNumerator);
BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(0x20), fpsDenominator);
var path = Path.Combine(_tempDirectory, $"{Guid.NewGuid():N}.bk2");
File.WriteAllBytes(path, header);
return path;
}
public void Dispose()
{
Directory.Delete(_tempDirectory, recursive: true);
}
}
@@ -0,0 +1,105 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Libs.Bink;
using Xunit;
namespace SharpEmu.Libs.Tests.Bink;
public sealed class BinkFramePlaybackTests
{
[Fact]
public void FramesAdvanceAccordingToMovieClock()
{
using var playback = new BinkFramePlayback(new SequenceDecoder(1, 2, 3));
Assert.Equal(1, WaitForAdvancedFrame(playback)[0]);
Assert.True(playback.TryGetFrame(true, out var heldFrame, out var advanced));
Assert.False(advanced);
Assert.Equal(1, heldFrame[0]);
Assert.Equal(2, WaitForAdvancedFrame(playback)[0]);
Assert.Equal(3, WaitForAdvancedFrame(playback)[0]);
}
private static byte[] WaitForAdvancedFrame(BinkFramePlayback playback)
{
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(2);
while (DateTime.UtcNow < deadline)
{
if (playback.TryGetFrame(true, out var frame, out var advanced) && advanced)
{
return frame;
}
Thread.Sleep(1);
}
throw new TimeoutException("The decoder did not produce a frame.");
}
[Fact]
public void FirstFrameWaitsUntilPresentationStarts()
{
using var playback = new BinkFramePlayback(new SequenceDecoder(1, 2));
var first = WaitForFrame(playback, advanceClock: false);
Assert.Equal(1, first[0]);
Thread.Sleep(100);
Assert.True(playback.TryGetFrame(false, out var held, out var advanced));
Assert.False(advanced);
Assert.Equal(1, held[0]);
Assert.True(playback.TryGetFrame(true, out held, out advanced));
Assert.False(advanced);
Assert.Equal(1, held[0]);
Assert.Equal(2, WaitForAdvancedFrame(playback)[0]);
}
private static byte[] WaitForFrame(
BinkFramePlayback playback,
bool advanceClock)
{
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(2);
while (DateTime.UtcNow < deadline)
{
if (playback.TryGetFrame(advanceClock, out var frame, out _))
{
return frame;
}
Thread.Sleep(1);
}
throw new TimeoutException("The decoder did not produce a frame.");
}
private sealed class SequenceDecoder(params byte[] values) : IBinkFrameDecoder
{
private int _index;
public uint Width => 1;
public uint Height => 1;
public uint FramesPerSecondNumerator => 20;
public uint FramesPerSecondDenominator => 1;
public bool TryDecodeNextFrame(Span<byte> destination)
{
if (_index >= values.Length)
{
return false;
}
destination.Fill(values[_index++]);
return true;
}
public void Dispose()
{
}
}
}
@@ -301,6 +301,38 @@ public sealed class KernelMemoryCompatExportsTests
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, result);
}
[Fact]
public void VirtualQuery_PreservesReservationPastFixedCommitAtSameBase()
{
const ulong memoryBase = 0x12_0000_0000;
const ulong reservedLength = 0x1_0000;
const ulong committedLength = 0x2000;
const ulong inOutAddress = memoryBase + 0x1_7000;
const ulong infoAddress = memoryBase + 0x1_8000;
var memory = new FakeCpuMemory(memoryBase, 0x2_0000);
var context = new CpuContext(memory, Generation.Gen5);
KernelMemoryCompatExports.RegisterReservedVirtualRange(memoryBase, reservedLength);
Assert.True(memory.TryWrite(inOutAddress, BitConverter.GetBytes(memoryBase)));
context[CpuRegister.Rdi] = inOutAddress;
context[CpuRegister.Rsi] = committedLength;
context[CpuRegister.Rdx] = 0x03;
context[CpuRegister.Rcx] = 0x10; // fixed mapping
Assert.Equal(0, KernelMemoryCompatExports.KernelMapNamedFlexibleMemory(context));
context[CpuRegister.Rdi] = memoryBase + 0x8000;
context[CpuRegister.Rsi] = 0;
context[CpuRegister.Rdx] = infoAddress;
context[CpuRegister.Rcx] = 0x48;
Assert.Equal(0, KernelMemoryCompatExports.KernelVirtualQuery(context));
Assert.True(context.TryReadUInt64(infoAddress, out var regionStart));
Assert.True(context.TryReadUInt64(infoAddress + 8, out var regionEnd));
Assert.Equal(memoryBase + committedLength, regionStart);
Assert.Equal(memoryBase + reservedLength, regionEnd);
}
[Fact]
public void Mprotect_ZeroAddressReturnsInvalidArgument()
{