A vplayer fix (#821)

* Astrobot-video-fix

* Astrobot-video-fix with updated files

* Astrobot-video-fix with tests files
This commit is contained in:
Astell
2026-08-16 23:57:42 +02:00
committed by GitHub
parent 7caf430aa9
commit 1660111189
8 changed files with 1280 additions and 75 deletions
@@ -0,0 +1,147 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Diagnostics;
using SharpEmu.HLE;
using SharpEmu.Libs.AvPlayer;
using Xunit;
namespace SharpEmu.Libs.Tests.AvPlayer;
public sealed class AvPlayerAbiTests
{
[Theory]
[InlineData(Generation.Gen4, false, 108UL)]
[InlineData(Generation.Gen5, false, 112UL)]
[InlineData(Generation.Gen4, true, 164UL)]
[InlineData(Generation.Gen5, true, 168UL)]
public void InitAutoStartOffsetMatchesGeneration(
Generation generation,
bool extended,
ulong expected)
{
Assert.Equal(expected, AvPlayerExports.GetAutoStartOffset(generation, extended));
}
[Theory]
[InlineData(Generation.Gen4, 40)]
[InlineData(Generation.Gen5, 32)]
public void LegacyStreamInfoSizeMatchesGeneration(
Generation generation,
int expected)
{
Assert.Equal(expected, AvPlayerExports.GetLegacyStreamInfoSize(generation));
}
[Theory]
[InlineData(Generation.Gen4, 0u, 0u)]
[InlineData(Generation.Gen4, 1u, 1u)]
[InlineData(Generation.Gen5, 0u, 1u)]
[InlineData(Generation.Gen5, 1u, 2u)]
public void StreamTypeMatchesGeneration(
Generation generation,
uint streamIndex,
uint expected)
{
Assert.Equal(expected, AvPlayerExports.GetStreamType(generation, streamIndex));
}
[Fact]
public void Gen5FrameInfoExCarriesPitchCropAndFrameRate()
{
var info = new byte[104];
AvPlayerExports.WriteVideoFrameInfo(
info,
Generation.Gen5,
extended: true,
bufferAddress: 0x1234_5000,
timestamp: 2_903,
width: 512,
visibleWidth: 378,
height: 150,
pitch: 512,
framesPerSecond: 29.97);
Assert.Equal(0x1234_5000UL, BinaryPrimitives.ReadUInt64LittleEndian(info));
Assert.Equal(2_903UL, BinaryPrimitives.ReadUInt64LittleEndian(info.AsSpan(16)));
Assert.Equal(512u, BinaryPrimitives.ReadUInt32LittleEndian(info.AsSpan(24)));
Assert.Equal(150u, BinaryPrimitives.ReadUInt32LittleEndian(info.AsSpan(28)));
Assert.Equal(134u, BinaryPrimitives.ReadUInt32LittleEndian(info.AsSpan(48)));
Assert.Equal(512u, BinaryPrimitives.ReadUInt32LittleEndian(info.AsSpan(60)));
Assert.Equal(8, info[64]);
Assert.Equal(8, info[65]);
Assert.Equal(29.97, BinaryPrimitives.ReadDoubleLittleEndian(info.AsSpan(0x48)));
}
[Fact]
public void FallbackFrameValidationUsesTheDecodedDimensions()
{
var fullHdFrame = new byte[1920 * 1080 * 4];
Assert.True(AvPlayerExports.IsValidBgraFrame(fullHdFrame, 1920, 1080));
Assert.False(AvPlayerExports.IsValidBgraFrame(fullHdFrame, 3840, 2160));
Assert.False(AvPlayerExports.IsValidBgraFrame(fullHdFrame, 0, 1080));
}
[Fact]
public void PosterSuppressesTheDuplicateFirstHostDecodedFrame()
{
var skipFirstDecodedFrame = true;
Assert.False(AvPlayerExports.ShouldPublishFallbackPlaybackFrame(
advanced: true,
hasPresentation: true,
ref skipFirstDecodedFrame));
Assert.False(skipFirstDecodedFrame);
Assert.True(AvPlayerExports.ShouldPublishFallbackPlaybackFrame(
advanced: true,
hasPresentation: true,
ref skipFirstDecodedFrame));
}
[Theory]
[InlineData(false, false, false)]
[InlineData(false, true, false)]
[InlineData(true, false, false)]
[InlineData(true, true, true)]
public void CompletedFallbackIsReleasedAtGuestEndOfStream(
bool fallbackCompleted,
bool guestEndOfStream,
bool expected)
{
var completedTicks = Stopwatch.GetTimestamp();
Assert.Equal(
expected,
AvPlayerExports.ShouldReleaseCompletedFallback(
fallbackCompleted,
guestEndOfStream,
completedTicks,
completedTicks));
}
/// <summary>
/// A title that pauses its player after the poster frame never reaches end
/// of stream, so the hold must expire on its own; otherwise the last movie
/// image stays pinned over everything the game renders next.
/// </summary>
[Fact]
public void CompletedFallbackHoldExpiresWithoutGuestEndOfStream()
{
var completedTicks = Stopwatch.GetTimestamp();
Assert.False(
AvPlayerExports.ShouldReleaseCompletedFallback(
fallbackPlaybackCompleted: true,
guestEndOfStream: false,
completedTicks,
completedTicks + (Stopwatch.Frequency / 10)));
Assert.True(
AvPlayerExports.ShouldReleaseCompletedFallback(
fallbackPlaybackCompleted: true,
guestEndOfStream: false,
completedTicks,
completedTicks + (Stopwatch.Frequency * 2)));
}
}
@@ -0,0 +1,142 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Memory;
using SharpEmu.HLE;
using SharpEmu.Libs.AvPlayer;
using SharpEmu.Libs.Tests.Kernel;
using Xunit;
namespace SharpEmu.Libs.Tests.AvPlayer;
[Collection(KernelMemoryCompatStateCollection.Name)]
public sealed class AvPlayerAllocationTests : IDisposable
{
private const ulong Handle = 0xA0_0000_1000;
private readonly IGuestThreadScheduler? _previousScheduler = GuestThreadExecution.Scheduler;
[Fact]
public void FailedGuestAllocatorsFallBackToHleMemoryInTheSameAttempt()
{
using var memory = new PhysicalVirtualMemory();
var context = new CpuContext(memory, Generation.Gen5);
var scheduler = new FailingAllocatorScheduler();
GuestThreadExecution.Scheduler = scheduler;
AvPlayerExports.RegisterPlayerForTest(
Handle,
width: 16,
height: 16,
durationMilliseconds: 1,
allocateTextureCallback: 0x1000,
allocateCallback: 0x2000);
Assert.True(AvPlayerExports.AllocateGuestVideoBuffersForTest(
context,
Handle,
out var firstBuffer));
Assert.NotEqual(0UL, firstBuffer);
Assert.Equal(2, scheduler.CallCount);
}
public void Dispose()
{
AvPlayerExports.RemovePlayerForTest(Handle);
GuestThreadExecution.Scheduler = _previousScheduler;
}
private sealed class FailingAllocatorScheduler : IGuestThreadScheduler
{
public int CallCount { get; private set; }
public bool SupportsGuestContextTransfer => false;
public void RegisterGuestThreadContext(ulong threadHandle, CpuContext context)
{
}
public bool TryStartThread(
CpuContext creatorContext,
GuestThreadStartRequest request,
out string? error)
{
error = "not supported";
return false;
}
public bool TryJoinThread(
CpuContext callerContext,
ulong threadHandle,
out ulong returnValue,
out string? error)
{
returnValue = 0;
error = "not supported";
return false;
}
public void Pump(CpuContext callerContext, string reason)
{
}
public int WakeBlockedThreads(string wakeKey, int maxCount = int.MaxValue) => 0;
public bool TrySetGuestThreadPriority(ulong guestThreadHandle, int guestPriority) => false;
public bool TrySetGuestThreadAffinity(ulong guestThreadHandle, ulong affinityMask) => false;
public IReadOnlyList<GuestThreadSnapshot> SnapshotThreads() => [];
public bool TryCallGuestFunction(
CpuContext callerContext,
ulong entryPoint,
ulong arg0,
ulong arg1,
ulong stackAddress,
ulong stackSize,
string reason,
out string? error)
{
error = "not supported";
return false;
}
public bool TryCallGuestFunction(
CpuContext callerContext,
ulong entryPoint,
ulong arg0,
ulong arg1,
ulong arg2,
ulong stackAddress,
ulong stackSize,
string reason,
out ulong returnValue,
out string? error)
{
CallCount++;
returnValue = 0;
error = "allocator rejected the request";
return false;
}
public bool TryCallGuestContinuation(
CpuContext callerContext,
GuestCpuContinuation continuation,
string reason,
out string? error)
{
error = "not supported";
return false;
}
public bool TryRaiseGuestException(
CpuContext callerContext,
ulong threadHandle,
ulong handler,
int exceptionType,
out string? error)
{
error = "not supported";
return false;
}
}
}
@@ -0,0 +1,106 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Libs.AvPlayer;
using Xunit;
namespace SharpEmu.Libs.Tests.AvPlayer;
public sealed class AvPlayerNv12LayoutTests
{
[Theory]
[InlineData(1920, 2048)]
[InlineData(3840, 3840)]
[InlineData(4097, 4352)]
public void CalculateNv12Pitch_AlignsTo256Bytes(int width, int expectedPitch)
{
Assert.Equal(expectedPitch, AvPlayerExports.CalculateNv12Pitch(width));
}
[Fact]
public void CalculateNv12BufferSize_IncludesBothPlanesAtTheAlignedPitch()
{
Assert.Equal(3_317_760, AvPlayerExports.CalculateNv12BufferSize(2048, 1080));
}
[Fact]
public void ConvertNv12ToBgra_UsesTheInterleavedChromaPlaneAndOpaqueAlpha()
{
byte[] nv12 =
[
16, 235,
81, 145,
128, 128,
];
var bgra = new byte[2 * 2 * 4];
AvPlayerExports.ConvertNv12ToBgra(
nv12,
pitch: 2,
bufferHeight: 2,
width: 2,
height: 2,
bgra);
Assert.Equal(
new byte[]
{
0, 0, 0, 255,
255, 255, 255, 255,
76, 76, 76, 255,
150, 150, 150, 255,
},
bgra);
}
[Fact]
public void CopyNv12ToGuestBuffer_UsesSourceStridesAndPitchedUvOffset()
{
const int width = 4;
const int height = 4;
const int sourceLumaStride = 6;
const int sourceChromaStride = 8;
const int destinationPitch = 8;
var source = Enumerable.Repeat((byte)0xEE, 40).ToArray();
for (var row = 0; row < height; row++)
{
for (var column = 0; column < width; column++)
{
source[(row * sourceLumaStride) + column] = checked((byte)(1 + (row * 10) + column));
}
}
var sourceChromaOffset = sourceLumaStride * height;
for (var row = 0; row < height / 2; row++)
{
for (var column = 0; column < width; column++)
{
source[sourceChromaOffset + (row * sourceChromaStride) + column] =
checked((byte)(101 + (row * 10) + column));
}
}
var destination = Enumerable.Repeat((byte)0xCC, 48).ToArray();
AvPlayerExports.CopyNv12ToGuestBuffer(
source,
destination,
width,
height,
sourceLumaStride,
sourceChromaStride,
destinationPitch);
var expected = new byte[48];
for (var row = 0; row < height; row++)
{
source.AsSpan(row * sourceLumaStride, width)
.CopyTo(expected.AsSpan(row * destinationPitch, width));
}
var destinationChromaOffset = destinationPitch * height;
for (var row = 0; row < height / 2; row++)
{
source.AsSpan(sourceChromaOffset + (row * sourceChromaStride), width)
.CopyTo(expected.AsSpan(destinationChromaOffset + (row * destinationPitch), width));
}
Assert.Equal(expected, destination);
}
}
@@ -19,36 +19,39 @@ public sealed class AvPlayerStreamInfoTests
private const byte Sentinel = 0xAB;
[Theory]
[InlineData(false, 0u)]
[InlineData(true, 0u)]
[InlineData(false, 1u)]
[InlineData(true, 1u)]
public void GetStreamInfoFunctionsDoNotWritePastThe32ByteStructure(
bool useExtendedFunction,
uint streamIndex)
[InlineData(Generation.Gen5, 0u, 32, 1u)]
[InlineData(Generation.Gen5, 1u, 32, 2u)]
[InlineData(Generation.Gen4, 0u, 40, 0u)]
[InlineData(Generation.Gen4, 1u, 40, 1u)]
public void GetStreamInfoUsesTheGenerationSpecificLayout(
Generation generation,
uint streamIndex,
int structureSize,
uint expectedStreamType)
{
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
var context = new CpuContext(memory, Generation.Gen5);
var context = new CpuContext(memory, generation);
AvPlayerExports.RegisterPlayerForTest(
Handle,
1280,
720,
DurationMilliseconds,
hasAudio: true);
AvPlayerExports.RegisterPlayerForTest(Handle, 1280, 720, DurationMilliseconds);
try
{
Span<byte> window = stackalloc byte[40];
Span<byte> window = stackalloc byte[48];
window.Fill(Sentinel);
Assert.True(memory.TryWrite(InfoAddress, window));
context[CpuRegister.Rdi] = Handle;
context[CpuRegister.Rsi] = streamIndex;
context[CpuRegister.Rdx] = InfoAddress;
Assert.Equal(0, AvPlayerExports.AvPlayerGetStreamInfo(context));
var resultCode = useExtendedFunction
? AvPlayerExports.AvPlayerGetStreamInfoEx(context)
: AvPlayerExports.AvPlayerGetStreamInfo(context);
Assert.Equal(0, resultCode);
Span<byte> result = stackalloc byte[40];
Span<byte> result = stackalloc byte[48];
Assert.True(memory.TryRead(InfoAddress, result));
Assert.Equal(streamIndex, BinaryPrimitives.ReadUInt32LittleEndian(result));
Assert.Equal(expectedStreamType, BinaryPrimitives.ReadUInt32LittleEndian(result));
if (streamIndex == 0)
{
Assert.Equal(1280u, BinaryPrimitives.ReadUInt32LittleEndian(result[8..]));
@@ -61,7 +64,71 @@ public sealed class AvPlayerStreamInfoTests
}
Assert.Equal(DurationMilliseconds, BinaryPrimitives.ReadUInt64LittleEndian(result[24..]));
for (var index = 32; index < result.Length; index++)
for (var index = structureSize; index < result.Length; index++)
{
Assert.Equal(Sentinel, result[index]);
}
}
finally
{
AvPlayerExports.RemovePlayerForTest(Handle);
}
}
[Fact]
public void StreamInfoRejectsAudioIndexForVideoOnlyMedia()
{
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
var context = new CpuContext(memory, Generation.Gen5);
AvPlayerExports.RegisterPlayerForTest(Handle, 1280, 720, DurationMilliseconds);
try
{
context[CpuRegister.Rdi] = Handle;
context[CpuRegister.Rsi] = 1;
context[CpuRegister.Rdx] = InfoAddress;
Assert.NotEqual(0, AvPlayerExports.AvPlayerGetStreamInfo(context));
Assert.NotEqual(0, AvPlayerExports.AvPlayerGetStreamInfoEx(context));
}
finally
{
AvPlayerExports.RemovePlayerForTest(Handle);
}
}
[Fact]
public void GetStreamInfoExWritesThe104ByteGen5Descriptor()
{
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
var context = new CpuContext(memory, Generation.Gen5);
AvPlayerExports.RegisterPlayerForTest(
Handle,
378,
150,
DurationMilliseconds,
framesPerSecond: 29.97);
try
{
Span<byte> window = stackalloc byte[120];
window.Fill(Sentinel);
Assert.True(memory.TryWrite(InfoAddress, window));
context[CpuRegister.Rdi] = Handle;
context[CpuRegister.Rsi] = 0;
context[CpuRegister.Rdx] = InfoAddress;
Assert.Equal(0, AvPlayerExports.AvPlayerGetStreamInfoEx(context));
Span<byte> result = stackalloc byte[120];
Assert.True(memory.TryRead(InfoAddress, result));
Assert.Equal(104UL, BinaryPrimitives.ReadUInt64LittleEndian(result));
Assert.Equal(1u, BinaryPrimitives.ReadUInt32LittleEndian(result[8..]));
Assert.Equal(378u, BinaryPrimitives.ReadUInt32LittleEndian(result[16..]));
Assert.Equal(150u, BinaryPrimitives.ReadUInt32LittleEndian(result[20..]));
Assert.Equal(29.97, BinaryPrimitives.ReadDoubleLittleEndian(result[0x40..]));
Assert.Equal(DurationMilliseconds, BinaryPrimitives.ReadUInt64LittleEndian(result[0x60..]));
for (var index = 104; index < result.Length; index++)
{
Assert.Equal(Sentinel, result[index]);
}
@@ -79,7 +146,12 @@ public sealed class AvPlayerStreamInfoTests
{
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
var context = new CpuContext(memory, Generation.Gen5);
AvPlayerExports.RegisterPlayerForTest(Handle, 1280, 720, DurationMilliseconds);
AvPlayerExports.RegisterPlayerForTest(
Handle,
1280,
720,
DurationMilliseconds,
hasAudio: true);
try
{