diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs index 5aae604..262c4f3 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs @@ -154,15 +154,57 @@ public sealed partial class DirectExecutionBackend bool flag = num7 >= 2156221920u && num7 <= 2156225024u; bool flag2 = num7 >= 2156351360u && num7 <= 2156352080u; bool flag3 = num >= 1020 && num <= 1040; - if (!flag0 && (num <= 128 || (num >= 240 && num <= 400) || (num >= 900 && num <= 1300) || num % 100000 == 0L || (importStubEntry.Nid == "tsvEmnenz48" && (num <= 256 || num % 1000 == 0L)) || (importStubEntry.Nid == "rTXw65xmLIA" && (num <= 256 || num % 128 == 0)) || flag || flag2 || flag3)) + bool logAllImports = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_ALL_IMPORTS"), "1", StringComparison.Ordinal); + string importFilter = Environment.GetEnvironmentVariable("SHARPEMU_LOG_IMPORT_FILTER"); + bool flag4 = !string.IsNullOrWhiteSpace(importFilter); + bool flag5 = false; + ExportedFunction matchedExport = null; + if (_moduleManager.TryGetExport(importStubEntry.Nid, out ExportedFunction export)) { - if (_moduleManager.TryGetExport(importStubEntry.Nid, out ExportedFunction export)) + matchedExport = export; + if (flag4) { - Console.Error.WriteLine($"[LOADER][TRACE] Import#{num}: {export.LibraryName}:{export.Name} ({importStubEntry.Nid})"); + flag5 = export.LibraryName.Contains(importFilter, StringComparison.OrdinalIgnoreCase) + || export.Name.Contains(importFilter, StringComparison.OrdinalIgnoreCase) + || importStubEntry.Nid.Contains(importFilter, StringComparison.OrdinalIgnoreCase); + } + } + else if (flag4) + { + flag5 = importStubEntry.Nid.Contains(importFilter, StringComparison.OrdinalIgnoreCase); + } + bool flag6 = logAllImports || flag5; + if (!flag0 && (flag6 || num <= 128 || (num >= 240 && num <= 400) || (num >= 900 && num <= 1300) || num % 100000 == 0L || (importStubEntry.Nid == "tsvEmnenz48" && (num <= 256 || num % 1000 == 0L)) || (importStubEntry.Nid == "rTXw65xmLIA" && (num <= 256 || num % 128 == 0)) || flag || flag2 || flag3)) + { + if (matchedExport != null) + { + if (flag6) + { + Console.Error.WriteLine( + $"[LOADER][TRACE] Import#{num}: {matchedExport.LibraryName}:{matchedExport.Name} ({importStubEntry.Nid}) " + + $"rdi=0x{value:X16} rsi=0x{value2:X16} rdx=0x{num3:X16} rcx=0x{num4:X16} ret=0x{num7:X16}"); + } + else + { + Console.Error.WriteLine($"[LOADER][TRACE] Import#{num}: {matchedExport.LibraryName}:{matchedExport.Name} ({importStubEntry.Nid})"); + } } else { - Console.Error.WriteLine($"[LOADER][TRACE] Import#{num}: {importStubEntry.Nid}"); + if (flag6) + { + Console.Error.WriteLine( + $"[LOADER][TRACE] Import#{num}: {importStubEntry.Nid} " + + $"rdi=0x{value:X16} rsi=0x{value2:X16} rdx=0x{num3:X16} rcx=0x{num4:X16} ret=0x{num7:X16}"); + } + else + { + Console.Error.WriteLine($"[LOADER][TRACE] Import#{num}: {importStubEntry.Nid}"); + } + } + if (flag6) + { + Console.Error.Flush(); } } if (!flag0) @@ -212,25 +254,6 @@ public sealed partial class DirectExecutionBackend } TryBypassStackChkFailTrap(num, num7); } - if (importStubEntry.Nid == "9rAeANT2tyE" || importStubEntry.Nid == "1j3S3n-tTW4") - { - ulong rspDbg = _cpuContext[CpuRegister.Rsp]; - Console.Error.WriteLine( - $"[LOADER][TRACE] ImportDbg#{num}: nid={importStubEntry.Nid} " + - $"ret=0x{num7:X16} rsp=0x{rspDbg:X16} rsp&7=0x{(rspDbg & 7):X} rsp&F=0x{(rspDbg & 0xF):X}"); - - if (_cpuContext.TryReadUInt64(rspDbg, out var s0)) - Console.Error.WriteLine($"[LOADER][TRACE] [rsp+0x00] = 0x{s0:X16}"); - if (_cpuContext.TryReadUInt64(rspDbg + 8, out var s8)) - Console.Error.WriteLine($"[LOADER][TRACE] [rsp+0x08] = 0x{s8:X16}"); - if (_cpuContext.TryReadUInt64(rspDbg + 16, out var s10)) - Console.Error.WriteLine($"[LOADER][TRACE] [rsp+0x10] = 0x{s10:X16}"); - if (_cpuContext.TryReadUInt64(rspDbg + 24, out var s18)) - Console.Error.WriteLine($"[LOADER][TRACE] [rsp+0x18] = 0x{s18:X16}"); - - ProbeReturnRip(num7, num); - Console.Error.Flush(); - } try { OrbisGen2Result orbisGen2Result; diff --git a/src/SharpEmu.Core/Runtime/SharpEmuRuntime.cs b/src/SharpEmu.Core/Runtime/SharpEmuRuntime.cs index 329c9b1..f2f556b 100644 --- a/src/SharpEmu.Core/Runtime/SharpEmuRuntime.cs +++ b/src/SharpEmu.Core/Runtime/SharpEmuRuntime.cs @@ -125,6 +125,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime public OrbisGen2Result Run(string ebootPath) { + var normalizedEbootPath = Path.GetFullPath(ebootPath); + using var app0Binding = BindApp0Root(normalizedEbootPath); Console.Error.WriteLine($"[RUNTIME] Loading: {ebootPath}"); LastExecutionDiagnostics = null; LastExecutionTrace = null; @@ -132,8 +134,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime LastBasicBlockTrace = null; LastMilestoneLog = null; KernelModuleRegistry.Reset(); - var image = LoadImage(ebootPath); - var normalizedEbootPath = Path.GetFullPath(ebootPath); + var image = LoadImage(normalizedEbootPath); RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false); KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress); Console.Error.WriteLine($"[RUNTIME] Entry: 0x{image.EntryPoint:X16}"); @@ -350,6 +351,41 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime return result; } + private static App0BindingScope? BindApp0Root(string normalizedEbootPath) + { + const string app0VariableName = "SHARPEMU_APP0_DIR"; + if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(app0VariableName))) + { + return null; + } + + var app0Root = Path.GetDirectoryName(normalizedEbootPath); + if (string.IsNullOrWhiteSpace(app0Root)) + { + return null; + } + + Environment.SetEnvironmentVariable(app0VariableName, app0Root); + return new App0BindingScope(app0VariableName); + } + + private sealed class App0BindingScope(string variableName) : IDisposable + { + private readonly string _variableName = variableName; + private bool _disposed; + + public void Dispose() + { + if (_disposed) + { + return; + } + + Environment.SetEnvironmentVariable(_variableName, null); + _disposed = true; + } + } + private OrbisGen2Result? RunAllInitializers( SelfImage mainImage, IReadOnlyList loadedModuleImages, diff --git a/src/SharpEmu.Libs/AppContent/AppContentExports.cs b/src/SharpEmu.Libs/AppContent/AppContentExports.cs index fb06634..c0b841b 100644 --- a/src/SharpEmu.Libs/AppContent/AppContentExports.cs +++ b/src/SharpEmu.Libs/AppContent/AppContentExports.cs @@ -2,11 +2,41 @@ // SPDX-License-Identifier: GPL-2.0-or-later using SharpEmu.HLE; +using System.Buffers.Binary; +using System.Text; namespace SharpEmu.Libs.AppContent; public static class AppContentExports { + private const ulong BootParamAttrOffset = 4; + private const string Temp0MountPoint = "/temp0"; + + [SysAbiExport( + Nid = "R9lA82OraNs", + ExportName = "sceAppContentInitialize", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceAppContent")] + public static int AppContentInitialize(CpuContext ctx) + { + var initParamAddress = ctx[CpuRegister.Rdi]; + var bootParamAddress = ctx[CpuRegister.Rsi]; + if (initParamAddress == 0 || bootParamAddress == 0) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + Span attrBytes = stackalloc byte[sizeof(uint)]; + BinaryPrimitives.WriteUInt32LittleEndian(attrBytes, 0); + if (!ctx.Memory.TryWrite(bootParamAddress + BootParamAttrOffset, attrBytes)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + [SysAbiExport( Nid = "xnd8BJzAxmk", ExportName = "sceAppContentGetAddcontInfoList", @@ -14,7 +44,67 @@ public static class AppContentExports LibraryName = "libSceAppContent")] public static int AppContentGetAddcontInfoList(CpuContext ctx) { - _ = ctx; + var hitCountAddress = ctx[CpuRegister.Rcx]; + if (hitCountAddress != 0) + { + Span hitCountBytes = stackalloc byte[sizeof(uint)]; + BinaryPrimitives.WriteUInt32LittleEndian(hitCountBytes, 0); + if (!ctx.Memory.TryWrite(hitCountAddress, hitCountBytes)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + } + + ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + + [SysAbiExport( + Nid = "buYbeLOGWmA", + ExportName = "sceAppContentTemporaryDataMount2", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceAppContent")] + public static int AppContentTemporaryDataMount2(CpuContext ctx) + { + var mountPointAddress = ctx[CpuRegister.Rsi]; + if (mountPointAddress == 0) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + Directory.CreateDirectory(ResolveTemp0Root()); + var mountPointBytes = Encoding.ASCII.GetBytes($"{Temp0MountPoint}\0"); + if (!ctx.Memory.TryWrite(mountPointAddress, mountPointBytes)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + private static string ResolveTemp0Root() + { + const string temp0VariableName = "SHARPEMU_TEMP0_DIR"; + var configuredRoot = Environment.GetEnvironmentVariable(temp0VariableName); + if (!string.IsNullOrWhiteSpace(configuredRoot)) + { + return Path.GetFullPath(configuredRoot); + } + + var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR"); + var appName = string.IsNullOrWhiteSpace(app0Root) + ? "default" + : Path.GetFileName(Path.TrimEndingDirectorySeparator(app0Root)); + if (string.IsNullOrWhiteSpace(appName)) + { + appName = "default"; + } + + var invalidChars = Path.GetInvalidFileNameChars(); + appName = new string(appName.Select(ch => invalidChars.Contains(ch) ? '_' : ch).ToArray()); + var root = Path.Combine(Path.GetTempPath(), "SharpEmu", appName, "temp0"); + Environment.SetEnvironmentVariable(temp0VariableName, root); + return root; + } } diff --git a/src/SharpEmu.Libs/Kernel/KernelExports.cs b/src/SharpEmu.Libs/Kernel/KernelExports.cs index 3d910ff..2e20a14 100644 --- a/src/SharpEmu.Libs/Kernel/KernelExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelExports.cs @@ -290,84 +290,17 @@ public static class KernelExports { ulong fmtPtr = ctx[CpuRegister.Rdi]; string fmt = ReadCString(ctx, fmtPtr, 4096); - - ulong[] args = + string outStr = KernelMemoryCompatExports.FormatStringFromVarArgs(ctx, fmt, firstGpArgIndex: 1); + if (outStr.EndsWith('\n') || outStr.EndsWith('\r')) { - ctx[CpuRegister.Rsi], - ctx[CpuRegister.Rdx], - ctx[CpuRegister.Rcx], - ctx[CpuRegister.R8], - ctx[CpuRegister.R9], - }; - - int argIndex = 0; - var sb = new System.Text.StringBuilder(fmt.Length + 64); - - for (int i = 0; i < fmt.Length; i++) + Console.Write($"[DEBUG][PRINF] {outStr}"); + } + else { - char ch = fmt[i]; - if (ch != '%') - { - sb.Append(ch); - continue; - } - - if (i + 1 < fmt.Length && fmt[i + 1] == '%') - { - sb.Append('%'); - i++; - continue; - } - - int j = i + 1; - while (j < fmt.Length && "-+ #0".IndexOf(fmt[j]) >= 0) j++; - while (j < fmt.Length && char.IsDigit(fmt[j])) j++; - - if (j < fmt.Length && fmt[j] == '.') - { - j++; - while (j < fmt.Length && char.IsDigit(fmt[j])) j++; - } - - if (j >= fmt.Length) break; - char spec = fmt[j]; - i = j; - - ulong a = argIndex < args.Length ? args[argIndex++] : 0; - - switch (spec) - { - case 's': - sb.Append(a == 0 ? "(null)" : ReadCString(ctx, a, 4096)); - break; - - case 'd': - case 'i': - sb.Append(unchecked((int)a)); - break; - - case 'u': - sb.Append(unchecked((uint)a)); - break; - - case 'x': - sb.Append(unchecked((uint)a).ToString("x")); - break; - - case 'p': - sb.Append("0x").Append(a.ToString("x16")); - break; - - default: - sb.Append('%').Append(spec); - break; - } + Console.WriteLine($"[DEBUG][PRINF] {outStr}"); } - string outStr = sb.ToString(); - Console.WriteLine($"[DEBUG][PRINF] {outStr}"); - - ctx[CpuRegister.Rax] = (ulong)outStr.Length; + ctx[CpuRegister.Rax] = (ulong)System.Text.Encoding.UTF8.GetByteCount(outStr); return (int)OrbisGen2Result.ORBIS_GEN2_OK; } diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index f829eb3..474c103 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -7,12 +7,14 @@ using System.Text; using System.Threading; using System.Reflection; using System.Runtime.InteropServices; +using System.Linq; namespace SharpEmu.Libs.Kernel; public static class KernelMemoryCompatExports { private const int MaxGuestStringLength = 4096; + private const int WideCharSize = sizeof(ushort); private const int MemsetChunkSize = 16 * 1024; private const int TlsModuleBlockSize = 0x10000; private const int O_WRONLY = 0x1; @@ -51,6 +53,26 @@ public static class KernelMemoryCompatExports private const int Enomem = 12; private const int Einval = 22; private const nuint DefaultLibcHeapAlignment = 16; + private const ushort KernelStatModeDirectory = 0x41FF; + private const ushort KernelStatModeRegular = 0x81FF; + private const int KernelStatSize = 120; + private const int KernelStatStDevOffset = 0; + private const int KernelStatStInoOffset = 4; + private const int KernelStatStModeOffset = 8; + private const int KernelStatStNlinkOffset = 10; + private const int KernelStatStUidOffset = 12; + private const int KernelStatStGidOffset = 16; + private const int KernelStatStRdevOffset = 20; + private const int KernelStatStAtimOffset = 24; + private const int KernelStatStMtimOffset = 40; + private const int KernelStatStCtimOffset = 56; + private const int KernelStatStSizeOffset = 72; + private const int KernelStatStBlocksOffset = 80; + private const int KernelStatStBlksizeOffset = 88; + private const int KernelStatStFlagsOffset = 92; + private const int KernelStatStGenOffset = 96; + private const int KernelStatStLspareOffset = 100; + private const int KernelStatStBirthtimOffset = 104; private static readonly object _fdGate = new(); private static readonly Dictionary _openFiles = new(); @@ -232,8 +254,24 @@ public static class KernelMemoryCompatExports LibraryName = "libc")] public static int Wcslen(CpuContext ctx) { - if (!TryReadWideCString(ctx, ctx[CpuRegister.Rdi], 1_048_576, out var units)) + var address = ctx[CpuRegister.Rdi]; + if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_WIDE"), "1", StringComparison.Ordinal)) { + Span probe = stackalloc byte[32]; + if (TryReadCompat(ctx, address, probe)) + { + Console.Error.WriteLine( + $"[LOADER][TRACE] wcslen probe @0x{address:X16}: {Convert.ToHexString(probe).ToLowerInvariant()}"); + } + else + { + Console.Error.WriteLine($"[LOADER][TRACE] wcslen probe @0x{address:X16}: "); + } + } + + if (!TryReadWideCString(ctx, address, 1_048_576, out var units)) + { + Console.Error.WriteLine($"[LOADER][WARN] wcslen: unreadable string at 0x{address:X16}"); return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } @@ -468,21 +506,21 @@ public static class KernelMemoryCompatExports var destination = ctx[CpuRegister.Rdi]; var source = ctx[CpuRegister.Rsi]; var count = (int)Math.Min(ctx[CpuRegister.Rdx], int.MaxValue); - if (count < 0 || count > (int.MaxValue / sizeof(ushort))) + if (count < 0 || count > (int.MaxValue / WideCharSize)) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; } - var payload = new byte[count * sizeof(ushort)]; + var payload = new byte[count * WideCharSize]; for (var copied = 0; copied < count; copied++) { - if (!TryReadUInt16Compat(ctx, source + ((ulong)copied * sizeof(ushort)), out var unit)) + if (!TryReadUInt16Compat(ctx, source + ((ulong)copied * WideCharSize), out var unit)) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } BinaryPrimitives.WriteUInt16LittleEndian( - payload.AsSpan(copied * sizeof(ushort), sizeof(ushort)), + payload.AsSpan(copied * WideCharSize, WideCharSize), unit); if (unit == 0) @@ -511,14 +549,14 @@ public static class KernelMemoryCompatExports var needle = unchecked((ushort)ctx[CpuRegister.Rsi]); for (ulong index = 0; index < 1_048_576; index++) { - if (!TryReadUInt16Compat(ctx, address + (index * sizeof(ushort)), out var unit)) + if (!TryReadUInt16Compat(ctx, address + (index * WideCharSize), out var unit)) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } if (unit == needle) { - ctx[CpuRegister.Rax] = address + (index * sizeof(ushort)); + ctx[CpuRegister.Rax] = address + (index * WideCharSize); return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -814,6 +852,38 @@ public static class KernelMemoryCompatExports var mode = ResolveOpenMode(flags, access); try { + var wantsDirectory = (flags & O_DIRECTORY) != 0; + if (wantsDirectory || Directory.Exists(hostPath)) + { + if (!Directory.Exists(hostPath)) + { + LogOpenTrace($"_open miss path='{guestPath}' host='{hostPath}' flags=0x{flags:X8} directory=1"); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; + } + + if (access != FileAccess.Read || (flags & (O_CREAT | O_TRUNC | O_APPEND)) != 0) + { + LogOpenTrace($"_open invalid-dir path='{guestPath}' host='{hostPath}' flags=0x{flags:X8}"); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + var directoryFd = (int)Interlocked.Increment(ref _nextFileDescriptor); + lock (_fdGate) + { + _openDirectories[directoryFd] = new OpenDirectory + { + Path = hostPath, + Entries = EnumerateDirectoryEntries(hostPath), + NextIndex = 0 + }; + } + + LogOpenTrace($"_open dir path='{guestPath}' host='{hostPath}' flags=0x{flags:X8} fd={directoryFd}"); + ctx[CpuRegister.Rax] = unchecked((ulong)directoryFd); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + EnsureOpenParentDirectoryExists(guestPath, hostPath, flags); var stream = new FileStream(hostPath, mode, access, FileShare.ReadWrite); if ((flags & O_APPEND) != 0) { @@ -826,16 +896,16 @@ public static class KernelMemoryCompatExports _openFiles[fd] = stream; } + LogOpenTrace($"_open file path='{guestPath}' host='{hostPath}' flags=0x{flags:X8} fd={fd}"); ctx[CpuRegister.Rax] = unchecked((ulong)fd); return (int)OrbisGen2Result.ORBIS_GEN2_OK; } - catch (IOException) + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; - } - catch (UnauthorizedAccessException) - { - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + LogOpenTrace($"_open fail path='{guestPath}' host='{hostPath}' flags=0x{flags:X8} ex={ex.GetType().Name}: {ex.Message}"); + return ex is UnauthorizedAccessException + ? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT + : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; } } @@ -844,9 +914,69 @@ public static class KernelMemoryCompatExports ExportName = "_close", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] - public static int KernelCloseUnderscore(CpuContext ctx) + public static int KernelCloseUnderscore(CpuContext ctx) => KernelCloseCore(ctx, unchecked((int)ctx[CpuRegister.Rdi])); + + [SysAbiExport( + Nid = "UK2Tl2DWUns", + ExportName = "sceKernelClose", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelClose(CpuContext ctx) => KernelCloseCore(ctx, unchecked((int)ctx[CpuRegister.Rdi])); + + [SysAbiExport( + Nid = "eV9wAD2riIA", + ExportName = "sceKernelStat", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelStat(CpuContext ctx) + { + var pathAddress = ctx[CpuRegister.Rdi]; + var statAddress = ctx[CpuRegister.Rsi]; + if (pathAddress == 0 || statAddress == 0) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + if (!TryReadNullTerminatedUtf8(ctx, pathAddress, MaxGuestStringLength, out var guestPath)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + var hostPath = ResolveGuestPath(guestPath); + if (!TryWriteHostPathStat(ctx, statAddress, hostPath)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; + } + + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "kBwCPsYX-m4", + ExportName = "sceKernelFstat", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelFstat(CpuContext ctx) { var fd = unchecked((int)ctx[CpuRegister.Rdi]); + var statAddress = ctx[CpuRegister.Rsi]; + if (statAddress == 0) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + if (!TryWriteOpenDescriptorStat(ctx, fd, statAddress)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; + } + + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + private static int KernelCloseCore(CpuContext ctx, int fd) + { if (fd is 0 or 1 or 2) { ctx[CpuRegister.Rax] = 0; @@ -856,7 +986,15 @@ public static class KernelMemoryCompatExports FileStream? stream; lock (_fdGate) { - if (!_openFiles.Remove(fd, out stream)) + if (_openFiles.Remove(fd, out stream)) + { + } + else if (_openDirectories.Remove(fd)) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + else { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; } @@ -910,6 +1048,36 @@ public static class KernelMemoryCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + [SysAbiExport( + Nid = "taRWhTJFTgE", + ExportName = "sceKernelGetdirentries", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelGetdirentries(CpuContext ctx) + { + return KernelGetdirentriesCore( + ctx, + unchecked((int)ctx[CpuRegister.Rdi]), + ctx[CpuRegister.Rsi], + unchecked((int)Math.Min(ctx[CpuRegister.Rdx], int.MaxValue)), + ctx[CpuRegister.Rcx]); + } + + [SysAbiExport( + Nid = "j2AIqSqJP0w", + ExportName = "sceKernelGetdents", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelGetdents(CpuContext ctx) + { + return KernelGetdirentriesCore( + ctx, + unchecked((int)ctx[CpuRegister.Rdi]), + ctx[CpuRegister.Rsi], + unchecked((int)Math.Min(ctx[CpuRegister.Rdx], int.MaxValue)), + 0); + } + [SysAbiExport( Nid = "FxVZqBAA7ks", ExportName = "_write", @@ -1883,9 +2051,9 @@ public static class KernelMemoryCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } - private static string FormatString(CpuContext ctx, string format) + internal static string FormatStringFromVarArgs(CpuContext ctx, string format, int firstGpArgIndex) { - var gpIndex = 3; // RDI, RSI, RDX used by snprintf itself, args start at RCX (index 3) + var gpIndex = Math.Max(0, firstGpArgIndex); ulong GetGpArg(int index) { @@ -1911,6 +2079,11 @@ public static class KernelMemoryCompatExports return FormatString(ctx, format, NextGpArg, NextFloatArg); } + private static string FormatString(CpuContext ctx, string format) + { + return FormatStringFromVarArgs(ctx, format, firstGpArgIndex: 3); + } + private static string FormatString( CpuContext ctx, string format, @@ -2137,6 +2310,20 @@ public static class KernelMemoryCompatExports { sb.Append("(null)"); } + else if (lengthMod == "l") + { + if (TryReadWideCString(ctx, strAddr, 1_048_576, out var wideUnits)) + { + var str = DecodeWideUnits(wideUnits); + if (precision >= 0 && str.Length > precision) + str = str.Substring(0, precision); + sb.Append(PadString(str, width, leftAlign, false)); + } + else + { + sb.Append("(null)"); + } + } else if (TryReadCString(ctx, strAddr, 1_048_576, out var strBytes)) { var str = Encoding.UTF8.GetString(strBytes); @@ -2153,8 +2340,20 @@ public static class KernelMemoryCompatExports case 'c': { - var ch = (char)(byte)nextGpArg(); - sb.Append(PadString(ch.ToString(), width, leftAlign, false)); + string renderedChar; + if (lengthMod == "l") + { + var scalar = unchecked((ushort)nextGpArg()); + renderedChar = TryConvertWideScalarToString(scalar, out var wideCharText) + ? wideCharText + : "?"; + } + else + { + renderedChar = ((char)(byte)nextGpArg()).ToString(); + } + + sb.Append(PadString(renderedChar, width, leftAlign, false)); } break; @@ -2510,6 +2709,37 @@ public static class KernelMemoryCompatExports return guestPath; } + var devlogAppRoot = ResolveDevlogAppRoot(); + if (guestPath.StartsWith("/devlog/app/", StringComparison.OrdinalIgnoreCase)) + { + var relative = guestPath["/devlog/app/".Length..].Replace('/', Path.DirectorySeparatorChar); + return Path.Combine(devlogAppRoot, relative); + } + + if (guestPath.StartsWith("devlog/app/", StringComparison.OrdinalIgnoreCase)) + { + var relative = guestPath["devlog/app/".Length..].Replace('/', Path.DirectorySeparatorChar); + return Path.Combine(devlogAppRoot, relative); + } + + if (string.Equals(guestPath, "/devlog/app", StringComparison.OrdinalIgnoreCase) || + string.Equals(guestPath, "devlog/app", StringComparison.OrdinalIgnoreCase)) + { + return devlogAppRoot; + } + + var temp0Root = ResolveTemp0Root(); + if (guestPath.StartsWith("/temp0/", StringComparison.OrdinalIgnoreCase)) + { + var relative = guestPath["/temp0/".Length..].Replace('/', Path.DirectorySeparatorChar); + return Path.Combine(temp0Root, relative); + } + + if (string.Equals(guestPath, "/temp0", StringComparison.OrdinalIgnoreCase)) + { + return temp0Root; + } + var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR"); if (!string.IsNullOrWhiteSpace(app0Root)) { @@ -2529,6 +2759,71 @@ public static class KernelMemoryCompatExports return guestPath; } + private static string ResolveDevlogAppRoot() + { + var configuredRoot = Environment.GetEnvironmentVariable("SHARPEMU_DEVLOG_APP_DIR"); + string root; + if (!string.IsNullOrWhiteSpace(configuredRoot)) + { + root = Path.GetFullPath(configuredRoot); + } + else + { + root = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, "logs", "devlog", "app")); + } + + Directory.CreateDirectory(root); + return root; + } + + private static string ResolveTemp0Root() + { + const string temp0VariableName = "SHARPEMU_TEMP0_DIR"; + var configuredRoot = Environment.GetEnvironmentVariable(temp0VariableName); + if (!string.IsNullOrWhiteSpace(configuredRoot)) + { + return Path.GetFullPath(configuredRoot); + } + + var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR"); + var appName = string.IsNullOrWhiteSpace(app0Root) + ? "default" + : Path.GetFileName(Path.TrimEndingDirectorySeparator(app0Root)); + if (string.IsNullOrWhiteSpace(appName)) + { + appName = "default"; + } + + var invalidChars = Path.GetInvalidFileNameChars(); + appName = new string(appName.Select(ch => invalidChars.Contains(ch) ? '_' : ch).ToArray()); + var root = Path.Combine(Path.GetTempPath(), "SharpEmu", appName, "temp0"); + Environment.SetEnvironmentVariable(temp0VariableName, root); + return root; + } + + private static void EnsureOpenParentDirectoryExists(string guestPath, string hostPath, int flags) + { + if (string.IsNullOrWhiteSpace(hostPath)) + { + return; + } + + var shouldCreateParent = + (flags & O_CREAT) != 0 || + guestPath.StartsWith("/devlog/app/", StringComparison.OrdinalIgnoreCase) || + guestPath.StartsWith("devlog/app/", StringComparison.OrdinalIgnoreCase); + if (!shouldCreateParent) + { + return; + } + + var parentDirectory = Path.GetDirectoryName(hostPath); + if (!string.IsNullOrWhiteSpace(parentDirectory)) + { + Directory.CreateDirectory(parentDirectory); + } + } + private static bool TryReadCString(CpuContext ctx, ulong address, ulong maxLength, out byte[] bytes) { bytes = Array.Empty(); @@ -2602,7 +2897,7 @@ public static class KernelMemoryCompatExports var buffer = new List(Math.Min(limit, 256)); for (var i = 0; i < limit; i++) { - if (!TryReadUInt16Compat(ctx, address + ((ulong)i * sizeof(ushort)), out var unit)) + if (!TryReadUInt16Compat(ctx, address + ((ulong)i * WideCharSize), out var unit)) { return false; } @@ -2631,13 +2926,13 @@ public static class KernelMemoryCompatExports var max = limit == ulong.MaxValue ? 1_048_576UL : Math.Min(limit, 1_048_576UL); for (ulong i = 0; i < max; i++) { - if (!TryReadUInt16Compat(ctx, left + (i * sizeof(ushort)), out var leftUnit) || - !TryReadUInt16Compat(ctx, right + (i * sizeof(ushort)), out var rightUnit)) + if (!TryReadUInt16Compat(ctx, left + (i * WideCharSize), out var leftUnit) || + !TryReadUInt16Compat(ctx, right + (i * WideCharSize), out var rightUnit)) { return false; } - compare = leftUnit - rightUnit; + compare = leftUnit == rightUnit ? 0 : leftUnit < rightUnit ? -1 : 1; if (compare != 0 || leftUnit == 0 || rightUnit == 0) { return true; @@ -2650,20 +2945,36 @@ public static class KernelMemoryCompatExports private static byte[] EncodeWideUnits(ReadOnlySpan units) { - var bytes = new byte[units.Length * sizeof(ushort)]; + var bytes = new byte[units.Length * WideCharSize]; for (var i = 0; i < units.Length; i++) { BinaryPrimitives.WriteUInt16LittleEndian( - bytes.AsSpan(i * sizeof(ushort), sizeof(ushort)), + bytes.AsSpan(i * WideCharSize, WideCharSize), units[i]); } return bytes; } + private static string DecodeWideUnits(ReadOnlySpan units) + { + if (units.IsEmpty) + { + return string.Empty; + } + + return new string(MemoryMarshal.Cast(units)); + } + + private static bool TryConvertWideScalarToString(ushort scalar, out string text) + { + text = ((char)scalar).ToString(); + return true; + } + private static byte[] EncodeWideUnitsWithTerminator(ReadOnlySpan units) { - var bytes = new byte[(units.Length + 1) * sizeof(ushort)]; + var bytes = new byte[(units.Length + 1) * WideCharSize]; EncodeWideUnits(units).CopyTo(bytes, 0); return bytes; } @@ -3501,6 +3812,202 @@ public static class KernelMemoryCompatExports return ctx.Memory.TryWrite(address, bytes); } + private static bool TryWriteOpenDescriptorStat(CpuContext ctx, int fd, ulong statAddress) + { + if (fd is 0 or 1 or 2) + { + var now = DateTime.UtcNow; + return TryWriteKernelStat(ctx, statAddress, isDirectory: false, size: 0, now, now, now, $"stdio:{fd}"); + } + + string? hostPath = null; + bool isDirectory = false; + lock (_fdGate) + { + if (_openDirectories.TryGetValue(fd, out var directory)) + { + hostPath = directory.Path; + isDirectory = true; + } + else if (_openFiles.TryGetValue(fd, out var stream)) + { + hostPath = stream.Name; + } + } + + return !string.IsNullOrWhiteSpace(hostPath) && TryWriteHostPathStat(ctx, statAddress, hostPath!, isDirectory); + } + + private static bool TryWriteHostPathStat(CpuContext ctx, ulong statAddress, string hostPath) + { + var isDirectory = Directory.Exists(hostPath); + if (!isDirectory && !File.Exists(hostPath)) + { + return false; + } + + return TryWriteHostPathStat(ctx, statAddress, hostPath, isDirectory); + } + + private static bool TryWriteHostPathStat(CpuContext ctx, ulong statAddress, string hostPath, bool isDirectory) + { + if (isDirectory) + { + if (!Directory.Exists(hostPath)) + { + return false; + } + } + else if (!File.Exists(hostPath)) + { + return false; + } + + try + { + var lastAccessUtc = File.GetLastAccessTimeUtc(hostPath); + var lastWriteUtc = File.GetLastWriteTimeUtc(hostPath); + var creationUtc = File.GetCreationTimeUtc(hostPath); + var size = isDirectory ? 65536L : new FileInfo(hostPath).Length; + return TryWriteKernelStat(ctx, statAddress, isDirectory, size, lastAccessUtc, lastWriteUtc, creationUtc, hostPath); + } + catch + { + return false; + } + } + + private static bool TryWriteKernelStat( + CpuContext ctx, + ulong statAddress, + bool isDirectory, + long size, + DateTime lastAccessUtc, + DateTime lastWriteUtc, + DateTime creationUtc, + string inodeSeed) + { + Span payload = stackalloc byte[KernelStatSize]; + payload.Clear(); + + var seedBytes = Encoding.UTF8.GetBytes(inodeSeed); + BinaryPrimitives.WriteUInt32LittleEndian(payload[KernelStatStDevOffset..], 0); + BinaryPrimitives.WriteUInt32LittleEndian(payload[KernelStatStInoOffset..], ComputeDirectoryEntryHash(seedBytes)); + BinaryPrimitives.WriteUInt16LittleEndian(payload[KernelStatStModeOffset..], isDirectory ? KernelStatModeDirectory : KernelStatModeRegular); + BinaryPrimitives.WriteUInt16LittleEndian(payload[KernelStatStNlinkOffset..], 1); + BinaryPrimitives.WriteUInt32LittleEndian(payload[KernelStatStUidOffset..], 0); + BinaryPrimitives.WriteUInt32LittleEndian(payload[KernelStatStGidOffset..], 0); + BinaryPrimitives.WriteUInt32LittleEndian(payload[KernelStatStRdevOffset..], 0); + WriteKernelTimespec(payload[KernelStatStAtimOffset..], lastAccessUtc); + WriteKernelTimespec(payload[KernelStatStMtimOffset..], lastWriteUtc); + WriteKernelTimespec(payload[KernelStatStCtimOffset..], lastWriteUtc); + BinaryPrimitives.WriteInt64LittleEndian(payload[KernelStatStSizeOffset..], size); + BinaryPrimitives.WriteInt64LittleEndian(payload[KernelStatStBlocksOffset..], isDirectory ? 128 : (size + 511) / 512); + BinaryPrimitives.WriteUInt32LittleEndian(payload[KernelStatStBlksizeOffset..], isDirectory ? 65536U : 512U); + BinaryPrimitives.WriteUInt32LittleEndian(payload[KernelStatStFlagsOffset..], 0); + BinaryPrimitives.WriteUInt32LittleEndian(payload[KernelStatStGenOffset..], 0); + BinaryPrimitives.WriteInt32LittleEndian(payload[KernelStatStLspareOffset..], 0); + WriteKernelTimespec(payload[KernelStatStBirthtimOffset..], creationUtc); + return TryWriteCompat(ctx, statAddress, payload); + } + + private static void WriteKernelTimespec(Span destination, DateTime utcTime) + { + var timestamp = utcTime.Kind == DateTimeKind.Utc ? utcTime : utcTime.ToUniversalTime(); + var dto = new DateTimeOffset(timestamp); + BinaryPrimitives.WriteInt64LittleEndian(destination, dto.ToUnixTimeSeconds()); + var ticksWithinSecond = timestamp.Ticks % TimeSpan.TicksPerSecond; + BinaryPrimitives.WriteInt64LittleEndian(destination[sizeof(long)..], ticksWithinSecond * 100); + } + + private static int KernelGetdirentriesCore(CpuContext ctx, int fd, ulong bufferAddress, int requested, ulong basePointerAddress) + { + if (fd < 0 || bufferAddress == 0 || requested < 512) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + OpenDirectory? directory; + lock (_fdGate) + { + _openDirectories.TryGetValue(fd, out directory); + } + + if (directory is null) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; + } + + var currentIndex = directory.NextIndex; + if (basePointerAddress != 0 && !TryWriteUInt64Compat(ctx, basePointerAddress, (ulong)currentIndex)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + if (currentIndex >= directory.Entries.Length) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + var entryName = directory.Entries[currentIndex]; + directory.NextIndex = currentIndex + 1; + + var entryBytes = Encoding.UTF8.GetBytes(entryName); + var nameLength = Math.Min(entryBytes.Length, 255); + var entryPath = Path.Combine(directory.Path, entryName); + var entryType = Directory.Exists(entryPath) ? (byte)4 : (byte)8; + + var payload = new byte[512]; + BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(0, sizeof(uint)), ComputeDirectoryEntryHash(entryBytes.AsSpan(0, nameLength))); + BinaryPrimitives.WriteUInt16LittleEndian(payload.AsSpan(4, sizeof(ushort)), 512); + payload[6] = entryType; + payload[7] = unchecked((byte)nameLength); + entryBytes.AsSpan(0, nameLength).CopyTo(payload.AsSpan(8)); + + if (!TryWriteCompat(ctx, bufferAddress, payload)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + ctx[CpuRegister.Rax] = 512; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + private static string[] EnumerateDirectoryEntries(string hostPath) + { + return Directory.EnumerateFileSystemEntries(hostPath) + .Select(Path.GetFileName) + .Where(static name => !string.IsNullOrEmpty(name)) + .OrderBy(static name => name, StringComparer.OrdinalIgnoreCase) + .ToArray()!; + } + + private static uint ComputeDirectoryEntryHash(ReadOnlySpan utf8Name) + { + const uint offsetBasis = 2166136261; + const uint prime = 16777619; + + var hash = offsetBasis; + for (var i = 0; i < utf8Name.Length; i++) + { + hash ^= utf8Name[i]; + hash *= prime; + } + + return hash; + } + + private static void LogOpenTrace(string message) + { + if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_OPEN"), "1", StringComparison.Ordinal)) + { + return; + } + + Console.Error.WriteLine($"[LOADER][TRACE] {message}"); + } + private static ulong AlignUp(ulong value, ulong alignment) { if (alignment <= 1) diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs index 94f1a64..d5c109e 100644 --- a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs @@ -12,7 +12,9 @@ public static class KernelPthreadCompatExports private const int MutexTypeDefault = 1; private const int MutexTypeErrorCheck = 1; private const int MutexTypeRecursive = 2; - private const int MutexTypeNormal = 4; + private const int MutexTypeNormal = 3; + private const int MutexTypeAdaptiveNp = 4; + private const ulong StaticAdaptiveMutexInitializer = 1; private const ulong SyntheticMutexHandleBase = 0x00006000_0000_0000; private const ulong SyntheticMutexAttrHandleBase = 0x00006001_0000_0000; private const ulong SyntheticCondHandleBase = 0x00006002_0000_0000; @@ -87,18 +89,14 @@ public static class KernelPthreadCompatExports ExportName = "scePthreadGetthreadid", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] - public static int PthreadGetthreadid(CpuContext ctx) - { - var currentThreadId = KernelPthreadState.GetCurrentThreadUniqueId(); - var outAddress = ctx[CpuRegister.Rdi]; - if (outAddress != 0 && !ctx.TryWriteUInt64(outAddress, currentThreadId)) - { - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; - } + public static int PthreadGetthreadid(CpuContext ctx) => PthreadGetthreadidCore(ctx); - ctx[CpuRegister.Rax] = currentThreadId; - return (int)OrbisGen2Result.ORBIS_GEN2_OK; - } + [SysAbiExport( + Nid = "3eqs37G74-s", + ExportName = "pthread_getthreadid_np", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PosixPthreadGetthreadidNp(CpuContext ctx) => PthreadGetthreadidCore(ctx); [SysAbiExport( Nid = "cmo1RIYva9o", @@ -170,6 +168,12 @@ public static class KernelPthreadCompatExports LibraryName = "libKernel")] public static int PosixPthreadMutexUnlock(CpuContext ctx) => PthreadMutexUnlockCore(ctx, ctx[CpuRegister.Rdi], requireOwner: true); + private static int PthreadGetthreadidCore(CpuContext ctx) + { + ctx[CpuRegister.Rax] = KernelPthreadState.GetCurrentThreadUniqueId(); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + [SysAbiExport( Nid = "F8bUHwAG284", ExportName = "scePthreadMutexattrInit", @@ -630,6 +634,11 @@ public static class KernelPthreadCompatExports return false; } + if (pointedHandle == StaticAdaptiveMutexInitializer) + { + return CreateImplicitMutexState(ctx, mutexAddress, MutexTypeAdaptiveNp, out resolvedAddress, out state); + } + if (pointedHandle != 0) { lock (_stateGate) @@ -652,18 +661,7 @@ public static class KernelPthreadCompatExports return false; } - var createdState = new PthreadMutexState(); - var syntheticHandle = AllocateSyntheticHandle(SyntheticMutexHandleBase, ref _nextSyntheticMutexHandleId); - lock (_stateGate) - { - _mutexStates[mutexAddress] = createdState; - _mutexStates[syntheticHandle] = createdState; - } - - _ = ctx.TryWriteUInt64(mutexAddress, syntheticHandle); - resolvedAddress = syntheticHandle; - state = createdState; - return true; + return CreateImplicitMutexState(ctx, mutexAddress, MutexTypeDefault, out resolvedAddress, out state); } private static ulong ResolveMutexAttrHandle(CpuContext ctx, ulong attrAddress) @@ -953,11 +951,43 @@ public static class KernelPthreadCompatExports 1 => MutexTypeErrorCheck, 2 => MutexTypeRecursive, 3 => MutexTypeNormal, - 4 => MutexTypeNormal, + 4 => MutexTypeAdaptiveNp, _ => MutexTypeDefault, }; } + private static bool CreateImplicitMutexState(CpuContext ctx, ulong mutexAddress, int type, out ulong resolvedAddress, [NotNullWhen(true)] out PthreadMutexState? state) + { + var createdState = new PthreadMutexState + { + Type = type, + }; + + var syntheticHandle = AllocateSyntheticHandle(SyntheticMutexHandleBase, ref _nextSyntheticMutexHandleId); + lock (_stateGate) + { + if (_mutexStates.TryGetValue(mutexAddress, out state)) + { + resolvedAddress = mutexAddress; + return true; + } + + if (_mutexStates.TryGetValue(syntheticHandle, out state)) + { + resolvedAddress = syntheticHandle; + return true; + } + + _mutexStates[mutexAddress] = createdState; + _mutexStates[syntheticHandle] = createdState; + } + + _ = ctx.TryWriteUInt64(mutexAddress, syntheticHandle); + resolvedAddress = syntheticHandle; + state = createdState; + return true; + } + private static void TracePthreadSelf(CpuContext ctx, ulong currentThreadHandle) { if (!ShouldTracePthread()) diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs index a7919d9..f629309 100644 --- a/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs @@ -24,7 +24,7 @@ public static class KernelPthreadExtendedCompatExports private static readonly object _stateGate = new(); private static readonly Dictionary _threadStates = new(); private static readonly Dictionary _attrStates = new(); - private static readonly Dictionary _rwlockStates = new(); + private static readonly Dictionary _rwlockStates = new(); private static readonly Dictionary _tlsKeys = new(); private static int _nextTlsKey = 1; private static long _nextSyntheticRwlockHandleId = 1; @@ -41,6 +41,47 @@ public static class KernelPthreadExtendedCompatExports public PthreadAttrState Attributes { get; set; } = PthreadAttrState.Default; } + private sealed class PthreadRwlockState + { + public object SyncRoot { get; } = new(); + public Dictionary ReaderCounts { get; } = new(); + public int ReaderTotalCount { get; set; } + public ulong WriterThreadId { get; set; } + public int WaitingWriters { get; set; } + + public int GetReaderCount(ulong threadId) + { + return ReaderCounts.TryGetValue(threadId, out var count) ? count : 0; + } + + public void AddReader(ulong threadId) + { + ReaderCounts.TryGetValue(threadId, out var currentCount); + ReaderCounts[threadId] = currentCount + 1; + ReaderTotalCount++; + } + + public bool RemoveReader(ulong threadId) + { + if (!ReaderCounts.TryGetValue(threadId, out var currentCount) || currentCount <= 0) + { + return false; + } + + if (currentCount == 1) + { + ReaderCounts.Remove(threadId); + } + else + { + ReaderCounts[threadId] = currentCount - 1; + } + + ReaderTotalCount = Math.Max(0, ReaderTotalCount - 1); + return true; + } + } + private readonly record struct TlsKeyState(ulong Destructor); private readonly record struct PthreadAttrState( @@ -600,10 +641,9 @@ public static class KernelPthreadExtendedCompatExports var resolvedAddress = ResolveRwlockHandle(ctx, rwlockAddress); if (_rwlockStates.Remove(resolvedAddress, out var existing)) { - existing.Dispose(); } - var rwlock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); + var rwlock = new PthreadRwlockState(); _rwlockStates[rwlockAddress] = rwlock; _rwlockStates[syntheticHandle] = rwlock; } @@ -634,14 +674,10 @@ public static class KernelPthreadExtendedCompatExports } var resolvedAddress = ResolveRwlockHandle(ctx, rwlockAddress); - ReaderWriterLockSlim? state; + PthreadRwlockState? state; lock (_stateGate) { - _rwlockStates.Remove(resolvedAddress, out state); - if (resolvedAddress != rwlockAddress) - { - _rwlockStates.Remove(rwlockAddress); - } + _rwlockStates.TryGetValue(resolvedAddress, out state); } if (state is null) @@ -649,8 +685,24 @@ public static class KernelPthreadExtendedCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; } + lock (state.SyncRoot) + { + if (state.WriterThreadId != 0 || state.ReaderTotalCount != 0 || state.WaitingWriters != 0) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY; + } + } + + lock (_stateGate) + { + _rwlockStates.Remove(resolvedAddress); + if (resolvedAddress != rwlockAddress) + { + _rwlockStates.Remove(rwlockAddress); + } + } + _ = ctx.TryWriteUInt64(rwlockAddress, 0); - state.Dispose(); ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -703,29 +755,38 @@ public static class KernelPthreadExtendedCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; } - if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: true, out _, out var rwlock)) + if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: false, out _, out var rwlock)) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; } + var currentThreadId = KernelPthreadState.GetCurrentThreadHandle(); + try { - if (rwlock.IsWriteLockHeld) + lock (rwlock.SyncRoot) { - rwlock.ExitWriteLock(); - } - else if (rwlock.IsReadLockHeld) - { - rwlock.ExitReadLock(); - } - else - { - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + if (rwlock.WriterThreadId == currentThreadId) + { + rwlock.WriterThreadId = 0; + Monitor.PulseAll(rwlock.SyncRoot); + } + else if (rwlock.RemoveReader(currentThreadId)) + { + if (rwlock.ReaderTotalCount == 0 || rwlock.WaitingWriters > 0) + { + Monitor.PulseAll(rwlock.SyncRoot); + } + } + else + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED; + } } } catch (SynchronizationLockException) { - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED; } return (int)OrbisGen2Result.ORBIS_GEN2_OK; @@ -881,21 +942,47 @@ public static class KernelPthreadExtendedCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; } - try + var currentThreadId = KernelPthreadState.GetCurrentThreadHandle(); + lock (rwlock.SyncRoot) { if (write) { - rwlock.EnterWriteLock(); + if (rwlock.WriterThreadId == currentThreadId || rwlock.GetReaderCount(currentThreadId) > 0) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK; + } + + rwlock.WaitingWriters++; + try + { + while (rwlock.WriterThreadId != 0 || rwlock.ReaderTotalCount != 0) + { + Monitor.Wait(rwlock.SyncRoot); + } + } + finally + { + rwlock.WaitingWriters--; + } + + rwlock.WriterThreadId = currentThreadId; } else { - rwlock.EnterReadLock(); + if (rwlock.WriterThreadId == currentThreadId) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK; + } + + while (rwlock.WriterThreadId != 0 || + (rwlock.WaitingWriters > 0 && rwlock.GetReaderCount(currentThreadId) == 0)) + { + Monitor.Wait(rwlock.SyncRoot); + } + + rwlock.AddReader(currentThreadId); } } - catch (LockRecursionException) - { - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_ALREADY_EXISTS; - } return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -929,7 +1016,7 @@ public static class KernelPthreadExtendedCompatExports return rwlockAddress; } - private static bool TryResolveRwlockState(CpuContext ctx, ulong rwlockAddress, bool createIfZero, out ulong resolvedAddress, [NotNullWhen(true)] out ReaderWriterLockSlim? rwlock) + private static bool TryResolveRwlockState(CpuContext ctx, ulong rwlockAddress, bool createIfZero, out ulong resolvedAddress, [NotNullWhen(true)] out PthreadRwlockState? rwlock) { resolvedAddress = 0; rwlock = null; @@ -974,7 +1061,7 @@ public static class KernelPthreadExtendedCompatExports return false; } - var createdRwlock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); + var createdRwlock = new PthreadRwlockState(); var syntheticHandle = AllocateSyntheticHandle(SyntheticRwlockHandleBase, ref _nextSyntheticRwlockHandleId); lock (_stateGate) { diff --git a/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs index f49f5c1..57e3652 100644 --- a/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs @@ -31,6 +31,7 @@ public static class KernelRuntimeCompatExports private const ulong DefaultKernelTscFrequency = 10_000_000UL; private const ulong PrtAreaStartAddress = 0x0000001000000000UL; private const ulong PrtAreaSize = 0x000000EC00000000UL; + private const int AioInitParamSize = 0x3C; private const uint MemCommit = 0x1000; private const uint MemReserve = 0x2000; private const uint PageExecuteReadWrite = 0x40; @@ -780,6 +781,48 @@ public static class KernelRuntimeCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + [SysAbiExport( + Nid = "nu4a0-arQis", + ExportName = "sceKernelAioInitializeParam", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelAioInitializeParam(CpuContext ctx) + { + var paramAddress = ctx[CpuRegister.Rdi]; + if (paramAddress == 0) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + Span zero = stackalloc byte[AioInitParamSize]; + zero.Clear(); + if (!ctx.Memory.TryWrite(paramAddress, zero)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "vYU8P9Td2Zo", + ExportName = "sceKernelAioInitializeImpl", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelAioInitializeImpl(CpuContext ctx) + { + var paramAddress = ctx[CpuRegister.Rdi]; + var size = unchecked((int)ctx[CpuRegister.Rsi]); + if (paramAddress == 0 || size < AioInitParamSize) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + [SysAbiExport( Nid = "eR2bZFAAU0Q", ExportName = "sceSysmoduleUnloadModule", diff --git a/src/SharpEmu.Libs/PlayGo/PlayGoExports.cs b/src/SharpEmu.Libs/PlayGo/PlayGoExports.cs new file mode 100644 index 0000000..8de630e --- /dev/null +++ b/src/SharpEmu.Libs/PlayGo/PlayGoExports.cs @@ -0,0 +1,176 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; +using System.Buffers.Binary; + +namespace SharpEmu.Libs.PlayGo; + +public static class PlayGoExports +{ + private const int OrbisPlayGoErrorInvalidArgument = unchecked((int)0x80B20004); + private const int OrbisPlayGoErrorNotInitialized = unchecked((int)0x80B20005); + private const int OrbisPlayGoErrorAlreadyInitialized = unchecked((int)0x80B20006); + private const int OrbisPlayGoErrorBadHandle = unchecked((int)0x80B20009); + private const int OrbisPlayGoErrorBadPointer = unchecked((int)0x80B2000A); + private const int OrbisPlayGoErrorBadSize = unchecked((int)0x80B2000B); + private const int OrbisPlayGoErrorNotSupportPlayGo = unchecked((int)0x80B2000E); + private const ulong PlayGoInitBufAddrOffset = 0; + private const ulong PlayGoInitBufSizeOffset = 8; + private const uint PlayGoMinimumInitBufferSize = 0x200000; + private const uint PlayGoHandle = 1; + + private static readonly object _stateGate = new(); + private static bool _initialized; + private static bool _hasPlayGoData; + + [SysAbiExport( + Nid = "ts6GlZOKRrE", + ExportName = "scePlayGoInitialize", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePlayGo")] + public static int PlayGoInitialize(CpuContext ctx) + { + var initParamsAddress = ctx[CpuRegister.Rdi]; + if (initParamsAddress == 0) + { + return OrbisPlayGoErrorBadPointer; + } + + if (!ctx.TryReadUInt64(initParamsAddress + PlayGoInitBufAddrOffset, out var bufferAddress)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + Span bufferSizeBytes = stackalloc byte[sizeof(uint)]; + if (!ctx.Memory.TryRead(initParamsAddress + PlayGoInitBufSizeOffset, bufferSizeBytes)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + var bufferSize = BinaryPrimitives.ReadUInt32LittleEndian(bufferSizeBytes); + + if (bufferAddress == 0) + { + return OrbisPlayGoErrorBadPointer; + } + + if (bufferSize < PlayGoMinimumInitBufferSize) + { + return OrbisPlayGoErrorBadSize; + } + + lock (_stateGate) + { + if (_initialized) + { + return OrbisPlayGoErrorAlreadyInitialized; + } + + _hasPlayGoData = HasPlayGoChunkData(); + _initialized = true; + } + + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "M1Gma1ocrGE", + ExportName = "scePlayGoOpen", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePlayGo")] + public static int PlayGoOpen(CpuContext ctx) + { + var outHandleAddress = ctx[CpuRegister.Rdi]; + var paramAddress = ctx[CpuRegister.Rsi]; + if (outHandleAddress == 0) + { + return OrbisPlayGoErrorBadPointer; + } + + if (paramAddress != 0) + { + return OrbisPlayGoErrorInvalidArgument; + } + + lock (_stateGate) + { + if (!_initialized) + { + return OrbisPlayGoErrorNotInitialized; + } + + if (!_hasPlayGoData) + { + return OrbisPlayGoErrorNotSupportPlayGo; + } + } + + Span handleBytes = stackalloc byte[sizeof(uint)]; + BinaryPrimitives.WriteUInt32LittleEndian(handleBytes, PlayGoHandle); + if (!ctx.Memory.TryWrite(outHandleAddress, handleBytes)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "MPe0EeBGM-E", + ExportName = "scePlayGoTerminate", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePlayGo")] + public static int PlayGoTerminate(CpuContext ctx) + { + _ = ctx; + lock (_stateGate) + { + if (!_initialized) + { + return OrbisPlayGoErrorNotInitialized; + } + + _initialized = false; + _hasPlayGoData = false; + } + + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "Uco1I0dlDi8", + ExportName = "scePlayGoClose", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePlayGo")] + public static int PlayGoClose(CpuContext ctx) + { + var handle = unchecked((uint)ctx[CpuRegister.Rdi]); + lock (_stateGate) + { + if (!_initialized) + { + return OrbisPlayGoErrorNotInitialized; + } + + if (handle != PlayGoHandle) + { + return OrbisPlayGoErrorBadHandle; + } + } + + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + private static bool HasPlayGoChunkData() + { + var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR"); + if (string.IsNullOrWhiteSpace(app0Root)) + { + return false; + } + + var hostPath = Path.Combine(app0Root, "sce_sys", "playgo-chunk.dat"); + return File.Exists(hostPath); + } +} diff --git a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs index adf3ad1..a834899 100644 --- a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs +++ b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs @@ -2,9 +2,408 @@ // SPDX-License-Identifier: GPL-2.0-or-later using SharpEmu.HLE; +using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; +using System.Threading; namespace SharpEmu.Libs.VideoOut; public static class VideoOutExports { + private const int OrbisVideoOutErrorInvalidValue = unchecked((int)0x80290001); + private const int OrbisVideoOutErrorInvalidAddress = unchecked((int)0x80290002); + private const int OrbisVideoOutErrorResourceBusy = unchecked((int)0x80290009); + private const int OrbisVideoOutErrorInvalidIndex = unchecked((int)0x8029000A); + private const int OrbisVideoOutErrorInvalidHandle = unchecked((int)0x8029000B); + private const int OrbisVideoOutErrorInvalidOption = unchecked((int)0x8029001A); + private const int SceVideoOutBusTypeMain = 0; + private const int SceVideoOutBufferAttributeOptionNone = 0; + private const int MaxOpenPorts = 4; + private const int MaxDisplayBuffers = 16; + private const int VideoOutBufferAttributeSize = 0x24; + private const int VideoOutBufferAttribute2Size = 0x50; + private const int VideoOutBuffersEntrySize = 0x20; + + private static readonly object _stateGate = new(); + private static readonly Dictionary _ports = new(); + private static int _nextHandle = 1; + + private sealed class VideoOutPortState + { + public required int Handle { get; init; } + public int FlipRate { get; set; } + public ulong VblankCount { get; set; } + public int NextSetId { get; set; } = 1; + public HashSet RegisteredSetIds { get; } = new(); + } + + [SysAbiExport( + Nid = "Up36PTk687E", + ExportName = "sceVideoOutOpen", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceVideoOut")] + public static int VideoOutOpen(CpuContext ctx) + { + var userId = unchecked((int)ctx[CpuRegister.Rdi]); + var busType = unchecked((int)ctx[CpuRegister.Rsi]); + var index = unchecked((int)ctx[CpuRegister.Rdx]); + _ = ctx[CpuRegister.Rcx]; + + if (busType != SceVideoOutBusTypeMain || index != 0) + { + return OrbisVideoOutErrorInvalidValue; + } + + if (userId != 0 && userId != 255) + { + return OrbisVideoOutErrorInvalidValue; + } + + lock (_stateGate) + { + if (_ports.Count >= MaxOpenPorts) + { + return OrbisVideoOutErrorResourceBusy; + } + + var handle = _nextHandle++; + _ports[handle] = new VideoOutPortState + { + Handle = handle, + }; + return handle; + } + } + + [SysAbiExport( + Nid = "uquVH4-Du78", + ExportName = "sceVideoOutClose", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceVideoOut")] + public static int VideoOutClose(CpuContext ctx) + { + var handle = unchecked((int)ctx[CpuRegister.Rdi]); + lock (_stateGate) + { + _ports.Remove(handle); + } + + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "CBiu4mCE1DA", + ExportName = "sceVideoOutSetFlipRate", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceVideoOut")] + public static int VideoOutSetFlipRate(CpuContext ctx) + { + var handle = unchecked((int)ctx[CpuRegister.Rdi]); + var rate = unchecked((int)ctx[CpuRegister.Rsi]); + if (rate is < 0 or > 2) + { + return OrbisVideoOutErrorInvalidValue; + } + + if (!TryGetPort(handle, out var port)) + { + return OrbisVideoOutErrorInvalidHandle; + } + + port.FlipRate = rate; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "j6RaAUlaLv0", + ExportName = "sceVideoOutWaitVblank", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceVideoOut")] + public static int VideoOutWaitVblank(CpuContext ctx) + { + var handle = unchecked((int)ctx[CpuRegister.Rdi]); + if (!TryGetPort(handle, out var port)) + { + return OrbisVideoOutErrorInvalidHandle; + } + + Thread.Sleep(1); + lock (_stateGate) + { + port.VblankCount++; + } + + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "MTxxrOCeSig", + ExportName = "sceVideoOutSetWindowModeMargins", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceVideoOut")] + public static int VideoOutSetWindowModeMargins(CpuContext ctx) + { + var handle = unchecked((int)ctx[CpuRegister.Rdi]); + _ = unchecked((int)ctx[CpuRegister.Rsi]); + _ = unchecked((int)ctx[CpuRegister.Rdx]); + + return TryGetPort(handle, out _) + ? (int)OrbisGen2Result.ORBIS_GEN2_OK + : OrbisVideoOutErrorInvalidHandle; + } + + [SysAbiExport( + Nid = "N5KDtkIjjJ4", + ExportName = "sceVideoOutUnregisterBuffers", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceVideoOut")] + public static int VideoOutUnregisterBuffers(CpuContext ctx) + { + var handle = unchecked((int)ctx[CpuRegister.Rdi]); + var attributeIndex = unchecked((int)ctx[CpuRegister.Rsi]); + if (!TryGetPort(handle, out var port)) + { + return OrbisVideoOutErrorInvalidHandle; + } + + if (attributeIndex < 0) + { + return OrbisVideoOutErrorInvalidValue; + } + + lock (_stateGate) + { + return port.RegisteredSetIds.Remove(attributeIndex) + ? (int)OrbisGen2Result.ORBIS_GEN2_OK + : OrbisVideoOutErrorInvalidValue; + } + } + + [SysAbiExport( + Nid = "i6-sR91Wt-4", + ExportName = "sceVideoOutSetBufferAttribute", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceVideoOut")] + public static int VideoOutSetBufferAttribute(CpuContext ctx) + { + var attributeAddress = ctx[CpuRegister.Rdi]; + var pixelFormat = unchecked((uint)ctx[CpuRegister.Rsi]); + var tilingMode = unchecked((uint)ctx[CpuRegister.Rdx]); + var aspectRatio = unchecked((uint)ctx[CpuRegister.Rcx]); + var width = unchecked((uint)ctx[CpuRegister.R8]); + var height = unchecked((uint)ctx[CpuRegister.R9]); + if (!TryReadStackUInt32(ctx, 0, out var pitchInPixel)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + if (attributeAddress == 0) + { + return OrbisVideoOutErrorInvalidAddress; + } + + Span attribute = stackalloc byte[VideoOutBufferAttributeSize]; + attribute.Clear(); + BinaryPrimitives.WriteUInt32LittleEndian(attribute[0x00..0x04], pixelFormat); + BinaryPrimitives.WriteUInt32LittleEndian(attribute[0x04..0x08], tilingMode); + BinaryPrimitives.WriteUInt32LittleEndian(attribute[0x08..0x0C], aspectRatio); + BinaryPrimitives.WriteUInt32LittleEndian(attribute[0x0C..0x10], width); + BinaryPrimitives.WriteUInt32LittleEndian(attribute[0x10..0x14], height); + BinaryPrimitives.WriteUInt32LittleEndian(attribute[0x14..0x18], pitchInPixel); + BinaryPrimitives.WriteUInt32LittleEndian(attribute[0x18..0x1C], SceVideoOutBufferAttributeOptionNone); + if (!ctx.Memory.TryWrite(attributeAddress, attribute)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "PjS5uASwcV8", + ExportName = "sceVideoOutSetBufferAttribute2", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceVideoOut")] + public static int VideoOutSetBufferAttribute2(CpuContext ctx) + { + var attributeAddress = ctx[CpuRegister.Rdi]; + var pixelFormat = ctx[CpuRegister.Rsi]; + var tilingMode = unchecked((uint)ctx[CpuRegister.Rdx]); + var width = unchecked((uint)ctx[CpuRegister.Rcx]); + var height = unchecked((uint)ctx[CpuRegister.R8]); + var option = ctx[CpuRegister.R9]; + if (!TryReadStackUInt32(ctx, 0, out var dccControl) || + !ctx.TryReadUInt64(ctx[CpuRegister.Rsp] + 0x10, out var dccClearColor)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + if (attributeAddress == 0) + { + return OrbisVideoOutErrorInvalidAddress; + } + + Span attribute = stackalloc byte[VideoOutBufferAttribute2Size]; + attribute.Clear(); + BinaryPrimitives.WriteUInt32LittleEndian(attribute[0x04..0x08], tilingMode); + BinaryPrimitives.WriteUInt32LittleEndian(attribute[0x0C..0x10], width); + BinaryPrimitives.WriteUInt32LittleEndian(attribute[0x10..0x14], height); + BinaryPrimitives.WriteUInt64LittleEndian(attribute[0x18..0x20], option); + BinaryPrimitives.WriteUInt64LittleEndian(attribute[0x20..0x28], pixelFormat); + BinaryPrimitives.WriteUInt64LittleEndian(attribute[0x28..0x30], dccClearColor); + BinaryPrimitives.WriteUInt32LittleEndian(attribute[0x30..0x34], dccControl); + if (!ctx.Memory.TryWrite(attributeAddress, attribute)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "w3BY+tAEiQY", + ExportName = "sceVideoOutRegisterBuffers", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceVideoOut")] + public static int VideoOutRegisterBuffers(CpuContext ctx) + { + var handle = unchecked((int)ctx[CpuRegister.Rdi]); + var startIndex = unchecked((int)ctx[CpuRegister.Rsi]); + var addressesAddress = ctx[CpuRegister.Rdx]; + var bufferNum = unchecked((int)ctx[CpuRegister.Rcx]); + var attributeAddress = ctx[CpuRegister.R8]; + if (!TryGetPort(handle, out var port)) + { + return OrbisVideoOutErrorInvalidHandle; + } + + if (addressesAddress == 0) + { + return OrbisVideoOutErrorInvalidAddress; + } + + if (attributeAddress == 0) + { + return OrbisVideoOutErrorInvalidOption; + } + + if (!IsValidBufferRange(startIndex, bufferNum)) + { + return OrbisVideoOutErrorInvalidValue; + } + + for (var i = 0; i < bufferNum; i++) + { + if (!ctx.TryReadUInt64(addressesAddress + ((ulong)i * 8), out _)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + } + + var setId = AllocateBufferSet(port); + return setId; + } + + [SysAbiExport( + Nid = "rKBUtgRrtbk", + ExportName = "sceVideoOutRegisterBuffers2", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceVideoOut")] + public static int VideoOutRegisterBuffers2(CpuContext ctx) + { + var handle = unchecked((int)ctx[CpuRegister.Rdi]); + var setIndex = unchecked((int)ctx[CpuRegister.Rsi]); + var bufferIndexStart = unchecked((int)ctx[CpuRegister.Rdx]); + var buffersAddress = ctx[CpuRegister.Rcx]; + var bufferNum = unchecked((int)ctx[CpuRegister.R8]); + var attributeAddress = ctx[CpuRegister.R9]; + if (!ctx.TryReadUInt64(ctx[CpuRegister.Rsp] + 0x08, out var categoryRaw) || + !ctx.TryReadUInt64(ctx[CpuRegister.Rsp] + 0x10, out var option)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + if (!TryGetPort(handle, out var port)) + { + return OrbisVideoOutErrorInvalidHandle; + } + + if (buffersAddress == 0) + { + return OrbisVideoOutErrorInvalidAddress; + } + + if (attributeAddress == 0) + { + return OrbisVideoOutErrorInvalidOption; + } + + if (!IsValidBufferRange(bufferIndexStart, bufferNum)) + { + return OrbisVideoOutErrorInvalidValue; + } + + if (categoryRaw != 0 || option != 0) + { + return OrbisVideoOutErrorInvalidValue; + } + + for (var i = 0; i < bufferNum; i++) + { + var entryAddress = buffersAddress + ((ulong)i * VideoOutBuffersEntrySize); + if (!ctx.TryReadUInt64(entryAddress + 0x00, out _) || + !ctx.TryReadUInt64(entryAddress + 0x08, out _)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + } + + lock (_stateGate) + { + port.RegisteredSetIds.Add(setIndex); + } + + return setIndex; + } + + private static int AllocateBufferSet(VideoOutPortState port) + { + lock (_stateGate) + { + var setId = port.NextSetId++; + port.RegisteredSetIds.Add(setId); + return setId; + } + } + + private static bool IsValidBufferRange(int startIndex, int bufferNum) + { + return startIndex >= 0 && + startIndex < MaxDisplayBuffers && + bufferNum >= 1 && + bufferNum <= MaxDisplayBuffers && + startIndex + bufferNum <= MaxDisplayBuffers; + } + + private static bool TryGetPort(int handle, [NotNullWhen(true)] out VideoOutPortState? port) + { + lock (_stateGate) + { + return _ports.TryGetValue(handle, out port); + } + } + + private static bool TryReadStackUInt32(CpuContext ctx, int stackIndex, out uint value) + { + var address = ctx[CpuRegister.Rsp] + 0x08 + ((ulong)stackIndex * 0x08); + Span buffer = stackalloc byte[sizeof(uint)]; + if (!ctx.Memory.TryRead(address, buffer)) + { + value = 0; + return false; + } + + value = BinaryPrimitives.ReadUInt32LittleEndian(buffer); + return true; + } }