diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index 3e62c202..c9dd8c4a 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -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 diff --git a/tests/SharpEmu.Libs.Tests/Ampr/AprStreamingContractTests.cs b/tests/SharpEmu.Libs.Tests/Ampr/AprStreamingContractTests.cs index cf587f8d..c303c8d1 100644 --- a/tests/SharpEmu.Libs.Tests/Ampr/AprStreamingContractTests.cs +++ b/tests/SharpEmu.Libs.Tests/Ampr/AprStreamingContractTests.cs @@ -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() {