fix(kernel): implement APR ResolveFilepathsWithPrefixToIdsAndFileSizes (#534)

* fix(kernel): implement APR ResolveFilepathsWithPrefixToIdsAndFileSizes

Resource streamers resolve relative paths against a shared prefix; without
this HLE every call returned NOT_FOUND and assets never got real ids/sizes.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: retrigger gameplay CI for PR #534

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
MarcelMediaDev
2026-07-23 13:34:58 +01:00
committed by GitHub
parent e13cb28267
commit eb1195e59a
2 changed files with 193 additions and 0 deletions
@@ -1773,6 +1773,113 @@ public static partial class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// WithPrefix sibling of sceKernelAprResolveFilepathsToIdsAndFileSizes.
// Resource streamers resolve relative asset paths against a shared directory
// prefix. Without HLE, every call returned the generic NOT_FOUND sentinel
// and no asset received a real file id/size. Signature inferred from
// observed guest registers (rdi=prefix, rsi=path list, rdx=count, rcx=ids,
// r8=sizes, r9=error index): the no-prefix sibling's args shifted right by
// one with a leading `const char* prefix`.
[SysAbiExport(
Nid = "w5fcCG+t31g",
ExportName = "sceKernelAprResolveFilepathsWithPrefixToIdsAndFileSizes",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelAprResolveFilepathsWithPrefixToIdsAndFileSizes(CpuContext ctx)
{
var prefixAddress = ctx[CpuRegister.Rdi];
var pathListAddress = ctx[CpuRegister.Rsi];
var count = ctx[CpuRegister.Rdx];
var idsAddress = ctx[CpuRegister.Rcx];
var sizesAddress = ctx[CpuRegister.R8];
var errorIndexAddress = ctx[CpuRegister.R9];
if (pathListAddress == 0 || count == 0 || sizesAddress == 0 || count > 1024)
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Einval);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var prefix = string.Empty;
if (prefixAddress != 0)
{
_ = TryReadNullTerminatedUtf8(ctx, prefixAddress, MaxGuestStringLength, out prefix);
}
for (ulong i = 0; i < count; i++)
{
if (idsAddress != 0 &&
!TryWriteUInt32Compat(ctx, idsAddress + (i * sizeof(uint)), uint.MaxValue))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (!TryResolveAprFilepath(ctx, pathListAddress, i, out var relativePath))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var guestPath = CombineAprPrefixedPath(prefix, relativePath);
var hostPath = ResolveGuestPath(guestPath);
if (!TryGetAprFileSize(hostPath, out var fileSize))
{
LogIoTrace("apr_resolve_with_prefix", guestPath, $"host='{hostPath}' index={i} count={count} result=not_found");
if (sizesAddress != 0 &&
!TryWriteUInt64Compat(ctx, sizesAddress + (i * sizeof(ulong)), 0))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
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);
LogIoTrace("apr_resolve_with_prefix", guestPath, $"host='{hostPath}' index={i} count={count} id=0x{fileId:X8} size={fileSize}");
if (idsAddress != 0 &&
!TryWriteUInt32Compat(ctx, idsAddress + (i * sizeof(uint)), fileId))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (!TryWriteUInt64Compat(ctx, sizesAddress + (i * sizeof(ulong)), fileSize))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static string CombineAprPrefixedPath(string prefix, string relative)
{
if (string.IsNullOrEmpty(prefix))
{
return relative;
}
if (string.IsNullOrEmpty(relative))
{
return prefix;
}
return $"{prefix.TrimEnd('/')}/{relative.TrimStart('/')}";
}
// The IDs-only sibling of sceKernelAprResolveFilepathsToIdsAndFileSizes.
// Games that stream via AMPR APR call this to turn asset paths into file
// IDs, then hand those IDs to sceAmprAprCommandBufferReadFile. Without it
@@ -105,6 +105,92 @@ public sealed class AprStreamingContractTests
}
}
[Fact]
public void ResolveFilepathsWithPrefixToIdsAndFileSizes_CombinesPrefixAndResolvesRealFile()
{
// Resource streamers call WithPrefix to join a directory prefix with a
// relative asset path. Without HLE every call returned NOT_FOUND and no
// asset received a real file id/size.
const ulong memoryBase = 0x1_0000_0000;
const ulong prefixAddress = memoryBase + 0x80;
const ulong pathListAddress = memoryBase + 0x100;
const ulong pathAddress = memoryBase + 0x200;
const ulong idsAddress = memoryBase + 0x800;
const ulong sizesAddress = memoryBase + 0x880;
byte[] fileContents = [1, 2, 3, 4, 5, 6];
var mountRoot = Path.Combine(
Path.GetTempPath(),
$"sharpemu-apr-prefix-{Guid.NewGuid():N}");
Directory.CreateDirectory(mountRoot);
var mountPoint = $"/sharpemu_apr_prefix_mnt_{Guid.NewGuid():N}";
const string fileName = "asset.bin";
var hostPath = Path.Combine(mountRoot, fileName);
try
{
File.WriteAllBytes(hostPath, fileContents);
KernelMemoryCompatExports.RegisterGuestPathMount(mountPoint, mountRoot);
var memory = new FakeCpuMemory(memoryBase, 0x4000);
var context = new CpuContext(memory, Generation.Gen5);
memory.WriteCString(prefixAddress, mountPoint);
memory.WriteCString(pathAddress, fileName);
WriteUInt64(memory, pathListAddress, pathAddress);
context[CpuRegister.Rdi] = prefixAddress;
context[CpuRegister.Rsi] = pathListAddress;
context[CpuRegister.Rdx] = 1;
context[CpuRegister.Rcx] = idsAddress;
context[CpuRegister.R8] = sizesAddress;
context[CpuRegister.R9] = 0;
Assert.Equal(
(int)OrbisGen2Result.ORBIS_GEN2_OK,
KernelMemoryCompatExports.KernelAprResolveFilepathsWithPrefixToIdsAndFileSizes(context));
Assert.NotEqual(uint.MaxValue, ReadUInt32(memory, idsAddress));
Assert.Equal((ulong)fileContents.Length, ReadUInt64(memory, sizesAddress));
}
finally
{
KernelMemoryCompatExports.UnregisterGuestPathMount(mountPoint);
if (Directory.Exists(mountRoot))
{
Directory.Delete(mountRoot, recursive: true);
}
}
}
[Fact]
public void ResolveFilepathsWithPrefixToIdsAndFileSizes_MissingFile_FailsFastWithErrorIndex()
{
const ulong memoryBase = 0x1_0000_0000;
const ulong prefixAddress = memoryBase + 0x80;
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);
memory.WriteCString(prefixAddress, "/does-not-exist-prefix");
memory.WriteCString(pathAddress, $"missing-{Guid.NewGuid():N}.bin");
WriteUInt64(memory, pathListAddress, pathAddress);
context[CpuRegister.Rdi] = prefixAddress;
context[CpuRegister.Rsi] = pathListAddress;
context[CpuRegister.Rdx] = 1;
context[CpuRegister.Rcx] = idsAddress;
context[CpuRegister.R8] = sizesAddress;
context[CpuRegister.R9] = errorIndexAddress;
Assert.Equal(
-1,
KernelMemoryCompatExports.KernelAprResolveFilepathsWithPrefixToIdsAndFileSizes(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_MissingFile_FailsFastWithErrorIndex()
{