Prevent AvPlayer movie startup failures across supported hosts (#456)

* Prevent AvPlayer movie startup failures across supported hosts

* Prevent GR2 startup stalls during APR file checks and adaptive mutex self-locks.
This commit is contained in:
StealUrKill
2026-07-20 07:27:37 -05:00
committed by GitHub
parent 3574a3b145
commit 9d187dec55
7 changed files with 493 additions and 25 deletions
+90 -11
View File
@@ -17,7 +17,9 @@ public static class AvPlayerExports
private const int FrameBufferCount = 3;
private const int FrameInfoSize = 40;
private const int FrameInfoExSize = 104;
private const int StreamInfoSize = 40;
// This structure is 32 bytes. A larger write can damage the guest stack.
private const int StreamInfoSize = 32;
private const int StreamInfoExSize = 32;
private const int MaxGuestPathLength = 4096;
private static readonly object StateGate = new();
private static readonly Dictionary<ulong, PlayerState> Players = new();
@@ -404,7 +406,8 @@ public static class AvPlayerExports
ExportName = "sceAvPlayerGetStreamInfoEx",
Target = Generation.Gen5,
LibraryName = "libSceAvPlayer")]
public static int AvPlayerSetDecoderMode(CpuContext ctx) => ValidatePlayer(ctx);
public static int AvPlayerGetStreamInfoEx(CpuContext ctx) =>
GetStreamInfoCore(ctx, StreamInfoExSize);
[SysAbiExport(
Nid = "XC9wM+xULz8",
@@ -561,12 +564,48 @@ public static class AvPlayerExports
}
}
internal static void RegisterPlayerForTest(
ulong handle,
int width,
int height,
ulong durationMilliseconds)
{
PlayerState? previous;
lock (StateGate)
{
Players.Remove(handle, out previous);
Players[handle] = new PlayerState
{
Handle = handle,
Width = width,
Height = height,
DurationMilliseconds = durationMilliseconds,
};
}
previous?.Dispose();
}
internal static void RemovePlayerForTest(ulong handle)
{
PlayerState? player;
lock (StateGate)
{
Players.Remove(handle, out player);
}
player?.Dispose();
}
[SysAbiExport(
Nid = "d8FcbzfAdQw",
ExportName = "sceAvPlayerGetStreamInfo",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAvPlayer")]
public static int AvPlayerGetStreamInfo(CpuContext ctx)
public static int AvPlayerGetStreamInfo(CpuContext ctx) =>
GetStreamInfoCore(ctx, StreamInfoSize);
private static int GetStreamInfoCore(CpuContext ctx, int infoSize)
{
var streamIndex = unchecked((uint)ctx[CpuRegister.Rsi]);
var infoAddress = ctx[CpuRegister.Rdx];
@@ -578,7 +617,7 @@ public static class AvPlayerExports
return SetReturn(ctx, InvalidParameters);
}
Span<byte> info = stackalloc byte[StreamInfoSize];
Span<byte> info = stackalloc byte[infoSize];
info.Clear();
BinaryPrimitives.WriteUInt32LittleEndian(info[0..], streamIndex); // 0=video, 1=audio
if (streamIndex == 0)
@@ -1009,7 +1048,7 @@ public static class AvPlayerExports
{
return false;
}
var ffprobe = Path.Combine(Path.GetDirectoryName(ffmpeg) ?? string.Empty, "ffprobe");
var ffprobe = GetFfprobePath(ffmpeg, OperatingSystem.IsWindows());
if (!File.Exists(ffprobe))
{
return false;
@@ -1092,13 +1131,33 @@ public static class AvPlayerExports
}
}
private static string? FindFfmpeg()
private static string? FindFfmpeg() =>
FindFfmpeg(
Environment.GetEnvironmentVariable("SHARPEMU_FFMPEG_PATH"),
Environment.GetEnvironmentVariable("PATH"),
OperatingSystem.IsWindows());
internal static string? FindFfmpeg(
string? configured,
string? searchPath,
bool isWindows)
{
var configured = Environment.GetEnvironmentVariable("SHARPEMU_FFMPEG_PATH");
if (!string.IsNullOrWhiteSpace(configured) && File.Exists(configured))
{
return configured;
}
var executable = isWindows ? "ffmpeg.exe" : "ffmpeg";
foreach (var directory in (searchPath ?? string.Empty)
.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries))
{
var candidate = Path.Combine(RemovePathQuotes(directory), executable);
if (File.Exists(candidate))
{
return candidate;
}
}
foreach (var candidate in new[] { "/opt/homebrew/bin/ffmpeg", "/usr/local/bin/ffmpeg" })
{
if (File.Exists(candidate))
@@ -1109,6 +1168,16 @@ public static class AvPlayerExports
return null;
}
internal static string GetFfprobePath(string ffmpeg, bool isWindows) =>
Path.Combine(
Path.GetDirectoryName(ffmpeg) ?? string.Empty,
isWindows ? "ffprobe.exe" : "ffprobe");
private static string RemovePathQuotes(string directory) =>
directory.Length >= 2 && directory[0] == '"' && directory[^1] == '"'
? directory[1..^1]
: directory;
internal static string? ResolveGuestPath(string guestPath)
{
if (string.IsNullOrWhiteSpace(guestPath))
@@ -1118,7 +1187,9 @@ public static class AvPlayerExports
var normalized = guestPath.Replace('\\', '/');
var fileReference = normalized.StartsWith("file:", StringComparison.OrdinalIgnoreCase);
var unrealProjectRelative = false;
var unrealProjectRelative =
normalized.StartsWith("../", StringComparison.Ordinal) ||
normalized.StartsWith("./", StringComparison.Ordinal);
if (normalized.StartsWith("file://", StringComparison.OrdinalIgnoreCase) &&
Uri.TryCreate(normalized, UriKind.Absolute, out var uri) &&
uri.IsFile)
@@ -1149,7 +1220,10 @@ public static class AvPlayerExports
if (unrealProjectRelative)
{
normalized = RemoveUnrealLeadingDotSegments(normalized);
if (!TryRemoveUnrealLeadingDotSegments(normalized, out normalized))
{
return null;
}
}
var app0 = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
@@ -1233,15 +1307,20 @@ public static class AvPlayerExports
}
}
private static string RemoveUnrealLeadingDotSegments(string guestPath)
private static bool TryRemoveUnrealLeadingDotSegments(
string guestPath,
out string normalized)
{
var removedParent = false;
while (guestPath.StartsWith("../", StringComparison.Ordinal) ||
guestPath.StartsWith("./", StringComparison.Ordinal))
{
removedParent |= guestPath.StartsWith("../", StringComparison.Ordinal);
guestPath = guestPath[(guestPath.IndexOf('/') + 1)..];
}
return guestPath;
normalized = guestPath;
return !removedParent || guestPath.Contains('/');
}
private static bool TryDecodeFileReference(string encoded, out string decoded)
@@ -1678,6 +1678,7 @@ public static partial class KernelMemoryCompatExports
var count = ctx[CpuRegister.Rsi];
var idsAddress = ctx[CpuRegister.Rdx];
var sizesAddress = ctx[CpuRegister.Rcx];
var errorIndexAddress = ctx[CpuRegister.R8];
if (pathListAddress == 0 || count == 0 || sizesAddress == 0 || count > 1024)
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Einval);
@@ -1702,12 +1703,8 @@ public static partial class KernelMemoryCompatExports
var hostPath = ResolveGuestPath(guestPath);
if (!TryGetAprFileSize(hostPath, out var fileSize))
{
// Per-file resolve: a missing entry gets an invalid id
// (0xFFFFFFFF, already written above) and size 0, and the batch
// CONTINUES. Aborting the whole batch on the first miss left the
// remaining paths unresolved and could stall the guest's asset
// streaming when a batch happens to include an absent (e.g.
// patch/DLC) file; the caller checks per-file id/size.
// Stop at the first miss and report its index.
// The caller can then use its normal file-open fallback.
LogIoTrace("apr_resolve", guestPath, $"host='{hostPath}' index={i} count={count} result=not_found");
if (sizesAddress != 0 &&
!TryWriteUInt64Compat(ctx, sizesAddress + (i * sizeof(ulong)), 0))
@@ -1716,7 +1713,16 @@ public static partial class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
continue;
if (errorIndexAddress != 0 &&
!TryWriteUInt32Compat(ctx, errorIndexAddress, (uint)i))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
KernelRuntimeCompatExports.TrySetErrno(ctx, 2); // ENOENT
ctx[CpuRegister.Rax] = ulong.MaxValue;
return -1;
}
var fileId = AmprFileRegistry.Register(guestPath, hostPath);
@@ -771,6 +771,13 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (!tryOnly && state.Type == MutexTypeAdaptiveNp &&
IsGuestTrackedSelfLock(ctx, mutexAddress, currentThreadId))
{
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
}
if (state.Type == MutexTypeAdaptiveNp)
{
var adaptiveResult = tryOnly
@@ -816,6 +823,13 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (!tryOnly && state.Type == MutexTypeAdaptiveNp &&
IsGuestTrackedSelfLock(ctx, mutexAddress, currentThreadId))
{
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
}
if (state.Type == MutexTypeAdaptiveNp)
{
if (tryOnly)
@@ -1812,6 +1826,10 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
}
private static bool IsGuestTrackedSelfLock(CpuContext ctx, ulong mutexAddress, ulong currentThreadId) =>
KernelMemoryCompatExports.TryReadUInt64Compat(ctx, mutexAddress + 8, out var guestOwner) &&
guestOwner == currentThreadId;
private static bool CompleteCondWaiterLocked(
PthreadCondState state,
PthreadCondWaiter waiter,
@@ -90,6 +90,133 @@ public sealed class AprStreamingContractTests
}
}
[Fact]
public void ResolveFilepathsToIdsAndFileSizes_MissingFile_FailsFastWithErrorIndex()
{
const ulong memoryBase = 0x1_0000_0000;
const ulong pathListAddress = memoryBase + 0x100;
const ulong pathAddress = memoryBase + 0x200;
const ulong idsAddress = memoryBase + 0x800;
const ulong sizesAddress = memoryBase + 0x880;
const ulong errorIndexAddress = memoryBase + 0x8F0;
var memory = new FakeCpuMemory(memoryBase, 0x4000);
var context = new CpuContext(memory, Generation.Gen5);
var missingHostPath = Path.Combine(
Path.GetTempPath(),
$"sharpemu-apr-missing-{Guid.NewGuid():N}.bin");
memory.WriteCString(pathAddress, missingHostPath);
WriteUInt64(memory, pathListAddress, pathAddress);
context[CpuRegister.Rdi] = pathListAddress;
context[CpuRegister.Rsi] = 1;
context[CpuRegister.Rdx] = idsAddress;
context[CpuRegister.Rcx] = sizesAddress;
context[CpuRegister.R8] = errorIndexAddress;
Assert.Equal(-1, KernelMemoryCompatExports.KernelAprResolveFilepathsToIdsAndFileSizes(context));
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
Assert.Equal(uint.MaxValue, ReadUInt32(memory, idsAddress));
Assert.Equal(0ul, ReadUInt64(memory, sizesAddress));
Assert.Equal(0u, ReadUInt32(memory, errorIndexAddress));
}
[Fact]
public void ResolveFilepathsToIdsAndFileSizes_InvalidErrorIndex_ReturnsMemoryFault()
{
const ulong memoryBase = 0x1_0000_0000;
const ulong pathListAddress = memoryBase + 0x100;
const ulong pathAddress = memoryBase + 0x200;
const ulong idsAddress = memoryBase + 0x800;
const ulong sizesAddress = memoryBase + 0x880;
var memory = new FakeCpuMemory(memoryBase, 0x4000);
var context = new CpuContext(memory, Generation.Gen5);
var missingHostPath = Path.Combine(
Path.GetTempPath(),
$"sharpemu-apr-missing-{Guid.NewGuid():N}.bin");
memory.WriteCString(pathAddress, missingHostPath);
WriteUInt64(memory, pathListAddress, pathAddress);
context[CpuRegister.Rdi] = pathListAddress;
context[CpuRegister.Rsi] = 1;
context[CpuRegister.Rdx] = idsAddress;
context[CpuRegister.Rcx] = sizesAddress;
context[CpuRegister.R8] = memoryBase + 0x5000;
Assert.Equal(
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT,
KernelMemoryCompatExports.KernelAprResolveFilepathsToIdsAndFileSizes(context));
}
[Fact]
public void ResolveFilepathsToIdsAndFileSizes_MissingMidBatch_StopsAtFailingEntry()
{
const ulong memoryBase = 0x1_0000_0000;
const ulong pathListAddress = memoryBase + 0x100;
const ulong idsAddress = memoryBase + 0x800;
const ulong sizesAddress = memoryBase + 0x880;
const ulong errorIndexAddress = memoryBase + 0x8F0;
byte[] fileContents = [1, 2, 3, 4, 5];
var hostPath = Path.GetTempFileName();
var missingHostPath = Path.Combine(
Path.GetTempPath(),
$"sharpemu-apr-missing-{Guid.NewGuid():N}.bin");
try
{
File.WriteAllBytes(hostPath, fileContents);
var memory = new FakeCpuMemory(memoryBase, 0x4000);
var context = new CpuContext(memory, Generation.Gen5);
memory.WriteCString(memoryBase + 0x200, hostPath);
memory.WriteCString(memoryBase + 0x400, missingHostPath);
memory.WriteCString(memoryBase + 0x600, hostPath);
WriteUInt64(memory, pathListAddress, memoryBase + 0x200);
WriteUInt64(memory, pathListAddress + 8, memoryBase + 0x400);
WriteUInt64(memory, pathListAddress + 16, memoryBase + 0x600);
WriteUInt32(memory, idsAddress + 8, 0x1234_5678); // sentinel: entry 2 untouched
WriteUInt64(memory, sizesAddress + 16, 0xDEAD);
context[CpuRegister.Rdi] = pathListAddress;
context[CpuRegister.Rsi] = 3;
context[CpuRegister.Rdx] = idsAddress;
context[CpuRegister.Rcx] = sizesAddress;
context[CpuRegister.R8] = errorIndexAddress;
Assert.Equal(-1, KernelMemoryCompatExports.KernelAprResolveFilepathsToIdsAndFileSizes(context));
Assert.NotEqual(uint.MaxValue, ReadUInt32(memory, idsAddress));
Assert.Equal((ulong)fileContents.Length, ReadUInt64(memory, sizesAddress));
Assert.Equal(uint.MaxValue, ReadUInt32(memory, idsAddress + 4));
Assert.Equal(0ul, ReadUInt64(memory, sizesAddress + 8));
Assert.Equal(1u, ReadUInt32(memory, errorIndexAddress));
Assert.Equal(0x1234_5678u, ReadUInt32(memory, idsAddress + 8));
Assert.Equal(0xDEADul, ReadUInt64(memory, sizesAddress + 16));
}
finally
{
File.Delete(hostPath);
}
}
private static uint ReadUInt32(FakeCpuMemory memory, ulong address)
{
Span<byte> bytes = stackalloc byte[sizeof(uint)];
Assert.True(memory.TryRead(address, bytes));
return BinaryPrimitives.ReadUInt32LittleEndian(bytes);
}
private static ulong ReadUInt64(FakeCpuMemory memory, ulong address)
{
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
Assert.True(memory.TryRead(address, bytes));
return BinaryPrimitives.ReadUInt64LittleEndian(bytes);
}
private static void WriteUInt32(FakeCpuMemory memory, ulong address, uint value)
{
Span<byte> bytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
Assert.True(memory.TryWrite(address, bytes));
}
private static void WriteUInt64(FakeCpuMemory memory, ulong address, ulong value)
{
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
@@ -40,6 +40,65 @@ public sealed class AvPlayerPathTests : IDisposable
AssertPathIsInsideApp0(resolved);
}
[Fact]
public void UnrealRelativeRawPathAnchorsAtApp0AndResolvesMedia()
{
var mediaPath = CreateFile("SampleProject/Content/Movies/Startup.mp4");
var resolved = AvPlayerExports.ResolveGuestPath(
"../../../SampleProject/Content/Movies/Startup.mp4");
Assert.NotNull(resolved);
Assert.Equal(File.ReadAllBytes(mediaPath), File.ReadAllBytes(resolved));
AssertPathIsInsideApp0(resolved);
}
[Fact]
public void UnrealRelativeRawPathCannotEscapeApp0()
{
var outsidePath = Path.Combine(_tempRoot, "outside.mp4");
File.WriteAllBytes(outsidePath, [0x7F]);
CreateFile("outside.mp4");
Assert.Null(AvPlayerExports.ResolveGuestPath("../../../outside.mp4"));
}
[Fact]
public void CurrentDirectoryRawPathResolvesInsideApp0()
{
var mediaPath = CreateFile("Movies/Intro.mp4");
var resolved = AvPlayerExports.ResolveGuestPath("./Movies/Intro.mp4");
Assert.NotNull(resolved);
Assert.Equal(Path.GetFullPath(mediaPath), resolved);
AssertPathIsInsideApp0(resolved);
}
[Theory]
[InlineData(false, "ffmpeg", "ffprobe")]
[InlineData(true, "ffmpeg.exe", "ffprobe.exe")]
public void MediaToolLookupUsesPlatformNames(
bool isWindows,
string ffmpegName,
string ffprobeName)
{
var toolDirectory = Path.Combine(_tempRoot, "Media Tools");
Directory.CreateDirectory(toolDirectory);
var ffmpeg = Path.Combine(toolDirectory, ffmpegName);
File.WriteAllBytes(ffmpeg, []);
var resolved = AvPlayerExports.FindFfmpeg(
configured: null,
searchPath: $"\"{toolDirectory}\"",
isWindows);
Assert.Equal(ffmpeg, resolved);
Assert.Equal(
Path.Combine(toolDirectory, ffprobeName),
AvPlayerExports.GetFfprobePath(ffmpeg, isWindows));
}
[Fact]
public void RelativeFileUriCannotEscapeApp0()
{
@@ -143,12 +202,14 @@ public sealed class AvPlayerPathTests : IDisposable
private void AssertPathIsInsideApp0(string resolved)
{
var rootWithSeparator =
Path.TrimEndingDirectorySeparator(Path.GetFullPath(_app0Root)) +
Path.DirectorySeparatorChar;
Assert.StartsWith(
rootWithSeparator,
Path.GetFullPath(resolved),
StringComparison.OrdinalIgnoreCase);
var relative = Path.GetRelativePath(
Path.GetFullPath(_app0Root),
Path.GetFullPath(resolved));
Assert.False(Path.IsPathFullyQualified(relative));
Assert.NotEqual("..", relative);
Assert.False(
relative.StartsWith(
".." + Path.DirectorySeparatorChar,
StringComparison.Ordinal));
}
}
@@ -0,0 +1,126 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.HLE;
using SharpEmu.Libs.AvPlayer;
using Xunit;
namespace SharpEmu.Libs.Tests.AvPlayer;
public sealed class AvPlayerStreamInfoTests
{
private const string StreamInfoExNid = "ctTAcF5DiKQ";
private const ulong BaseAddress = 0x1_0000_0000;
private const int MemorySize = 0x2000;
private const ulong InfoAddress = BaseAddress + 0x100;
private const ulong Handle = 0xA0_0000_0001;
private const ulong DurationMilliseconds = 0x0102_0304_0506_0708;
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)
{
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
var context = new CpuContext(memory, Generation.Gen5);
AvPlayerExports.RegisterPlayerForTest(Handle, 1280, 720, DurationMilliseconds);
try
{
Span<byte> window = stackalloc byte[40];
window.Fill(Sentinel);
Assert.True(memory.TryWrite(InfoAddress, window));
context[CpuRegister.Rdi] = Handle;
context[CpuRegister.Rsi] = streamIndex;
context[CpuRegister.Rdx] = InfoAddress;
var resultCode = useExtendedFunction
? AvPlayerExports.AvPlayerGetStreamInfoEx(context)
: AvPlayerExports.AvPlayerGetStreamInfo(context);
Assert.Equal(0, resultCode);
Span<byte> result = stackalloc byte[40];
Assert.True(memory.TryRead(InfoAddress, result));
Assert.Equal(streamIndex, BinaryPrimitives.ReadUInt32LittleEndian(result));
if (streamIndex == 0)
{
Assert.Equal(1280u, BinaryPrimitives.ReadUInt32LittleEndian(result[8..]));
Assert.Equal(720u, BinaryPrimitives.ReadUInt32LittleEndian(result[12..]));
}
else
{
Assert.Equal(2, BinaryPrimitives.ReadUInt16LittleEndian(result[8..]));
Assert.Equal(48_000u, BinaryPrimitives.ReadUInt32LittleEndian(result[12..]));
}
Assert.Equal(DurationMilliseconds, BinaryPrimitives.ReadUInt64LittleEndian(result[24..]));
for (var index = 32; index < result.Length; index++)
{
Assert.Equal(Sentinel, result[index]);
}
}
finally
{
AvPlayerExports.RemovePlayerForTest(Handle);
}
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void GetStreamInfoFunctionsRejectInvalidArguments(bool useExtendedFunction)
{
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] = 2;
context[CpuRegister.Rdx] = InfoAddress;
Assert.NotEqual(0, InvokeGetStreamInfo(context, useExtendedFunction));
context[CpuRegister.Rsi] = 0;
context[CpuRegister.Rdx] = 0;
Assert.NotEqual(0, InvokeGetStreamInfo(context, useExtendedFunction));
context[CpuRegister.Rdi] = Handle + 1;
context[CpuRegister.Rdx] = InfoAddress;
Assert.NotEqual(0, InvokeGetStreamInfo(context, useExtendedFunction));
}
finally
{
AvPlayerExports.RemovePlayerForTest(Handle);
}
}
[Fact]
public void StreamInfoExExportIsRegisteredForGen5Only()
{
var gen4Manager = new ModuleManager();
gen4Manager.RegisterExports(
SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen4));
Assert.False(gen4Manager.TryGetExport(StreamInfoExNid, out _));
var gen5Manager = new ModuleManager();
gen5Manager.RegisterExports(
SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen5));
Assert.True(gen5Manager.TryGetExport(StreamInfoExNid, out var export));
Assert.Equal("sceAvPlayerGetStreamInfoEx", export.Name);
Assert.Equal("libSceAvPlayer", export.LibraryName);
Assert.Equal(Generation.Gen5, export.Target);
}
private static int InvokeGetStreamInfo(CpuContext context, bool useExtendedFunction) =>
useExtendedFunction
? AvPlayerExports.AvPlayerGetStreamInfoEx(context)
: AvPlayerExports.AvPlayerGetStreamInfo(context);
}
@@ -25,6 +25,57 @@ public sealed class PthreadMutexSemanticsTests
Assert.NotEqual(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
}
[Fact]
public void AdaptiveMutex_GuestTrackedSelfLockReturnsDeadlockAndSingleUnlockReleases()
{
const ulong memoryBase = 0x1_0001_0000;
const ulong mutexAddress = memoryBase + 0x100;
var memory = new AllocatingCpuMemory(memoryBase, 0x4000);
var context = new CpuContext(memory, Generation.Gen5);
Assert.True(context.TryWriteUInt64(mutexAddress, 1)); // Static adaptive initializer.
context[CpuRegister.Rdi] = mutexAddress;
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(context));
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
Assert.True(context.TryWriteUInt64(mutexAddress + 8, currentThreadHandle));
Assert.Equal(
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK,
KernelPthreadCompatExports.PthreadMutexLock(context));
Assert.True(context.TryWriteUInt64(mutexAddress + 8, 0));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexTrylock(context));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
}
[Fact]
public void RecursiveMutex_GuestTrackedSelfLockKeepsRecursiveSemantics()
{
const ulong memoryBase = 0x1_0002_0000;
const ulong attrAddress = memoryBase + 0x100;
const ulong mutexAddress = memoryBase + 0x200;
var memory = new AllocatingCpuMemory(memoryBase, 0x4000);
var context = new CpuContext(memory, Generation.Gen5);
context[CpuRegister.Rdi] = attrAddress;
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexattrInit(context));
context[CpuRegister.Rsi] = 2;
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexattrSettype(context));
context[CpuRegister.Rdi] = mutexAddress;
context[CpuRegister.Rsi] = attrAddress;
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexInit(context));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(context));
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
Assert.True(context.TryWriteUInt64(mutexAddress + 8, currentThreadHandle));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(context));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
Assert.NotEqual(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
}
[Fact]
public async Task ContendedMutex_HandsOffOneHostWaiterAtATime()
{