PlayGo, VideoOut minimum HLE implements and fix some direct runner

This commit is contained in:
ParantezTech
2026-03-28 18:02:37 +03:00
parent 71ba5cf1db
commit 812879aa81
10 changed files with 1506 additions and 182 deletions
+7 -74
View File
@@ -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;
}
@@ -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<int, FileStream> _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<byte> 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}: <unreadable>");
}
}
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<byte>();
@@ -2602,7 +2897,7 @@ public static class KernelMemoryCompatExports
var buffer = new List<ushort>(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<ushort> 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<ushort> units)
{
if (units.IsEmpty)
{
return string.Empty;
}
return new string(MemoryMarshal.Cast<ushort, char>(units));
}
private static bool TryConvertWideScalarToString(ushort scalar, out string text)
{
text = ((char)scalar).ToString();
return true;
}
private static byte[] EncodeWideUnitsWithTerminator(ReadOnlySpan<ushort> 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<byte> 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<byte> 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<byte> 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)
@@ -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())
@@ -24,7 +24,7 @@ public static class KernelPthreadExtendedCompatExports
private static readonly object _stateGate = new();
private static readonly Dictionary<ulong, ThreadState> _threadStates = new();
private static readonly Dictionary<ulong, PthreadAttrState> _attrStates = new();
private static readonly Dictionary<ulong, ReaderWriterLockSlim> _rwlockStates = new();
private static readonly Dictionary<ulong, PthreadRwlockState> _rwlockStates = new();
private static readonly Dictionary<int, TlsKeyState> _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<ulong, int> 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)
{
@@ -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<byte> 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",