mirror of
https://github.com/par274/sharpemu.git
synced 2026-08-19 16:01:30 +08:00
A vplayer fix (#821)
* Astrobot-video-fix * Astrobot-video-fix with updated files * Astrobot-video-fix with tests files
This commit is contained in:
@@ -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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user