sharpemu/tests/SharpEmu.Libs.Tests/AvPlayer/AvPlayerAllocationTests.cs

420 lines
13 KiB
C#

// 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 System.Text;
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 async Task FallbackPollDoesNotWaitForThePlayerLock()
{
var gate = typeof(AvPlayerExports).GetField(
"StateGate", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!.GetValue(null)!;
using var locked = new ManualResetEventSlim();
using var release = new ManualResetEventSlim();
var owner = new Thread(() =>
{
lock (gate)
{
locked.Set();
release.Wait();
}
});
owner.Start();
try
{
Assert.True(locked.Wait(TimeSpan.FromSeconds(5)));
var poll = Task.Run(() =>
{
var found = AvPlayerExports.TryGetFallbackPresentationFrame(
out var pixels, out var width, out var height, out var serial);
return (found, pixels, width, height, serial);
});
var result = await poll.WaitAsync(TimeSpan.FromSeconds(5));
Assert.False(result.found);
Assert.Empty(result.pixels);
Assert.Equal((0u, 0u, 0L), (result.width, result.height, result.serial));
}
finally
{
release.Set();
owner.Join();
}
}
[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);
}
[Fact]
public void ReplacementFileCallbacksMaterializeOffsetReadsAndCleanUp()
{
using var memory = new PhysicalVirtualMemory();
var context = new CpuContext(memory, Generation.Gen5);
var payload = new byte[(1024 * 1024) + 257];
for (var index = 0; index < payload.Length; index++)
{
payload[index] = unchecked((byte)(index * 37));
}
var scheduler = new ReplacementFileScheduler(payload);
GuestThreadExecution.Scheduler = scheduler;
AvPlayerExports.RegisterPlayerForTest(
Handle,
width: 16,
height: 16,
durationMilliseconds: 1,
fileObject: 0xCAFE,
fileOpenCallback: ReplacementFileScheduler.OpenCallback,
fileCloseCallback: ReplacementFileScheduler.CloseCallback,
fileReadOffsetCallback: ReplacementFileScheduler.ReadCallback,
fileSizeCallback: ReplacementFileScheduler.SizeCallback);
Assert.True(AvPlayerExports.MaterializeReplacementSourceForTest(
context,
Handle,
"archive:/packed.resource",
out var path));
Assert.Equal("archive:/packed.resource", scheduler.OpenedPath);
Assert.Equal(payload, File.ReadAllBytes(path));
Assert.Equal(2, scheduler.ReadCount);
Assert.Equal(1, scheduler.CloseCount);
AvPlayerExports.RemovePlayerForTest(Handle);
Assert.False(File.Exists(path));
}
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 HasPendingGuestExceptionForCurrentThread() => false;
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 TryCallGuestFunction(
CpuContext callerContext,
ulong entryPoint,
ulong arg0,
ulong arg1,
ulong arg2,
ulong arg3,
ulong stackAddress,
ulong stackSize,
string reason,
out ulong returnValue,
out string? error) =>
TryCallGuestFunction(
callerContext,
entryPoint,
arg0,
arg1,
arg2,
stackAddress,
stackSize,
reason,
out returnValue,
out error);
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;
}
}
private sealed class ReplacementFileScheduler(byte[] payload) : IGuestThreadScheduler
{
public const ulong OpenCallback = 0x10_0000;
public const ulong CloseCallback = 0x10_0100;
public const ulong ReadCallback = 0x10_0200;
public const ulong SizeCallback = 0x10_0300;
public string? OpenedPath { get; private set; }
public int ReadCount { get; private set; }
public int CloseCount { 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 HasPendingGuestExceptionForCurrentThread() => false;
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 = "unsupported callback shape";
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) =>
TryCallGuestFunction(
callerContext,
entryPoint,
arg0,
arg1,
arg2,
0,
stackAddress,
stackSize,
reason,
out returnValue,
out error);
public bool TryCallGuestFunction(
CpuContext callerContext,
ulong entryPoint,
ulong arg0,
ulong arg1,
ulong arg2,
ulong arg3,
ulong stackAddress,
ulong stackSize,
string reason,
out ulong returnValue,
out string? error)
{
error = null;
switch (entryPoint)
{
case OpenCallback:
OpenedPath = ReadGuestString(callerContext, arg1);
returnValue = 0;
return true;
case CloseCallback:
CloseCount++;
returnValue = 0;
return true;
case SizeCallback:
returnValue = checked((ulong)payload.Length);
return true;
case ReadCallback:
var position = checked((int)arg2);
if (position < 0 || position >= payload.Length)
{
returnValue = 0;
return true;
}
var count = Math.Min(checked((int)arg3), payload.Length - position);
if (count <= 0 ||
!callerContext.Memory.TryWrite(arg1, payload.AsSpan(position, count)))
{
returnValue = unchecked((ulong)-1L);
return true;
}
ReadCount++;
returnValue = checked((ulong)count);
return true;
default:
returnValue = 0;
error = "unknown callback";
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;
}
private static string ReadGuestString(CpuContext context, ulong address)
{
var bytes = new List<byte>();
Span<byte> value = stackalloc byte[1];
while (context.Memory.TryRead(address++, value) && value[0] != 0)
{
bytes.Add(value[0]);
}
return Encoding.UTF8.GetString(bytes.ToArray());
}
}
}