420 lines
13 KiB
C#
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());
|
|
}
|
|
}
|
|
}
|