diff --git a/.gitignore b/.gitignore index 082ad42..6148721 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,8 @@ arm64/ packages/ *.nupkg .nuget/ +.dotnet-home/ +.cache/ .DS_Store Thumbs.db diff --git a/src/SharpEmu.Libs/Ampr/AmprExports.cs b/src/SharpEmu.Libs/Ampr/AmprExports.cs new file mode 100644 index 0000000..7340d39 --- /dev/null +++ b/src/SharpEmu.Libs/Ampr/AmprExports.cs @@ -0,0 +1,489 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; +using System.Buffers.Binary; +using System.Collections.Concurrent; + +namespace SharpEmu.Libs.Ampr; + +public static class AmprExports +{ + private const int CommandBufferHeaderSize = 0x28; + private const ulong CommandBufferSelfOffset = 0x00; + private const ulong CommandBufferDataOffset = 0x08; + private const ulong CommandBufferSizeOffset = 0x10; + private const ulong CommandBufferAux0Offset = 0x18; + private const ulong CommandBufferAux1Offset = 0x20; + private const ulong ReadFileRecordSize = 0x30; + private static readonly ConcurrentDictionary _commandBuffers = new(); + + private sealed class CommandBufferState + { + public ulong Buffer; + public ulong Size; + public ulong WriteOffset; + } + + [SysAbiExport( + Nid = "8aI7R7WaOlc", + ExportName = "sceAmprCommandBufferConstructor", + Target = Generation.Gen5, + LibraryName = "libSceAmpr")] + public static int CommandBufferConstructor(CpuContext ctx) + { + var commandBuffer = ctx[CpuRegister.Rdi]; + var buffer = ctx[CpuRegister.Rsi]; + var size = ctx[CpuRegister.Rdx]; + + if (commandBuffer == 0) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + if (!InitializeCommandBuffer(ctx, commandBuffer, buffer, size, aux0: 0, aux1: 0, clear: true)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + TraceAmpr(ctx, "ctor", commandBuffer, buffer, size); + ctx[CpuRegister.Rax] = commandBuffer; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "a8uLzYY--tM", + ExportName = "sceAmprAprCommandBufferConstructor", + Target = Generation.Gen5, + LibraryName = "libSceAmpr")] + public static int AprCommandBufferConstructor(CpuContext ctx) + { + var commandBuffer = ctx[CpuRegister.Rdi]; + var aux0 = ctx[CpuRegister.Rsi]; + var aux1 = ctx[CpuRegister.Rdx]; + + if (commandBuffer == 0) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + var buffer = 0UL; + var size = 0UL; + _ = ctx.TryReadUInt64(commandBuffer + CommandBufferDataOffset, out buffer); + _ = ctx.TryReadUInt64(commandBuffer + CommandBufferSizeOffset, out size); + + if (!InitializeCommandBuffer(ctx, commandBuffer, buffer, size, aux0, aux1, clear: false)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + TraceAmpr(ctx, "apr_ctor", commandBuffer, aux0, aux1); + ctx[CpuRegister.Rax] = commandBuffer; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "Qs1xtplKo0U", + ExportName = "sceAmprAprCommandBufferDestructor", + Target = Generation.Gen5, + LibraryName = "libSceAmpr")] + public static int AprCommandBufferDestructor(CpuContext ctx) + { + var commandBuffer = ctx[CpuRegister.Rdi]; + if (commandBuffer == 0) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + if (!ctx.TryWriteUInt64(commandBuffer + CommandBufferAux0Offset, 0) || + !ctx.TryWriteUInt64(commandBuffer + CommandBufferAux1Offset, 0)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + TraceAmpr(ctx, "apr_dtor", commandBuffer, 0, 0); + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "GuchCTefuZw", + ExportName = "sceAmprCommandBufferDestructor", + Target = Generation.Gen5, + LibraryName = "libSceAmpr")] + public static int CommandBufferDestructor(CpuContext ctx) + { + var commandBuffer = ctx[CpuRegister.Rdi]; + if (commandBuffer == 0) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + if (!WriteVisibleCommandBufferPointers(ctx, commandBuffer, buffer: 0, size: 0)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + _commandBuffers.TryRemove(commandBuffer, out _); + TraceAmpr(ctx, "dtor", commandBuffer, 0, 0); + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "N-FSPA4S3nI", + ExportName = "sceAmprCommandBufferSetBuffer", + Target = Generation.Gen5, + LibraryName = "libSceAmpr")] + public static int CommandBufferSetBuffer(CpuContext ctx) + { + var commandBuffer = ctx[CpuRegister.Rdi]; + var buffer = ctx[CpuRegister.Rsi]; + var size = ctx[CpuRegister.Rdx]; + + if (commandBuffer == 0) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + if (!WriteCommandBufferPointers(ctx, commandBuffer, buffer, size)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + TraceAmpr(ctx, "set_buffer", commandBuffer, buffer, size); + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "baQO9ez2gL4", + ExportName = "sceAmprCommandBufferReset", + Target = Generation.Gen5, + LibraryName = "libSceAmpr")] + public static int CommandBufferReset(CpuContext ctx) + { + var commandBuffer = ctx[CpuRegister.Rdi]; + if (commandBuffer == 0) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + if (!ctx.TryReadUInt64(commandBuffer + CommandBufferDataOffset, out var buffer) || + !ctx.TryReadUInt64(commandBuffer + CommandBufferSizeOffset, out var size) || + !WriteCommandBufferPointers(ctx, commandBuffer, buffer, size, writeOffset: 0)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + TraceAmpr(ctx, "reset", commandBuffer, buffer, size); + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "ULvXMDz56po", + ExportName = "sceAmprCommandBufferClearBuffer", + Target = Generation.Gen5, + LibraryName = "libSceAmpr")] + public static int CommandBufferClearBuffer(CpuContext ctx) + { + var commandBuffer = ctx[CpuRegister.Rdi]; + if (commandBuffer == 0) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + if (!TryGetCommandBufferState(ctx, commandBuffer, out var buffer, out var size, out _) || + !WriteVisibleCommandBufferPointers(ctx, commandBuffer, buffer: 0, size: 0)) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + _commandBuffers.TryRemove(commandBuffer, out _); + TraceAmpr(ctx, "clear_buffer", commandBuffer, buffer, size); + ctx[CpuRegister.Rax] = buffer; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "mQ16-QdKv7k", + ExportName = "sceAmprAprCommandBufferReadFile", + Target = Generation.Gen5, + LibraryName = "libSceAmpr")] + public static int AprCommandBufferReadFile(CpuContext ctx) + { + var commandBuffer = ctx[CpuRegister.Rdi]; + var fileId = unchecked((uint)ctx[CpuRegister.Rcx]); + var destination = ctx[CpuRegister.R8]; + var size = ctx[CpuRegister.R9]; + + if (commandBuffer == 0 || (destination == 0 && size != 0)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + if (!ctx.TryReadUInt64(ctx[CpuRegister.Rsp] + sizeof(ulong), out var fileOffset)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath) || !File.Exists(hostPath)) + { + TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead: 0, hostPath, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; + } + + var result = TryReadFileToGuestMemory(ctx, hostPath, fileOffset, destination, size, out var bytesRead); + if (result != (int)OrbisGen2Result.ORBIS_GEN2_OK) + { + TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, hostPath, result); + return result; + } + + if (!AppendReadFileRecord(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, hostPath, (int)OrbisGen2Result.ORBIS_GEN2_OK); + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + private static bool InitializeCommandBuffer( + CpuContext ctx, + ulong commandBuffer, + ulong buffer, + ulong size, + ulong aux0, + ulong aux1, + bool clear) + { + if (clear) + { + Span header = stackalloc byte[CommandBufferHeaderSize]; + header.Clear(); + if (!ctx.Memory.TryWrite(commandBuffer, header)) + { + return false; + } + } + + return ctx.TryWriteUInt64(commandBuffer + CommandBufferSelfOffset, commandBuffer) && + ctx.TryWriteUInt64(commandBuffer + CommandBufferAux0Offset, aux0) && + ctx.TryWriteUInt64(commandBuffer + CommandBufferAux1Offset, aux1) && + WriteCommandBufferPointers(ctx, commandBuffer, buffer, size, writeOffset: 0); + } + + private static bool WriteCommandBufferPointers(CpuContext ctx, ulong commandBuffer, ulong buffer, ulong size) + { + return WriteCommandBufferPointers(ctx, commandBuffer, buffer, size, writeOffset: 0); + } + + private static bool WriteCommandBufferPointers(CpuContext ctx, ulong commandBuffer, ulong buffer, ulong size, ulong writeOffset) + { + if (!WriteVisibleCommandBufferPointers(ctx, commandBuffer, buffer, size)) + { + return false; + } + + var state = _commandBuffers.GetOrAdd(commandBuffer, static _ => new CommandBufferState()); + lock (state) + { + state.Buffer = buffer; + state.Size = size; + state.WriteOffset = writeOffset; + } + + return true; + } + + private static bool WriteVisibleCommandBufferPointers(CpuContext ctx, ulong commandBuffer, ulong buffer, ulong size) + { + return ctx.TryWriteUInt64(commandBuffer + CommandBufferSelfOffset, commandBuffer) && + ctx.TryWriteUInt64(commandBuffer + CommandBufferDataOffset, buffer) && + ctx.TryWriteUInt64(commandBuffer + CommandBufferSizeOffset, size); + } + + private static bool TryGetCommandBufferState( + CpuContext ctx, + ulong commandBuffer, + out ulong buffer, + out ulong size, + out CommandBufferState? state) + { + if (_commandBuffers.TryGetValue(commandBuffer, out state)) + { + lock (state) + { + buffer = state.Buffer; + size = state.Size; + } + + return true; + } + + if (ctx.TryReadUInt64(commandBuffer + CommandBufferDataOffset, out buffer) && + ctx.TryReadUInt64(commandBuffer + CommandBufferSizeOffset, out size)) + { + state = _commandBuffers.GetOrAdd(commandBuffer, static _ => new CommandBufferState()); + lock (state) + { + state.Buffer = buffer; + state.Size = size; + state.WriteOffset = 0; + } + + return true; + } + + buffer = 0; + size = 0; + state = null; + return false; + } + + private static int TryReadFileToGuestMemory( + CpuContext ctx, + string hostPath, + ulong fileOffset, + ulong destination, + ulong size, + out ulong bytesRead) + { + bytesRead = 0; + if (size == 0) + { + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + if (fileOffset > long.MaxValue) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + const int ChunkSize = 64 * 1024; + var buffer = new byte[(int)Math.Min((ulong)ChunkSize, size)]; + + try + { + using var stream = new FileStream(hostPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); + if (fileOffset >= (ulong)stream.Length) + { + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + stream.Position = unchecked((long)fileOffset); + + while (bytesRead < size) + { + var request = (int)Math.Min((ulong)buffer.Length, size - bytesRead); + var read = stream.Read(buffer, 0, request); + if (read <= 0) + { + break; + } + + if (!ctx.Memory.TryWrite(destination + bytesRead, buffer.AsSpan(0, read))) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + bytesRead += (ulong)read; + } + } + catch (UnauthorizedAccessException) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED; + } + catch (IOException) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; + } + + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + private static bool AppendReadFileRecord( + CpuContext ctx, + ulong commandBuffer, + uint fileId, + ulong destination, + ulong size, + ulong fileOffset, + ulong bytesRead) + { + if (!TryGetCommandBufferState(ctx, commandBuffer, out var buffer, out var capacity, out var state) || state is null) + { + return false; + } + + Span record = stackalloc byte[(int)ReadFileRecordSize]; + record.Clear(); + BinaryPrimitives.WriteUInt32LittleEndian(record[0x00..], 1); + BinaryPrimitives.WriteUInt32LittleEndian(record[0x04..], fileId); + BinaryPrimitives.WriteUInt64LittleEndian(record[0x08..], destination); + BinaryPrimitives.WriteUInt64LittleEndian(record[0x10..], size); + BinaryPrimitives.WriteUInt64LittleEndian(record[0x18..], fileOffset); + BinaryPrimitives.WriteUInt64LittleEndian(record[0x20..], bytesRead); + + lock (state) + { + if (state.Buffer == 0 || state.WriteOffset + ReadFileRecordSize > state.Size) + { + return false; + } + + if (!ctx.Memory.TryWrite(state.Buffer + state.WriteOffset, record)) + { + return false; + } + + state.WriteOffset += ReadFileRecordSize; + } + + return true; + } + + private static void TraceAmpr(CpuContext ctx, string operation, ulong commandBuffer, ulong arg0, ulong arg1) + { + if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal)) + { + return; + } + + var returnRip = 0UL; + _ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip); + Console.Error.WriteLine( + $"[LOADER][TRACE] ampr.{operation}: cmd=0x{commandBuffer:X16} arg0=0x{arg0:X16} arg1=0x{arg1:X16} ret=0x{returnRip:X16}"); + } + + private static void TraceAmprRead( + CpuContext ctx, + ulong commandBuffer, + uint fileId, + ulong destination, + ulong size, + ulong fileOffset, + ulong bytesRead, + string? hostPath, + int result) + { + if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal)) + { + return; + } + + var returnRip = 0UL; + _ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip); + Console.Error.WriteLine( + $"[LOADER][TRACE] ampr.read_file: cmd=0x{commandBuffer:X16} id=0x{fileId:X8} dst=0x{destination:X16} size=0x{size:X16} offset=0x{fileOffset:X16} read=0x{bytesRead:X16} result=0x{result:X8} path='{hostPath ?? string.Empty}' ret=0x{returnRip:X16}"); + } +} diff --git a/src/SharpEmu.Libs/Ampr/AmprFileRegistry.cs b/src/SharpEmu.Libs/Ampr/AmprFileRegistry.cs new file mode 100644 index 0000000..084c8e6 --- /dev/null +++ b/src/SharpEmu.Libs/Ampr/AmprFileRegistry.cs @@ -0,0 +1,40 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Collections.Concurrent; + +namespace SharpEmu.Libs.Ampr; + +internal static class AmprFileRegistry +{ + private static readonly ConcurrentDictionary _hostPathsById = new(); + + public static uint Register(string guestPath, string hostPath) + { + var id = ComputeFileId(guestPath); + _hostPathsById[id] = hostPath; + return id; + } + + public static bool TryGetHostPath(uint id, out string hostPath) + { + return _hostPathsById.TryGetValue(id, out hostPath!); + } + + private static uint ComputeFileId(string guestPath) + { + var bytes = System.Text.Encoding.UTF8.GetBytes(guestPath); + + const uint offsetBasis = 2166136261; + const uint prime = 16777619; + + var hash = offsetBasis; + foreach (var b in bytes) + { + hash ^= b; + hash *= prime; + } + + return hash; + } +} diff --git a/src/SharpEmu.Libs/Kernel/KernelAprCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelAprCompatExports.cs new file mode 100644 index 0000000..dce05b5 --- /dev/null +++ b/src/SharpEmu.Libs/Kernel/KernelAprCompatExports.cs @@ -0,0 +1,158 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; +using System.Buffers.Binary; +using System.Collections.Concurrent; +using System.Threading; + +namespace SharpEmu.Libs.Kernel; + +public static class KernelAprCompatExports +{ + private static readonly ConcurrentDictionary _submittedCommandBuffers = new(); + private static int _nextSubmissionId; + + [SysAbiExport( + Nid = "ASoW5WE-UPo", + ExportName = "sceKernelAprSubmitCommandBufferAndGetResult", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelAprSubmitCommandBufferAndGetResult(CpuContext ctx) + { + var commandBuffer = ctx[CpuRegister.Rdi]; + var priority = ctx[CpuRegister.Rsi]; + var resultAddress = ctx[CpuRegister.Rdx]; + var outSubmissionId = ctx[CpuRegister.Rcx]; + + if (commandBuffer == 0) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + var submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId)); + if (submissionId == 0) + { + submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId)); + } + + _submittedCommandBuffers[submissionId] = commandBuffer; + + if (outSubmissionId != 0 && !TryWriteUInt32(ctx, outSubmissionId, submissionId)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + if (resultAddress != 0 && !TryWriteAprResult(ctx, resultAddress)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + TraceApr(ctx, "submit_get_result", submissionId, commandBuffer, priority, resultAddress); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "rqwFKI4PAiM", + ExportName = "sceKernelAprWaitCommandBuffer", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelAprWaitCommandBuffer(CpuContext ctx) + { + var submissionId = unchecked((uint)ctx[CpuRegister.Rdi]); + var priority = ctx[CpuRegister.Rsi]; + var resultAddress = ctx[CpuRegister.Rdx]; + + if (!_submittedCommandBuffers.TryRemove(submissionId, out var commandBuffer)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; + } + + if (resultAddress != 0 && !TryWriteAprResult(ctx, resultAddress)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + TraceApr(ctx, "wait", submissionId, commandBuffer, priority, resultAddress); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "eE4Szl8sil8", + ExportName = "sceKernelAprSubmitCommandBuffer", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelAprSubmitCommandBuffer(CpuContext ctx) + { + var commandBuffer = ctx[CpuRegister.Rdi]; + if (commandBuffer == 0) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + var submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId)); + _submittedCommandBuffers[submissionId] = commandBuffer; + + TraceApr(ctx, "submit", submissionId, commandBuffer, ctx[CpuRegister.Rsi], 0); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "qvMUCyyaCSI", + ExportName = "sceKernelAprSubmitCommandBufferAndGetId", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelAprSubmitCommandBufferAndGetId(CpuContext ctx) + { + var commandBuffer = ctx[CpuRegister.Rdi]; + var outSubmissionId = ctx[CpuRegister.Rdx]; + if (commandBuffer == 0 || outSubmissionId == 0) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + var submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId)); + _submittedCommandBuffers[submissionId] = commandBuffer; + + if (!TryWriteUInt32(ctx, outSubmissionId, submissionId)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + TraceApr(ctx, "submit_get_id", submissionId, commandBuffer, ctx[CpuRegister.Rsi], outSubmissionId); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + private static bool TryWriteAprResult(CpuContext ctx, ulong resultAddress) + { + Span result = stackalloc byte[sizeof(ulong)]; + result.Clear(); + return ctx.Memory.TryWrite(resultAddress, result); + } + + private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value) + { + Span buffer = stackalloc byte[sizeof(uint)]; + BinaryPrimitives.WriteUInt32LittleEndian(buffer, value); + return ctx.Memory.TryWrite(address, buffer); + } + + private static void TraceApr( + CpuContext ctx, + string operation, + uint submissionId, + ulong commandBuffer, + ulong priority, + ulong aux) + { + if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal)) + { + return; + } + + var returnRip = 0UL; + _ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip); + Console.Error.WriteLine( + $"[LOADER][TRACE] apr.{operation}: id=0x{submissionId:X8} cmd=0x{commandBuffer:X16} priority=0x{priority:X16} aux=0x{aux:X16} ret=0x{returnRip:X16}"); + } +} diff --git a/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs new file mode 100644 index 0000000..a724e51 --- /dev/null +++ b/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs @@ -0,0 +1,197 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; +using System.Buffers.Binary; +using System.Threading; + +namespace SharpEmu.Libs.Kernel; + +public static class KernelEventQueueCompatExports +{ + private static readonly object _eventQueueGate = new(); + private static readonly HashSet _eventQueues = new(); + private static long _nextEventQueueHandle = 1; + + [SysAbiExport( + Nid = "D0OdFMjp46I", + ExportName = "sceKernelCreateEqueue", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelCreateEqueue(CpuContext ctx) + { + var outAddress = ctx[CpuRegister.Rdi]; + if (outAddress == 0) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + var handle = unchecked((ulong)Interlocked.Increment(ref _nextEventQueueHandle)); + lock (_eventQueueGate) + { + _eventQueues.Add(handle); + } + + if (!ctx.TryWriteUInt64(outAddress, handle)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + TraceEventQueue(ctx, "create", handle); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "jpFjmgAC5AE", + ExportName = "sceKernelDeleteEqueue", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelDeleteEqueue(CpuContext ctx) + { + var handle = ctx[CpuRegister.Rdi]; + lock (_eventQueueGate) + { + _eventQueues.Remove(handle); + } + + TraceEventQueue(ctx, "delete", handle); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "WDszmSbWuDk", + ExportName = "sceKernelAddUserEventEdge", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelAddUserEventEdge(CpuContext ctx) + { + TraceEventQueue(ctx, "add_user_edge", ctx[CpuRegister.Rdi]); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "4R6-OvI2cEA", + ExportName = "sceKernelAddUserEvent", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelAddUserEvent(CpuContext ctx) + { + TraceEventQueue(ctx, "add_user", ctx[CpuRegister.Rdi]); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "LJDwdSNTnDg", + ExportName = "sceKernelDeleteUserEvent", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelDeleteUserEvent(CpuContext ctx) + { + TraceEventQueue(ctx, "delete_user", ctx[CpuRegister.Rdi]); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "F6e0kwo4cnk", + ExportName = "sceKernelTriggerUserEvent", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelTriggerUserEvent(CpuContext ctx) + { + TraceEventQueue(ctx, "trigger_user", ctx[CpuRegister.Rdi]); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "bBfz7kMF2Ho", + ExportName = "sceKernelAddAmprEvent", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelAddAmprEvent(CpuContext ctx) + { + TraceEventQueue(ctx, "add_ampr", ctx[CpuRegister.Rdi]); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "vuae5JPNt9A", + ExportName = "sceKernelAddAmprSystemEvent", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelAddAmprSystemEvent(CpuContext ctx) + { + TraceEventQueue(ctx, "add_ampr_system", ctx[CpuRegister.Rdi]); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "bMmid3pfyjo", + ExportName = "sceKernelDeleteAmprEvent", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelDeleteAmprEvent(CpuContext ctx) + { + TraceEventQueue(ctx, "delete_ampr", ctx[CpuRegister.Rdi]); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "Ij+ryuEClXQ", + ExportName = "sceKernelDeleteAmprSystemEvent", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelDeleteAmprSystemEvent(CpuContext ctx) + { + TraceEventQueue(ctx, "delete_ampr_system", ctx[CpuRegister.Rdi]); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "QyrxcdBrb0M", + ExportName = "sceKernelGetKqueueFromEqueue", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelGetKqueueFromEqueue(CpuContext ctx) + { + ctx[CpuRegister.Rax] = ctx[CpuRegister.Rdi]; + TraceEventQueue(ctx, "get_kqueue", ctx[CpuRegister.Rdi]); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "fzyMKs9kim0", + ExportName = "sceKernelWaitEqueue", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelWaitEqueue(CpuContext ctx) + { + var outCountAddress = ctx[CpuRegister.Rcx]; + if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, 0)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + TraceEventQueue(ctx, "wait", ctx[CpuRegister.Rdi]); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + private static void TraceEventQueue(CpuContext ctx, string operation, ulong handle) + { + if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EQUEUE"), "1", StringComparison.Ordinal)) + { + return; + } + + var returnRip = 0UL; + _ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip); + Console.Error.WriteLine( + $"[LOADER][TRACE] equeue.{operation}: handle=0x{handle:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} ret=0x{returnRip:X16}"); + } + + private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value) + { + Span buffer = stackalloc byte[sizeof(uint)]; + BinaryPrimitives.WriteUInt32LittleEndian(buffer, value); + return ctx.Memory.TryWrite(address, buffer); + } +} diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index 77b2edf..4d94116 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later using SharpEmu.HLE; +using SharpEmu.Libs.Ampr; using System.Buffers.Binary; using System.Text; using System.Threading; @@ -101,6 +102,7 @@ public static class KernelMemoryCompatExports private static int _inaccessibleMemsetRecoveryCount; private static int _hostMemoryWriteFallbackCount; private static int _hostMemoryReadFallbackCount; + private static int _nullWcscpyRecoveryCount; [StructLayout(LayoutKind.Sequential)] private struct MemoryBasicInformation @@ -364,6 +366,24 @@ public static class KernelMemoryCompatExports private static int WcscpyCore(CpuContext ctx, ulong destination, ulong source) { + if (source == 0) + { + var recoveryIndex = Interlocked.Increment(ref _nullWcscpyRecoveryCount); + if (recoveryIndex <= 8) + { + Console.Error.WriteLine( + $"[LOADER][WARNING] wcscpy null-src recovery#{recoveryIndex}: rip=0x{ctx.Rip:X16} dst=0x{destination:X16}"); + } + + if (!TryWriteWideTerminator(ctx, destination)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + ctx[CpuRegister.Rax] = destination; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + if (!TryReadWideCString(ctx, source, 1_048_576, out var units)) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; @@ -1281,8 +1301,10 @@ public static class KernelMemoryCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; } + var fileId = AmprFileRegistry.Register(guestPath, hostPath); + if (idsAddress != 0 && - !TryWriteUInt32Compat(ctx, idsAddress + (i * sizeof(uint)), ComputeDirectoryEntryHash(Encoding.UTF8.GetBytes(guestPath)))) + !TryWriteUInt32Compat(ctx, idsAddress + (i * sizeof(uint)), fileId)) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } diff --git a/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs index 57e3652..ae5d387 100644 --- a/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs @@ -258,6 +258,8 @@ public static class KernelRuntimeCompatExports } } + TraceProcParam(ctx, address); + ctx[CpuRegister.Rax] = address; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -270,6 +272,223 @@ public static class KernelRuntimeCompatExports } } + private static void TraceProcParam(CpuContext ctx, ulong address) + { + if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PROC_PARAM"), "1", StringComparison.Ordinal)) + { + return; + } + + if (address == 0) + { + Console.Error.WriteLine("[LOADER][TRACE] proc_param: address=0"); + return; + } + + const int dumpSize = 0x200; + var buffer = GC.AllocateUninitializedArray(dumpSize); + if (!ctx.Memory.TryRead(address, buffer)) + { + Console.Error.WriteLine($"[LOADER][TRACE] proc_param: address=0x{address:X16} unreadable"); + return; + } + + Console.Error.WriteLine($"[LOADER][TRACE] proc_param: address=0x{address:X16} size=0x{dumpSize:X}"); + for (var offset = 0; offset < dumpSize; offset += 16) + { + var slice = buffer.AsSpan(offset, 16); + var hex = Convert.ToHexString(slice); + Console.Error.WriteLine($"[LOADER][TRACE] proc_param[{offset:X3}]: {hex}"); + } + + TraceProcParamPointers(ctx, address, buffer); + } + + private static void TraceProcParamPointers(CpuContext ctx, ulong baseAddress, ReadOnlySpan buffer) + { + if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PROC_PARAM_PTRS"), "1", StringComparison.Ordinal)) + { + return; + } + + if (buffer.Length < 0x50) + { + return; + } + + for (var offset = 0x20; offset <= 0x48; offset += 8) + { + var ptr = BinaryPrimitives.ReadUInt64LittleEndian(buffer.Slice(offset, 8)); + Console.Error.WriteLine($"[LOADER][TRACE] proc_param.ptr@{offset:X2}: 0x{ptr:X16}"); + if (ptr == 0) + { + continue; + } + + TraceProcParamPointerTarget(ctx, ptr); + } + } + + private static void TraceProcParamPointerTarget(CpuContext ctx, ulong address) + { + const int maxAsciiBytes = 256; + const int maxWideChars = 128; + + if (TryReadUtf8CString(ctx, address, maxAsciiBytes, out var asciiValue)) + { + Console.Error.WriteLine($"[LOADER][TRACE] proc_param.ptr.target ascii@0x{address:X16}: \"{asciiValue}\""); + return; + } + + if (TryReadUtf16CString(ctx, address, maxWideChars, out var wideValue)) + { + Console.Error.WriteLine($"[LOADER][TRACE] proc_param.ptr.target wide@0x{address:X16}: \"{wideValue}\""); + return; + } + + var preview = GC.AllocateUninitializedArray(64); + if (ctx.Memory.TryRead(address, preview)) + { + var hex = Convert.ToHexString(preview); + Console.Error.WriteLine($"[LOADER][TRACE] proc_param.ptr.target hex@0x{address:X16}: {hex}"); + TraceProcParamEmbeddedPointers(ctx, address, preview); + } + else + { + Console.Error.WriteLine($"[LOADER][TRACE] proc_param.ptr.target unreadable@0x{address:X16}"); + } + } + + private static bool TryReadUtf8CString(CpuContext ctx, ulong address, int maxBytes, out string value) + { + value = string.Empty; + var buffer = GC.AllocateUninitializedArray(maxBytes); + if (!ctx.Memory.TryRead(address, buffer)) + { + return false; + } + + var length = Array.IndexOf(buffer, (byte)0); + if (length < 0) + { + length = maxBytes; + } + + if (length == 0) + { + return false; + } + + var text = Encoding.UTF8.GetString(buffer, 0, length); + if (!IsMostlyPrintable(text)) + { + return false; + } + + value = text; + return true; + } + + private static bool TryReadUtf16CString(CpuContext ctx, ulong address, int maxChars, out string value) + { + value = string.Empty; + var maxBytes = maxChars * 2; + var buffer = GC.AllocateUninitializedArray(maxBytes); + if (!ctx.Memory.TryRead(address, buffer)) + { + return false; + } + + var lengthBytes = -1; + for (var i = 0; i + 1 < buffer.Length; i += 2) + { + if (buffer[i] == 0 && buffer[i + 1] == 0) + { + lengthBytes = i; + break; + } + } + + if (lengthBytes <= 0) + { + return false; + } + + var text = Encoding.Unicode.GetString(buffer, 0, lengthBytes); + if (!IsMostlyPrintable(text)) + { + return false; + } + + value = text; + return true; + } + + private static bool IsMostlyPrintable(string text) + { + if (string.IsNullOrWhiteSpace(text)) + { + return false; + } + + var printable = 0; + for (var i = 0; i < text.Length; i++) + { + var ch = text[i]; + if (ch == '\0') + { + continue; + } + + if (!char.IsControl(ch) || ch == '\r' || ch == '\n' || ch == '\t') + { + printable++; + } + } + + return printable >= Math.Max(4, text.Length * 3 / 4); + } + + private static void TraceProcParamEmbeddedPointers(CpuContext ctx, ulong baseAddress, ReadOnlySpan data) + { + const int maxCandidates = 12; + var found = 0; + Span probe = stackalloc byte[2]; + + for (var offset = 0; offset + 8 <= data.Length; offset += 8) + { + var candidate = BinaryPrimitives.ReadUInt64LittleEndian(data.Slice(offset, 8)); + if (candidate == 0) + { + continue; + } + + if (!ctx.Memory.TryRead(candidate, probe)) + { + continue; + } + + if (TryReadUtf8CString(ctx, candidate, 256, out var ascii)) + { + Console.Error.WriteLine($"[LOADER][TRACE] proc_param.ptr.embed@0x{baseAddress:X16}+0x{offset:X2} -> 0x{candidate:X16} ascii \"{ascii}\""); + if (++found >= maxCandidates) + { + return; + } + continue; + } + + if (TryReadUtf16CString(ctx, candidate, 128, out var wide)) + { + Console.Error.WriteLine($"[LOADER][TRACE] proc_param.ptr.embed@0x{baseAddress:X16}+0x{offset:X2} -> 0x{candidate:X16} wide \"{wide}\""); + if (++found >= maxCandidates) + { + return; + } + } + } + } + [SysAbiExport( Nid = "9BcDykPmo1I", ExportName = "__error",