[VideoOut] Add Bink2 support via FFMPEG bridge

This commit is contained in:
ParantezTech
2026-07-22 18:31:29 +03:00
parent d3600c9255
commit c9c0793059
22 changed files with 3085 additions and 310 deletions
@@ -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()
{
}
}
}