mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-31 15:09:42 +08:00
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:
@@ -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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user