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,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)