initial commit

This commit is contained in:
ParantezTech
2026-03-11 15:48:28 +03:00
commit 4d73f469bc
92 changed files with 166746 additions and 0 deletions
+673
View File
@@ -0,0 +1,673 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Runtime;
using SharpEmu.Core.Cpu;
using SharpEmu.HLE;
using SharpEmu.Logging;
using System.Runtime.InteropServices;
using System.Text;
namespace SharpEmu.CLI;
internal static partial class Program
{
private static readonly SharpEmuLogger Log = SharpEmuLog.For("SharpEmu.CLI");
private const int DefaultImportTraceLimit = 32;
private const string MitigatedChildFlag = "--sharpemu-mitigated-child";
private const uint EXTENDED_STARTUPINFO_PRESENT = 0x00080000;
private const uint INFINITE = 0xFFFFFFFF;
private const int PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY = 0x00020007;
private const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000;
private const int JobObjectExtendedLimitInformation = 9;
private const ulong PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF = 0x00000002UL << 28;
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF = 0x00000002UL << 32;
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
private static int Main(string[] args)
{
Console.Error.WriteLine($"[DEBUG] SharpEmu starting with {args.Length} args");
args = NormalizeInternalArguments(args, out var isMitigatedChild);
if (!isMitigatedChild && TryRunMitigatedChild(args, out var childExitCode))
{
return childExitCode;
}
if (!TryParseArguments(args, out var ebootPath, out var runtimeOptions, out var logLevel))
{
PrintUsage();
return 1;
}
SharpEmuLog.MinimumLevel = logLevel;
ebootPath = Path.GetFullPath(ebootPath);
Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}");
if (!File.Exists(ebootPath))
{
Log.Error($"EBOOT file was not found: {ebootPath}");
return 2;
}
Console.Error.WriteLine("[DEBUG] Creating runtime...");
using var runtime = SharpEmuRuntime.CreateDefault(runtimeOptions);
OrbisGen2Result result;
try
{
Console.Error.WriteLine($"[DEBUG] Running: {ebootPath}");
result = runtime.Run(ebootPath);
Console.Error.WriteLine($"[DEBUG] Result: {result}");
}
catch (Exception ex)
{
Console.Error.WriteLine($"[DEBUG] Exception: {ex}");
Log.Error("SharpEmu failed to run.", ex);
return 3;
}
Log.Info($"SharpEmu execution completed. Result={result} (0x{(int)result:X8})");
if (!string.IsNullOrWhiteSpace(runtime.LastSessionSummary))
{
Log.Info(runtime.LastSessionSummary);
}
if (!string.IsNullOrWhiteSpace(runtime.LastBasicBlockTrace))
{
Log.Info("BB trace:");
Log.Info(runtime.LastBasicBlockTrace);
}
if (!string.IsNullOrWhiteSpace(runtime.LastMilestoneLog))
{
Log.Info(runtime.LastMilestoneLog);
}
if (result != OrbisGen2Result.ORBIS_GEN2_OK && !string.IsNullOrWhiteSpace(runtime.LastExecutionDiagnostics))
{
Log.Warn(runtime.LastExecutionDiagnostics);
}
if (runtimeOptions.ImportTraceLimit > 0 && !string.IsNullOrWhiteSpace(runtime.LastExecutionTrace))
{
Log.Info("Import trace:");
Log.Info(runtime.LastExecutionTrace);
}
return result == OrbisGen2Result.ORBIS_GEN2_OK ? 0 : 4;
}
private static string[] NormalizeInternalArguments(string[] args, out bool isMitigatedChild)
{
isMitigatedChild = false;
if (args.Length == 0)
{
return args;
}
var list = new List<string>(args.Length);
foreach (var arg in args)
{
if (string.Equals(arg, MitigatedChildFlag, StringComparison.Ordinal))
{
isMitigatedChild = true;
continue;
}
list.Add(arg);
}
return list.ToArray();
}
private static bool TryRunMitigatedChild(string[] args, out int childExitCode)
{
childExitCode = 0;
if (!OperatingSystem.IsWindows())
{
return false;
}
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_MITIGATION_RELAUNCH"), "1", StringComparison.Ordinal))
{
return false;
}
var processPath = Environment.ProcessPath;
if (string.IsNullOrWhiteSpace(processPath))
{
return false;
}
var childArgs = new string[args.Length + 1];
childArgs[0] = MitigatedChildFlag;
for (var i = 0; i < args.Length; i++)
{
childArgs[i + 1] = args[i];
}
var commandLine = BuildCommandLine(processPath, childArgs);
var startupInfoEx = new STARTUPINFOEX();
startupInfoEx.StartupInfo.cb = Marshal.SizeOf<STARTUPINFOEX>();
nint attributeList = 0;
nint mitigationPolicies = 0;
try
{
nuint attributeListSize = 0;
_ = InitializeProcThreadAttributeList(0, 1, 0, ref attributeListSize);
attributeList = Marshal.AllocHGlobal((nint)attributeListSize);
if (!InitializeProcThreadAttributeList(attributeList, 1, 0, ref attributeListSize))
{
return false;
}
startupInfoEx.lpAttributeList = attributeList;
var policy1 = PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF;
var policy2 =
PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF |
PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF |
PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF;
mitigationPolicies = Marshal.AllocHGlobal(sizeof(ulong) * 2);
Marshal.WriteInt64(mitigationPolicies, unchecked((long)policy1));
Marshal.WriteInt64(nint.Add(mitigationPolicies, sizeof(long)), unchecked((long)policy2));
if (!UpdateProcThreadAttribute(
attributeList,
0,
(nint)PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY,
mitigationPolicies,
(nuint)(sizeof(ulong) * 2),
0,
0))
{
return false;
}
var cmdLineBuilder = new StringBuilder(commandLine);
nint jobHandle = 0;
if (!CreateProcessW(
processPath,
cmdLineBuilder,
0,
0,
false,
EXTENDED_STARTUPINFO_PRESENT,
0,
Environment.CurrentDirectory,
ref startupInfoEx,
out var processInfo))
{
return false;
}
try
{
jobHandle = CreateJobObjectW(0, null);
if (jobHandle != 0 &&
TryEnableKillOnJobClose(jobHandle) &&
!AssignProcessToJobObject(jobHandle, processInfo.hProcess))
{
CloseHandle(jobHandle);
jobHandle = 0;
}
ConsoleCancelEventHandler? cancelHandler = null;
EventHandler? processExitHandler = null;
cancelHandler = (_, eventArgs) =>
{
_ = TerminateProcess(processInfo.hProcess, 1);
eventArgs.Cancel = true;
};
processExitHandler = (_, _) =>
{
_ = TerminateProcess(processInfo.hProcess, 1);
};
Console.CancelKeyPress += cancelHandler;
AppDomain.CurrentDomain.ProcessExit += processExitHandler;
_ = WaitForSingleObject(processInfo.hProcess, INFINITE);
Console.CancelKeyPress -= cancelHandler;
AppDomain.CurrentDomain.ProcessExit -= processExitHandler;
if (!GetExitCodeProcess(processInfo.hProcess, out var exitCode))
{
return false;
}
childExitCode = unchecked((int)exitCode);
Console.Error.WriteLine("[DEBUG] Running in mitigated child process (CET/CFG disabled).");
return true;
}
finally
{
if (jobHandle != 0)
{
CloseHandle(jobHandle);
}
CloseHandle(processInfo.hThread);
CloseHandle(processInfo.hProcess);
}
}
finally
{
if (attributeList != 0)
{
DeleteProcThreadAttributeList(attributeList);
Marshal.FreeHGlobal(attributeList);
}
if (mitigationPolicies != 0)
{
Marshal.FreeHGlobal(mitigationPolicies);
}
}
}
private static string BuildCommandLine(string processPath, IReadOnlyList<string> args)
{
var builder = new StringBuilder();
builder.Append(QuoteArgument(processPath));
for (var i = 0; i < args.Count; i++)
{
builder.Append(' ');
builder.Append(QuoteArgument(args[i]));
}
return builder.ToString();
}
private static bool TryEnableKillOnJobClose(nint jobHandle)
{
var extendedLimitInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
BasicLimitInformation = new JOBOBJECT_BASIC_LIMIT_INFORMATION
{
LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
},
};
var size = Marshal.SizeOf<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>();
var memory = Marshal.AllocHGlobal(size);
try
{
Marshal.StructureToPtr(extendedLimitInfo, memory, false);
return SetInformationJobObject(
jobHandle,
JobObjectExtendedLimitInformation,
memory,
unchecked((uint)size));
}
finally
{
Marshal.FreeHGlobal(memory);
}
}
private static string QuoteArgument(string argument)
{
if (argument.Length == 0)
{
return "\"\"";
}
var needsQuotes = false;
foreach (var c in argument)
{
if (char.IsWhiteSpace(c) || c == '"')
{
needsQuotes = true;
break;
}
}
if (!needsQuotes)
{
return argument;
}
var builder = new StringBuilder(argument.Length + 2);
builder.Append('"');
var backslashCount = 0;
foreach (var c in argument)
{
if (c == '\\')
{
backslashCount++;
continue;
}
if (c == '"')
{
builder.Append('\\', (backslashCount * 2) + 1);
builder.Append('"');
backslashCount = 0;
continue;
}
if (backslashCount > 0)
{
builder.Append('\\', backslashCount);
backslashCount = 0;
}
builder.Append(c);
}
if (backslashCount > 0)
{
builder.Append('\\', backslashCount * 2);
}
builder.Append('"');
return builder.ToString();
}
private static void PrintUsage()
{
Log.Info("Usage: SharpEmu.CLI [--strict] [--trace-imports[=N]] [--cpu-engine=<native>] [--log-level=<level>] <path-to-eboot.bin>");
Log.Info(@"Example: SharpEmu.CLI --cpu-engine=native --trace-imports=64 --log-level=debug ""E:\Games\...\eboot.bin""");
}
private static bool TryParseArguments(
string[] args,
out string ebootPath,
out SharpEmuRuntimeOptions runtimeOptions,
out LogLevel logLevel)
{
if (args.Length == 0)
{
ebootPath = string.Empty;
runtimeOptions = default;
logLevel = SharpEmuLog.MinimumLevel;
return false;
}
var strictDynlibResolution = false;
var importTraceLimit = 0;
var cpuEngine = CpuExecutionEngine.NativeOnly;
logLevel = SharpEmuLog.MinimumLevel;
var pathTokens = new List<string>(args.Length);
for (var i = 0; i < args.Length; i++)
{
var argument = args[i];
if (string.Equals(argument, "--strict", StringComparison.OrdinalIgnoreCase))
{
strictDynlibResolution = true;
continue;
}
if (string.Equals(argument, "--trace-imports", StringComparison.OrdinalIgnoreCase))
{
importTraceLimit = DefaultImportTraceLimit;
if (i + 1 < args.Length && int.TryParse(args[i + 1], out var explicitLimit))
{
importTraceLimit = Math.Max(0, explicitLimit);
i++;
}
continue;
}
if (string.Equals(argument, "--log-level", StringComparison.OrdinalIgnoreCase))
{
if (i + 1 >= args.Length || !SharpEmuLog.TryParseLevel(args[i + 1], out logLevel))
{
ebootPath = string.Empty;
runtimeOptions = default;
return false;
}
i++;
continue;
}
if (string.Equals(argument, "--cpu-engine", StringComparison.OrdinalIgnoreCase))
{
if (i + 1 >= args.Length || !TryParseCpuEngine(args[i + 1], out cpuEngine))
{
ebootPath = string.Empty;
runtimeOptions = default;
return false;
}
i++;
continue;
}
const string logLevelPrefix = "--log-level=";
if (argument.StartsWith(logLevelPrefix, StringComparison.OrdinalIgnoreCase))
{
var valueText = argument[logLevelPrefix.Length..];
if (!SharpEmuLog.TryParseLevel(valueText, out logLevel))
{
ebootPath = string.Empty;
runtimeOptions = default;
return false;
}
continue;
}
const string cpuEnginePrefix = "--cpu-engine=";
if (argument.StartsWith(cpuEnginePrefix, StringComparison.OrdinalIgnoreCase))
{
var valueText = argument[cpuEnginePrefix.Length..];
if (!TryParseCpuEngine(valueText, out cpuEngine))
{
ebootPath = string.Empty;
runtimeOptions = default;
logLevel = SharpEmuLog.MinimumLevel;
return false;
}
continue;
}
const string tracePrefix = "--trace-imports=";
if (argument.StartsWith(tracePrefix, StringComparison.OrdinalIgnoreCase))
{
var valueText = argument[tracePrefix.Length..];
if (!int.TryParse(valueText, out importTraceLimit))
{
ebootPath = string.Empty;
runtimeOptions = default;
logLevel = SharpEmuLog.MinimumLevel;
return false;
}
importTraceLimit = Math.Max(0, importTraceLimit);
continue;
}
if (argument.StartsWith("--", StringComparison.Ordinal))
{
ebootPath = string.Empty;
runtimeOptions = default;
logLevel = SharpEmuLog.MinimumLevel;
return false;
}
pathTokens.Add(argument);
}
if (pathTokens.Count == 0)
{
ebootPath = string.Empty;
runtimeOptions = default;
logLevel = SharpEmuLog.MinimumLevel;
return false;
}
ebootPath = string.Join(' ', pathTokens);
runtimeOptions = new SharpEmuRuntimeOptions
{
CpuEngine = cpuEngine,
StrictDynlibResolution = strictDynlibResolution,
ImportTraceLimit = importTraceLimit,
};
return true;
}
private static bool TryParseCpuEngine(string valueText, out CpuExecutionEngine engine)
{
if (string.Equals(valueText, "native", StringComparison.OrdinalIgnoreCase) ||
string.Equals(valueText, "native-only", StringComparison.OrdinalIgnoreCase))
{
engine = CpuExecutionEngine.NativeOnly;
return true;
}
engine = CpuExecutionEngine.NativeOnly;
return false;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct STARTUPINFO
{
public int cb;
public nint lpReserved;
public nint lpDesktop;
public nint lpTitle;
public int dwX;
public int dwY;
public int dwXSize;
public int dwYSize;
public int dwXCountChars;
public int dwYCountChars;
public int dwFillAttribute;
public int dwFlags;
public short wShowWindow;
public short cbReserved2;
public nint lpReserved2;
public nint hStdInput;
public nint hStdOutput;
public nint hStdError;
}
[StructLayout(LayoutKind.Sequential)]
private struct STARTUPINFOEX
{
public STARTUPINFO StartupInfo;
public nint lpAttributeList;
}
[StructLayout(LayoutKind.Sequential)]
private struct PROCESS_INFORMATION
{
public nint hProcess;
public nint hThread;
public int dwProcessId;
public int dwThreadId;
}
[StructLayout(LayoutKind.Sequential)]
private struct JOBOBJECT_BASIC_LIMIT_INFORMATION
{
public long PerProcessUserTimeLimit;
public long PerJobUserTimeLimit;
public uint LimitFlags;
public nuint MinimumWorkingSetSize;
public nuint MaximumWorkingSetSize;
public uint ActiveProcessLimit;
public nint Affinity;
public uint PriorityClass;
public uint SchedulingClass;
}
[StructLayout(LayoutKind.Sequential)]
private struct IO_COUNTERS
{
public ulong ReadOperationCount;
public ulong WriteOperationCount;
public ulong OtherOperationCount;
public ulong ReadTransferCount;
public ulong WriteTransferCount;
public ulong OtherTransferCount;
}
[StructLayout(LayoutKind.Sequential)]
private struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
public IO_COUNTERS IoInfo;
public nuint ProcessMemoryLimit;
public nuint JobMemoryLimit;
public nuint PeakProcessMemoryUsed;
public nuint PeakJobMemoryUsed;
}
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool InitializeProcThreadAttributeList(
nint lpAttributeList,
int dwAttributeCount,
int dwFlags,
ref nuint lpSize);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UpdateProcThreadAttribute(
nint lpAttributeList,
uint dwFlags,
nint attribute,
nint lpValue,
nuint cbSize,
nint lpPreviousValue,
nint lpReturnSize);
[DllImport("kernel32.dll")]
private static extern void DeleteProcThreadAttributeList(nint lpAttributeList);
[DllImport("kernel32.dll", EntryPoint = "CreateJobObjectW", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern nint CreateJobObjectW(nint lpJobAttributes, string? lpName);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetInformationJobObject(
nint hJob,
int jobObjectInfoClass,
nint lpJobObjectInfo,
uint cbJobObjectInfoLength);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool AssignProcessToJobObject(nint hJob, nint hProcess);
[DllImport("kernel32.dll", EntryPoint = "CreateProcessW", SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CreateProcessW(
string applicationName,
StringBuilder commandLine,
nint processAttributes,
nint threadAttributes,
[MarshalAs(UnmanagedType.Bool)] bool inheritHandles,
uint creationFlags,
nint environment,
string currentDirectory,
ref STARTUPINFOEX startupInfo,
out PROCESS_INFORMATION processInformation);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint WaitForSingleObject(nint handle, uint milliseconds);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetExitCodeProcess(nint process, out uint exitCode);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool TerminateProcess(nint process, uint exitCode);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CloseHandle(nint handle);
}
+41
View File
@@ -0,0 +1,41 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\SharpEmu.Core\SharpEmu.Core.csproj" />
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<AssemblyName>SharpEmu</AssemblyName>
<RuntimeIdentifiers>win-x64;linux-x64;osx-arm64</RuntimeIdentifiers>
<SelfContained>true</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Version>0.0.1</Version>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
<ItemGroup>
<Content Include="..\..\LICENSE.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<TargetPath>LICENSE.txt</TargetPath>
<Visible>False</Visible>
</Content>
</ItemGroup>
</Project>
+43
View File
@@ -0,0 +1,43 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "0B6nZyCHWXnvmlB559oduOspVdNOnpNXPjhpWVMovLPAsDVG7A4jJR9rzECf67JUzxP8/ee/wA8clwIzJcWNFA=="
},
"sharpemu.core": {
"type": "Project",
"dependencies": {
"Iced": "[1.21.0, )",
"SharpEmu.HLE": "[1.0.0, )",
"SharpEmu.Libs": "[1.0.0, )",
"SharpEmu.Logging": "[1.0.0, )"
}
},
"sharpemu.hle": {
"type": "Project"
},
"sharpemu.libs": {
"type": "Project",
"dependencies": {
"SharpEmu.HLE": "[1.0.0, )"
}
},
"sharpemu.logging": {
"type": "Project"
},
"Iced": {
"type": "CentralTransitive",
"requested": "[1.21.0, )",
"resolved": "1.21.0",
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
}
},
"net10.0/linux-x64": {},
"net10.0/osx-arm64": {},
"net10.0/win-x64": {}
}
}
@@ -0,0 +1,63 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Core.Cpu;
public readonly struct CpuControlTransferInfo
{
public CpuControlTransferInfo(
ulong sourceInstructionPointer,
byte opcode,
ulong targetInstructionPointer,
CpuControlTransferKind kind,
bool isIndirect,
ulong rax,
ulong rbx,
ulong rcx,
ulong rdx,
ulong rsi,
ulong rdi,
ulong rsp,
ulong rbp)
{
SourceInstructionPointer = sourceInstructionPointer;
Opcode = opcode;
TargetInstructionPointer = targetInstructionPointer;
Kind = kind;
IsIndirect = isIndirect;
Rax = rax;
Rbx = rbx;
Rcx = rcx;
Rdx = rdx;
Rsi = rsi;
Rdi = rdi;
Rsp = rsp;
Rbp = rbp;
}
public ulong SourceInstructionPointer { get; }
public byte Opcode { get; }
public ulong TargetInstructionPointer { get; }
public CpuControlTransferKind Kind { get; }
public bool IsIndirect { get; }
public ulong Rax { get; }
public ulong Rbx { get; }
public ulong Rcx { get; }
public ulong Rdx { get; }
public ulong Rsi { get; }
public ulong Rdi { get; }
public ulong Rsp { get; }
public ulong Rbp { get; }
}
@@ -0,0 +1,13 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Core.Cpu;
public enum CpuControlTransferKind
{
Call = 0,
Jump = 1,
Return = 2,
}
+580
View File
@@ -0,0 +1,580 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Text;
using SharpEmu.Core.Cpu.Native;
using SharpEmu.Core.Loader;
using SharpEmu.Core.Memory;
using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu;
public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
{
private const ulong StackBaseAddress = 0x7FFF_F000_0000UL;
private const ulong StackSize = 0x0020_0000UL;
private const ulong TlsBaseAddress = 0x7FFE_0000_0000UL;
private const ulong TlsSize = 0x0001_0000UL;
private const ulong BootstrapStubBaseAddress = 0x7FFD_F000_0000UL;
private const ulong BootstrapPayloadBaseAddress = 0x7FFD_E000_0000UL;
private const ulong DynlibFallbackStubBaseAddress = 0x7FFD_D000_0000UL;
private const ulong ReturnToHostStubBaseAddress = 0x7FFD_C000_0000UL;
private const ulong BootstrapRegionSize = 0x0000_1000UL;
private const ulong ReturnToHostStubStride = 0x0100_0000UL;
private const ulong BootstrapPayloadResultOffset = 0x28UL;
private const ulong BootstrapStatusOffset = 0x100UL;
private static readonly byte[] BootstrapStartSignature =
[
0x55, 0x48, 0x89, 0xE5, 0x41, 0x57, 0x41, 0x56,
0x41, 0x55, 0x41, 0x54, 0x53, 0x50, 0x48, 0x89,
];
private readonly IVirtualMemory _virtualMemory;
private readonly IModuleManager _moduleManager;
private INativeCpuBackend? _nativeCpuBackend;
public CpuDispatcher(
IVirtualMemory virtualMemory,
IModuleManager moduleManager,
INativeCpuBackend? nativeCpuBackend = null)
{
_virtualMemory = virtualMemory ?? throw new ArgumentNullException(nameof(virtualMemory));
_moduleManager = moduleManager ?? throw new ArgumentNullException(nameof(moduleManager));
_nativeCpuBackend = nativeCpuBackend;
}
public ulong? LastEntryPoint { get; private set; }
public CpuTrapInfo? LastTrapInfo { get; private set; }
public CpuMemoryFaultInfo? LastMemoryFaultInfo { get; private set; }
public CpuControlTransferInfo? LastControlTransferInfo { get; private set; }
public CpuNotImplementedInfo? LastNotImplementedInfo { get; private set; }
public string? LastImportResolutionTrace { get; private set; }
public string? LastBasicBlockTrace { get; private set; }
public string? LastMilestoneLog { get; private set; }
public string? LastRecentInstructionWindow { get; private set; }
public string? LastRecentControlTransferTrace { get; private set; }
public CpuSessionSummary LastSessionSummary { get; private set; }
public OrbisGen2Result DispatchEntry(
ulong entryPoint,
Generation generation,
IReadOnlyDictionary<ulong, string>? importStubs = null,
IReadOnlyDictionary<string, ulong>? runtimeSymbols = null,
string processImageName = "eboot.bin",
CpuExecutionOptions executionOptions = default)
{
Console.Error.WriteLine("[DISPATCHER] === DispatchEntry START ===");
Console.Error.WriteLine($"[DISPATCHER] entryPoint=0x{entryPoint:X16}, generation={generation}");
try
{
return DispatchEntryCore(entryPoint, generation, importStubs, runtimeSymbols, processImageName, executionOptions);
}
catch (Exception ex)
{
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}");
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
throw;
}
}
private OrbisGen2Result DispatchEntryCore(
ulong entryPoint,
Generation generation,
IReadOnlyDictionary<ulong, string>? importStubs = null,
IReadOnlyDictionary<string, ulong>? runtimeSymbols = null,
string processImageName = "eboot.bin",
CpuExecutionOptions executionOptions = default)
{
Console.Error.WriteLine("[DISPATCHER] DispatchEntryCore STARTING...");
LastEntryPoint = entryPoint;
LastTrapInfo = null;
LastMemoryFaultInfo = null;
LastControlTransferInfo = null;
LastNotImplementedInfo = null;
LastImportResolutionTrace = null;
LastBasicBlockTrace = null;
LastMilestoneLog = null;
LastRecentInstructionWindow = null;
LastRecentControlTransferTrace = null;
LastSessionSummary = default;
OrbisGen2Result FailEarly(OrbisGen2Result result, CpuExitReason reason = CpuExitReason.UnhandledException)
{
LastSessionSummary = new CpuSessionSummary(
result,
reason,
exitCode: null,
lastGuestRip: entryPoint,
lastStubRip: 0,
totalInstructions: 0,
importsHit: 0,
uniqueNidsHit: 0);
return result;
}
var stackBase = TryMapStackRegion();
if (stackBase == 0)
{
return FailEarly(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var tlsBase = TryMapTlsRegion();
if (tlsBase == 0)
{
return FailEarly(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var trackedMemory = new TrackedCpuMemory(_virtualMemory);
var context = new CpuContext(trackedMemory, generation)
{
Rip = entryPoint,
Rflags = 0x202,
FsBase = tlsBase,
GsBase = tlsBase,
};
var returnToHostStubAddress = TryMapReturnToHostStubRegion();
if (returnToHostStubAddress == 0)
{
return FailEarly(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
context[CpuRegister.Rsp] = stackBase + StackSize - sizeof(ulong);
if (!context.TryWriteUInt64(context[CpuRegister.Rsp], returnToHostStubAddress))
{
return FailEarly(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (!InitializeTls(context, tlsBase))
{
return FailEarly(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var effectiveImportStubs = importStubs is null
? new Dictionary<ulong, string>()
: new Dictionary<ulong, string>(importStubs);
var programExitHandlerStubAddress = TryMapDynlibFallbackStubRegion();
if (programExitHandlerStubAddress == 0)
{
return FailEarly(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (!InitializeProcessEntryFrame(context, processImageName, programExitHandlerStubAddress))
{
return FailEarly(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (ShouldInjectBootstrapPayload(entryPoint))
{
if (!TryInstallBootstrapPayload(context, effectiveImportStubs))
{
return FailEarly(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
}
var entryFrameDiagnostic = BuildEntryFrameDiagnostic(
entryPoint,
context,
sentinelEnabled: true,
sentinelValue: returnToHostStubAddress,
entryParamsConfigured: true);
if (executionOptions.CpuEngine != CpuExecutionEngine.NativeOnly)
{
LastMilestoneLog = string.Concat(
entryFrameDiagnostic,
Environment.NewLine,
$"CpuEngine: {executionOptions.CpuEngine} (unsupported)");
LastNotImplementedInfo = new CpuNotImplementedInfo(
CpuNotImplementedSource.NativeBackend,
entryPoint,
nid: null,
exportName: "cpu_engine_unsupported",
libraryName: executionOptions.CpuEngine.ToString(),
detail: "Unsupported CPU engine mode.");
return FailEarly(
OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED,
CpuExitReason.NativeBackendUnavailable);
}
LastMilestoneLog = string.Concat(
entryFrameDiagnostic,
Environment.NewLine,
"CpuEngine: native-only");
_nativeCpuBackend ??= new DirectExecutionBackend(_moduleManager);
if (_nativeCpuBackend.TryExecute(
context,
entryPoint,
generation,
effectiveImportStubs,
runtimeSymbols ?? new Dictionary<string, ulong>(StringComparer.Ordinal),
executionOptions,
out var nativeResult))
{
LastSessionSummary = new CpuSessionSummary(
nativeResult,
nativeResult == OrbisGen2Result.ORBIS_GEN2_OK
? CpuExitReason.ReturnedToHost
: CpuExitReason.UnhandledException,
exitCode: null,
lastGuestRip: context.Rip,
lastStubRip: 0,
totalInstructions: 0,
importsHit: 0,
uniqueNidsHit: 0);
return nativeResult;
}
var backendName = string.IsNullOrWhiteSpace(_nativeCpuBackend.BackendName)
? "native-backend"
: _nativeCpuBackend.BackendName;
var backendError = string.IsNullOrWhiteSpace(_nativeCpuBackend.LastError)
? "unknown backend error"
: _nativeCpuBackend.LastError;
LastNotImplementedInfo = new CpuNotImplementedInfo(
CpuNotImplementedSource.NativeBackend,
entryPoint,
nid: null,
exportName: "cpu_engine_native_only",
libraryName: backendName,
detail: backendError);
LastMilestoneLog = string.Concat(
LastMilestoneLog,
Environment.NewLine,
$"CpuEngine native-only failed: {backendError}");
Console.Error.WriteLine($"[DISPATCHER] Native backend FAILED: {backendError}");
return FailEarly(
OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED,
CpuExitReason.NativeBackendUnavailable);
}
private ulong TryMapStackRegion()
{
const ulong stackStride = 0x0100_0000UL;
for (var i = 0; i < 32; i++)
{
var candidateBase = StackBaseAddress - ((ulong)i * stackStride);
try
{
_virtualMemory.Map(
candidateBase,
StackSize,
fileOffset: 0,
fileData: ReadOnlySpan<byte>.Empty,
ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
return candidateBase;
}
catch (InvalidOperationException)
{
continue;
}
}
return 0;
}
private ulong TryMapTlsRegion()
{
const ulong tlsStride = 0x0100_0000UL;
for (var i = 0; i < 32; i++)
{
var candidateBase = TlsBaseAddress - ((ulong)i * tlsStride);
try
{
_virtualMemory.Map(
candidateBase,
TlsSize,
fileOffset: 0,
fileData: ReadOnlySpan<byte>.Empty,
ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
return candidateBase;
}
catch (InvalidOperationException)
{
continue;
}
}
return 0;
}
private static bool InitializeTls(CpuContext context, ulong tlsBase)
{
return context.TryWriteUInt64(tlsBase + 0x00, tlsBase) &&
context.TryWriteUInt64(tlsBase + 0x10, tlsBase) &&
context.TryWriteUInt64(tlsBase + 0x28, 0xC0DEC0DECAFEBABEUL);
}
private static bool InitializeProcessEntryFrame(
CpuContext context,
string processImageName,
ulong programExitHandlerAddress)
{
var imageName = string.IsNullOrWhiteSpace(processImageName) ? "eboot.bin" : processImageName;
var encodedNameLength = Encoding.UTF8.GetByteCount(imageName);
Span<byte> argv0Buffer = encodedNameLength + 1 <= 512
? stackalloc byte[encodedNameLength + 1]
: new byte[encodedNameLength + 1];
if (Encoding.UTF8.GetBytes(imageName.AsSpan(), argv0Buffer) != encodedNameLength)
{
return false;
}
argv0Buffer[encodedNameLength] = 0;
var cursor = context[CpuRegister.Rsp];
var argv0Address = AlignDown(cursor - (ulong)argv0Buffer.Length, 16);
if (!context.Memory.TryWrite(argv0Address, argv0Buffer))
{
return false;
}
const ulong entryParamsSize = 0x20;
var entryParamsAddress = AlignDown(argv0Address - entryParamsSize, 16);
if (!TryWriteUInt32(context, entryParamsAddress + 0x00, 1) ||
!TryWriteUInt32(context, entryParamsAddress + 0x04, 0) ||
!context.TryWriteUInt64(entryParamsAddress + 0x08, argv0Address) ||
!context.TryWriteUInt64(entryParamsAddress + 0x10, 0) ||
!context.TryWriteUInt64(entryParamsAddress + 0x18, 0))
{
return false;
}
context[CpuRegister.Rdi] = entryParamsAddress;
context[CpuRegister.Rsi] = programExitHandlerAddress;
context[CpuRegister.Rdx] = 0;
context[CpuRegister.Rcx] = 0;
context[CpuRegister.R8] = 0;
context[CpuRegister.R9] = 0;
return true;
}
private static ulong AlignDown(ulong value, ulong alignment)
{
return value & ~(alignment - 1);
}
private static bool TryWriteUInt32(CpuContext context, ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
return context.Memory.TryWrite(address, buffer);
}
private static string BuildEntryFrameDiagnostic(
ulong entryPoint,
CpuContext context,
bool sentinelEnabled,
ulong sentinelValue,
bool entryParamsConfigured)
{
var initialRsp = context[CpuRegister.Rsp];
var stackTopText = context.TryReadUInt64(initialRsp, out var stackTop)
? $"0x{stackTop:X16}"
: "??";
var sentinelText = sentinelEnabled ? "yes" : "no";
var sentinelValueText = sentinelEnabled ? $"0x{sentinelValue:X16}" : "??";
var entryParamsText = entryParamsConfigured ? "yes" : "no";
return
$"EntryFrame: entry_rip=0x{entryPoint:X16} initial_rsp=0x{initialRsp:X16} [rsp]={stackTopText} sentinel_enabled={sentinelText} sentinel_value={sentinelValueText} entry_params_configured={entryParamsText}";
}
private bool ShouldInjectBootstrapPayload(ulong entryPoint)
{
Span<byte> probe = stackalloc byte[16];
if (!_virtualMemory.TryRead(entryPoint, probe))
{
return false;
}
for (var i = 0; i < BootstrapStartSignature.Length; i++)
{
if (probe[i] != BootstrapStartSignature[i])
{
return false;
}
}
return true;
}
private bool TryInstallBootstrapPayload(CpuContext context, IDictionary<ulong, string> importStubs)
{
var stubAddress = TryMapBootstrapStubRegion();
if (stubAddress == 0)
{
return false;
}
var payloadAddress = TryMapBootstrapPayloadRegion();
if (payloadAddress == 0)
{
return false;
}
var statusAddress = payloadAddress + BootstrapStatusOffset;
if (!TryWriteUInt64(payloadAddress, stubAddress) ||
!TryWriteUInt64(payloadAddress + 0x08, statusAddress) ||
!TryWriteUInt64(payloadAddress + 0x10, statusAddress) ||
!TryWriteUInt64(payloadAddress + 0x18, statusAddress) ||
!TryWriteUInt64(payloadAddress + 0x20, statusAddress) ||
!TryWriteUInt64(payloadAddress + BootstrapPayloadResultOffset, statusAddress) ||
!TryWriteUInt32(statusAddress, 0))
{
return false;
}
importStubs[stubAddress] = RuntimeStubNids.BootstrapBridge;
importStubs[stubAddress + 0x0A] = RuntimeStubNids.BootstrapBridge;
context[CpuRegister.Rdi] = payloadAddress;
return true;
}
private ulong TryMapBootstrapStubRegion()
{
var stubData = new byte[(int)BootstrapRegionSize];
stubData[0] = 0xCC;
stubData[1] = 0xC3;
const ulong stride = 0x0100_0000UL;
for (var i = 0; i < 16; i++)
{
var candidateBase = BootstrapStubBaseAddress - ((ulong)i * stride);
try
{
_virtualMemory.Map(
candidateBase,
BootstrapRegionSize,
fileOffset: 0,
stubData,
ProgramHeaderFlags.Read | ProgramHeaderFlags.Execute);
return candidateBase;
}
catch (InvalidOperationException)
{
continue;
}
}
return 0;
}
private ulong TryMapBootstrapPayloadRegion()
{
const ulong stride = 0x0100_0000UL;
for (var i = 0; i < 16; i++)
{
var candidateBase = BootstrapPayloadBaseAddress - ((ulong)i * stride);
try
{
_virtualMemory.Map(
candidateBase,
BootstrapRegionSize,
fileOffset: 0,
ReadOnlySpan<byte>.Empty,
ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
return candidateBase;
}
catch (InvalidOperationException)
{
continue;
}
}
return 0;
}
private ulong TryMapDynlibFallbackStubRegion()
{
var stubData = new byte[(int)BootstrapRegionSize];
stubData[0] = 0x31;
stubData[1] = 0xC0;
stubData[2] = 0xC3;
const ulong stride = 0x0100_0000UL;
for (var i = 0; i < 16; i++)
{
var candidateBase = DynlibFallbackStubBaseAddress - ((ulong)i * stride);
try
{
_virtualMemory.Map(
candidateBase,
BootstrapRegionSize,
fileOffset: 0,
stubData,
ProgramHeaderFlags.Read | ProgramHeaderFlags.Execute);
return candidateBase;
}
catch (InvalidOperationException)
{
continue;
}
}
return 0;
}
private ulong TryMapReturnToHostStubRegion()
{
var stubData = new byte[(int)BootstrapRegionSize];
stubData[0] = 0xF4;
stubData[1] = 0xCC;
for (var i = 0; i < 16; i++)
{
var candidateBase = ReturnToHostStubBaseAddress - ((ulong)i * ReturnToHostStubStride);
try
{
_virtualMemory.Map(
candidateBase,
BootstrapRegionSize,
fileOffset: 0,
stubData,
ProgramHeaderFlags.Read | ProgramHeaderFlags.Execute);
return candidateBase;
}
catch (InvalidOperationException)
{
continue;
}
}
return 0;
}
private bool TryWriteUInt64(ulong address, ulong value)
{
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
return _virtualMemory.TryWrite(address, buffer);
}
private bool TryWriteUInt32(ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
return _virtualMemory.TryWrite(address, buffer);
}
public void Dispose()
{
if (_nativeCpuBackend is IDisposable disposableBackend)
{
disposableBackend.Dispose();
}
_nativeCpuBackend = null;
}
}
@@ -0,0 +1,9 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Core.Cpu;
public enum CpuExecutionEngine
{
NativeOnly = 0,
}
@@ -0,0 +1,15 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Core.Cpu;
public readonly struct CpuExecutionOptions
{
public bool EnableDisasmDiagnostics { get; init; }
public CpuExecutionEngine CpuEngine { get; init; }
public bool StrictDynlibResolution { get; init; }
public int ImportTraceLimit { get; init; }
}
+25
View File
@@ -0,0 +1,25 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Core.Cpu;
public enum CpuExitReason
{
Exited = 0,
SentinelReturn = 1,
ReturnedToHost = 2,
Halted = 3,
BudgetExceeded = 4,
CpuTrap = 5,
UnhandledException = 6,
UnhandledSyscall = 7,
NativeBackendUnavailable = 8,
}
@@ -0,0 +1,20 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Core.Cpu;
public readonly struct CpuMemoryAccessFailure
{
public CpuMemoryAccessFailure(ulong address, int size, bool isWrite)
{
Address = address;
Size = size;
IsWrite = isWrite;
}
public ulong Address { get; }
public int Size { get; }
public bool IsWrite { get; }
}
@@ -0,0 +1,20 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Core.Cpu;
public readonly struct CpuMemoryFaultInfo
{
public CpuMemoryFaultInfo(ulong instructionPointer, byte? opcode, CpuMemoryAccessFailure access)
{
InstructionPointer = instructionPointer;
Opcode = opcode;
Access = access;
}
public ulong InstructionPointer { get; }
public byte? Opcode { get; }
public CpuMemoryAccessFailure Access { get; }
}
@@ -0,0 +1,35 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Core.Cpu;
public readonly struct CpuNotImplementedInfo
{
public CpuNotImplementedInfo(
CpuNotImplementedSource source,
ulong instructionPointer,
string? nid,
string? exportName,
string? libraryName,
string? detail)
{
Source = source;
InstructionPointer = instructionPointer;
Nid = nid;
ExportName = exportName;
LibraryName = libraryName;
Detail = detail;
}
public CpuNotImplementedSource Source { get; }
public ulong InstructionPointer { get; }
public string? Nid { get; }
public string? ExportName { get; }
public string? LibraryName { get; }
public string? Detail { get; }
}
@@ -0,0 +1,17 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Core.Cpu;
public enum CpuNotImplementedSource
{
Unknown = 0,
InstructionBudget = 1,
KernelDynlibDlsym = 2,
HleExport = 3,
NativeBackend = 4,
}
@@ -0,0 +1,45 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu;
public readonly struct CpuSessionSummary
{
public CpuSessionSummary(
OrbisGen2Result result,
CpuExitReason reason,
int? exitCode,
ulong lastGuestRip,
ulong lastStubRip,
int totalInstructions,
int importsHit,
int uniqueNidsHit)
{
Result = result;
Reason = reason;
ExitCode = exitCode;
LastGuestRip = lastGuestRip;
LastStubRip = lastStubRip;
TotalInstructions = totalInstructions;
ImportsHit = importsHit;
UniqueNidsHit = uniqueNidsHit;
}
public OrbisGen2Result Result { get; }
public CpuExitReason Reason { get; }
public int? ExitCode { get; }
public ulong LastGuestRip { get; }
public ulong LastStubRip { get; }
public int TotalInstructions { get; }
public int ImportsHit { get; }
public int UniqueNidsHit { get; }
}
+17
View File
@@ -0,0 +1,17 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Core.Cpu;
public readonly struct CpuTrapInfo
{
public CpuTrapInfo(ulong instructionPointer, byte opcode)
{
InstructionPointer = instructionPointer;
Opcode = opcode;
}
public ulong InstructionPointer { get; }
public byte Opcode { get; }
}
@@ -0,0 +1,45 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using Iced.Intel;
namespace SharpEmu.Core.Cpu.Disasm;
public readonly struct DecodedInst
{
public DecodedInst(
ulong rip,
int length,
string text,
string mnemonic,
FlowControl flowControl,
ulong? nearBranchTarget,
ulong? memoryAddress,
byte[] bytes)
{
Rip = rip;
Length = length;
Text = text;
Mnemonic = mnemonic;
FlowControl = flowControl;
NearBranchTarget = nearBranchTarget;
MemoryAddress = memoryAddress;
Bytes = bytes;
}
public ulong Rip { get; }
public int Length { get; }
public string Text { get; }
public string Mnemonic { get; }
public FlowControl FlowControl { get; }
public ulong? NearBranchTarget { get; }
public ulong? MemoryAddress { get; }
public byte[] Bytes { get; }
}
+213
View File
@@ -0,0 +1,213 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using Iced.Intel;
using SharpEmu.Core.Memory;
using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu.Disasm;
public static class IcedDecoder
{
private const int MaxInstructionBytes = 15;
public static bool TryDecode(ulong rip, ReadOnlySpan<byte> codeBytes, out DecodedInst inst)
{
if (codeBytes.IsEmpty)
{
inst = default;
return false;
}
var decodeLength = Math.Min(MaxInstructionBytes, codeBytes.Length);
var decodeBytes = GC.AllocateUninitializedArray<byte>(decodeLength);
codeBytes[..decodeLength].CopyTo(decodeBytes);
try
{
var decoder = Decoder.Create(64, new ByteArrayCodeReader(decodeBytes));
decoder.IP = rip;
decoder.Decode(out var instruction);
if (instruction.Code == Code.INVALID || instruction.Length <= 0)
{
inst = default;
return false;
}
var formatter = new IntelFormatter();
var output = new StringOutput();
formatter.Format(instruction, output);
var effectiveLength = Math.Min(instruction.Length, decodeBytes.Length);
var effectiveBytes = new byte[effectiveLength];
Array.Copy(decodeBytes, 0, effectiveBytes, 0, effectiveLength);
inst = new DecodedInst(
rip,
instruction.Length,
output.ToString(),
instruction.Mnemonic.ToString(),
instruction.FlowControl,
GetNearBranchTarget(in instruction),
GetMemoryAddress(in instruction),
effectiveBytes);
return true;
}
catch
{
inst = default;
return false;
}
}
public static bool TryReadGuestBytes(ICpuMemory memory, ulong rip, int maxLen, out byte[] bytes)
{
ArgumentNullException.ThrowIfNull(memory);
var clampedLength = Math.Clamp(maxLen, 1, MaxInstructionBytes);
var buffer = new byte[clampedLength];
Span<byte> oneByte = stackalloc byte[1];
var readCount = 0;
for (var i = 0; i < clampedLength; i++)
{
if (!memory.TryRead(rip + (ulong)i, oneByte))
{
break;
}
buffer[readCount] = oneByte[0];
readCount++;
}
if (readCount == 0)
{
bytes = Array.Empty<byte>();
return false;
}
if (readCount == clampedLength)
{
bytes = buffer;
return true;
}
bytes = new byte[readCount];
Array.Copy(buffer, bytes, readCount);
return true;
}
public static bool TryReadGuestBytes(IVirtualMemory memory, ulong rip, int maxLen, out byte[] bytes)
{
ArgumentNullException.ThrowIfNull(memory);
var clampedLength = Math.Clamp(maxLen, 1, MaxInstructionBytes);
var buffer = new byte[clampedLength];
Span<byte> oneByte = stackalloc byte[1];
var readCount = 0;
for (var i = 0; i < clampedLength; i++)
{
if (!memory.TryRead(rip + (ulong)i, oneByte))
{
break;
}
buffer[readCount] = oneByte[0];
readCount++;
}
if (readCount == 0)
{
bytes = Array.Empty<byte>();
return false;
}
if (readCount == clampedLength)
{
bytes = buffer;
return true;
}
bytes = new byte[readCount];
Array.Copy(buffer, bytes, readCount);
return true;
}
public static string FormatBytes(ReadOnlySpan<byte> bytes)
{
if (bytes.IsEmpty)
{
return "??";
}
var parts = new string[bytes.Length];
for (var i = 0; i < bytes.Length; i++)
{
parts[i] = bytes[i].ToString("X2");
}
return string.Join(' ', parts);
}
private static ulong? GetNearBranchTarget(in Instruction instruction)
{
for (var opIndex = 0; opIndex < instruction.OpCount; opIndex++)
{
switch (instruction.GetOpKind(opIndex))
{
case OpKind.NearBranch16:
return instruction.NearBranch16;
case OpKind.NearBranch32:
return instruction.NearBranch32;
case OpKind.NearBranch64:
return instruction.NearBranch64;
}
}
return null;
}
private static ulong? GetMemoryAddress(in Instruction instruction)
{
var hasMemoryOperand = false;
for (var opIndex = 0; opIndex < instruction.OpCount; opIndex++)
{
if (IsMemoryOpKind(instruction.GetOpKind(opIndex)))
{
hasMemoryOperand = true;
break;
}
}
if (!hasMemoryOperand)
{
return null;
}
if (instruction.IsIPRelativeMemoryOperand)
{
return instruction.IPRelativeMemoryAddress;
}
if (instruction.MemoryBase == Register.None &&
instruction.MemoryIndex == Register.None &&
instruction.MemoryDisplacement64 != 0)
{
return instruction.MemoryDisplacement64;
}
return null;
}
private static bool IsMemoryOpKind(OpKind opKind)
{
return opKind is
OpKind.Memory or
OpKind.MemorySegSI or
OpKind.MemorySegESI or
OpKind.MemorySegRSI or
OpKind.MemorySegDI or
OpKind.MemorySegEDI or
OpKind.MemorySegRDI or
OpKind.MemoryESDI or
OpKind.MemoryESEDI or
OpKind.MemoryESRDI;
}
}
+37
View File
@@ -0,0 +1,37 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu;
public interface ICpuDispatcher
{
CpuTrapInfo? LastTrapInfo { get; }
CpuMemoryFaultInfo? LastMemoryFaultInfo { get; }
CpuControlTransferInfo? LastControlTransferInfo { get; }
CpuNotImplementedInfo? LastNotImplementedInfo { get; }
string? LastImportResolutionTrace { get; }
string? LastBasicBlockTrace { get; }
string? LastMilestoneLog { get; }
string? LastRecentInstructionWindow { get; }
string? LastRecentControlTransferTrace { get; }
CpuSessionSummary LastSessionSummary { get; }
OrbisGen2Result DispatchEntry(
ulong entryPoint,
Generation generation,
IReadOnlyDictionary<ulong, string>? importStubs = null,
IReadOnlyDictionary<string, ulong>? runtimeSymbols = null,
string processImageName = "eboot.bin",
CpuExecutionOptions executionOptions = default);
}
@@ -0,0 +1,9 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Core.Cpu;
public interface ITrackedCpuMemory
{
CpuMemoryAccessFailure? LastFailure { get; }
}
@@ -0,0 +1,47 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System;
using System.Runtime.InteropServices;
namespace SharpEmu.Core.Cpu.Native;
public sealed unsafe class CpuPatcher : IDisposable
{
private readonly nint _tlsBaseAddress;
public CpuPatcher(nint tlsBaseAddress)
{
_tlsBaseAddress = tlsBaseAddress;
}
public bool TryPatchInstruction(nint address)
{
return false;
}
public void Dispose()
{
}
internal unsafe class UnsafeCodeReader : Iced.Intel.CodeReader
{
private byte* _start;
private byte* _current;
private int _length;
public UnsafeCodeReader(byte* start, int length)
{
_start = start;
_current = start;
_length = length;
}
public override int ReadByte()
{
if (_current >= _start + _length)
return -1;
return *_current++;
}
}
}
@@ -0,0 +1,264 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu.Native;
public sealed partial class DirectExecutionBackend
{
private void RecordRecentImportTrace(string traceLine)
{
_recentImportTrace[_recentImportTraceWriteIndex] = traceLine;
_recentImportTraceWriteIndex = (_recentImportTraceWriteIndex + 1) % _recentImportTrace.Length;
if (_recentImportTraceCount < _recentImportTrace.Length)
{
_recentImportTraceCount++;
}
}
private void DumpRecentImportTrace()
{
if (_recentImportTraceCount == 0)
{
return;
}
Console.Error.WriteLine($"[LOADER][INFO] Recent import calls ({_recentImportTraceCount}):");
int num = (_recentImportTraceWriteIndex - _recentImportTraceCount + _recentImportTrace.Length) % _recentImportTrace.Length;
for (int i = 0; i < _recentImportTraceCount; i++)
{
int num2 = (num + i) % _recentImportTrace.Length;
string text = _recentImportTrace[num2];
if (!string.IsNullOrEmpty(text))
{
Console.Error.WriteLine("[LOADER][INFO] " + text);
}
}
}
private unsafe static List<ulong> ScanSuspiciousResolverPointers(ulong scanStart, ulong scanEnd)
{
if (scanEnd <= scanStart)
{
return new List<ulong>(0);
}
int num = 0;
int num2 = 0;
List<ulong> list = new List<ulong>(16);
ulong num3 = scanStart;
MEMORY_BASIC_INFORMATION64 lpBuffer;
while (num3 < scanEnd && VirtualQuery((void*)num3, out lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) != 0)
{
ulong baseAddress = lpBuffer.BaseAddress;
ulong num4 = baseAddress + lpBuffer.RegionSize;
if (num4 <= num3)
{
break;
}
ulong value = Math.Max(num3, baseAddress);
ulong num5 = Math.Min(num4, scanEnd);
if (lpBuffer.State == 4096 && IsReadableProtection(lpBuffer.Protect) && !IsExecutableProtection(lpBuffer.Protect))
{
ulong num6 = AlignUp(value, 8uL);
for (ulong num7 = num6; num7 + 8 <= num5; num7 += 8)
{
ulong value2 = *(ulong*)num7;
if (IsUnresolvedSentinel(value2))
{
num++;
list.Add(num7);
if (num2 < 32)
{
Console.Error.WriteLine($"[LOADER][INFO] Suspicious unresolved pointer: slot=0x{num7:X16} value=0x{value2:X16}");
num2++;
}
if (num >= 16384)
{
Console.Error.WriteLine($"[LOADER][WARNING] Suspicious unresolved pointer scan reached cap ({16384}); truncating.");
return list;
}
}
}
}
num3 = num5;
}
if (num != 0)
{
Console.Error.WriteLine($"[LOADER][WARNING] Suspicious unresolved pointer hits: {num}");
}
return list;
}
private void ProbeReturnRip(ulong returnRip, long dispatchIndex)
{
if (_cpuContext == null || returnRip == 0)
{
return;
}
Span<byte> destination = stackalloc byte[128];
if (!_cpuContext.Memory.TryRead(returnRip, destination))
{
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip probe: unreadable @0x{returnRip:X16}");
return;
}
string value = BitConverter.ToString(destination.ToArray()).Replace("-", " ");
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip bytes @0x{returnRip:X16}: {value}");
if (destination[0] == byte.MaxValue && (destination[1] == 21 || destination[1] == 37))
{
int num = BitConverter.ToInt32(destination.Slice(2, 4));
ulong num2 = returnRip + 6 + (ulong)num;
if (_cpuContext.TryReadUInt64(num2, out var value2))
{
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip slot: [0x{num2:X16}] = 0x{value2:X16}");
}
}
if (destination[0] == 72 && destination[1] == 139 && destination[2] == 5)
{
int num3 = BitConverter.ToInt32(destination.Slice(3, 4));
ulong num4 = returnRip + 7 + (ulong)num3;
if (_cpuContext.TryReadUInt64(num4, out var value3))
{
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip mov-slot: [0x{num4:X16}] = 0x{value3:X16}");
}
}
for (int i = 0; i + 6 <= destination.Length; i++)
{
if (destination[i] == byte.MaxValue && (destination[i + 1] == 21 || destination[i + 1] == 37))
{
int num5 = BitConverter.ToInt32(destination.Slice(i + 2, 4));
ulong num6 = returnRip + (ulong)i;
ulong num7 = num6 + 6 + (ulong)num5;
if (_cpuContext.TryReadUInt64(num7, out var value4))
{
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} near-indirect @{num6:X16}: slot=0x{num7:X16} val=0x{value4:X16}");
}
}
}
}
private static bool IsUnresolvedSentinel(ulong value)
{
return value == 65534 || value == 4294967294u || value == 18446744073709551614uL;
}
private static bool IsPlausibleReturnAddress(ulong address)
{
return address >= 12884901888L && address < 17592186044416L && !IsUnresolvedSentinel(address);
}
private static bool TryGetPlausibleReturnFromStack(ulong rsp, out ulong returnRip, out ulong nextRsp)
{
returnRip = 0uL;
nextRsp = rsp;
if (rsp <= 65536 || rsp >= 140737488355328L)
{
return false;
}
ulong num = rsp & 0xFFFFFFFFFFFFFFF8uL;
ulong num2 = ((num >= 8) ? (num - 8) : num);
for (int i = 0; i < 24; i++)
{
ulong num3 = num2 + (ulong)((long)i * 8L);
if (TryReadStackU64(num3, out var value) && IsLikelyReturnAddress(value))
{
returnRip = value;
nextRsp = num3 + 8;
return true;
}
}
for (ulong num4 = 1uL; num4 < 8; num4++)
{
for (int j = 0; j < 24; j++)
{
ulong num5 = rsp + num4 + (ulong)((long)j * 8L);
if (TryReadStackU64(num5, out var value2) && IsLikelyReturnAddress(value2))
{
returnRip = value2;
ulong num6 = num5 + 8;
nextRsp = (num6 + 7) & 0xFFFFFFFFFFFFFFF8uL;
return true;
}
}
}
return false;
}
private unsafe static bool TryReadStackU64(ulong address, out ulong value)
{
value = 0uL;
if (address <= 65536 || address >= 140737488355328L)
{
return false;
}
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
{
return false;
}
ulong num = lpBuffer.BaseAddress + lpBuffer.RegionSize;
if (num < lpBuffer.BaseAddress || address > num - 8)
{
return false;
}
if (lpBuffer.State != 4096 || !IsReadableProtection(lpBuffer.Protect))
{
return false;
}
try
{
value = *(ulong*)address;
return true;
}
catch
{
return false;
}
}
private static bool IsLikelyReturnAddress(ulong address)
{
if (!IsPlausibleReturnAddress(address))
{
return false;
}
return IsExecutableAddress(address);
}
private unsafe static bool IsExecutableAddress(ulong address)
{
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
{
return false;
}
return lpBuffer.State == 4096 && IsExecutableProtection(lpBuffer.Protect);
}
private static ulong AlignUp(ulong value, ulong alignment)
{
if (alignment == 0)
{
return value;
}
ulong num = alignment - 1;
return (value + num) & ~num;
}
private static bool IsReadableProtection(uint protect)
{
if ((protect & 0x100) != 0 || (protect & 1) != 0)
{
return false;
}
return (protect & 0xEE) != 0;
}
private static bool IsExecutableProtection(uint protect)
{
return (protect & 0xF0) != 0;
}
}
@@ -0,0 +1,740 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.InteropServices;
using SharpEmu.Core.Cpu.Disasm;
using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu.Native;
public sealed partial class DirectExecutionBackend
{
private unsafe void SetupExceptionHandler()
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_RAW_HANDLER"), "1", StringComparison.Ordinal))
{
_rawExceptionHandler = (nint)AddVectoredExceptionHandler(1u, RawVectoredHandlerPtrManaged);
Console.Error.WriteLine($"[LOADER][INFO] Raw exception handler installed: 0x{_rawExceptionHandler:X16}");
}
else
{
Console.Error.WriteLine("[LOADER][INFO] Raw exception handler disabled by SHARPEMU_DISABLE_RAW_HANDLER=1");
}
_handlerDelegate = VectoredHandler;
_handlerHandle = GCHandle.Alloc(_handlerDelegate);
_exceptionHandler = (nint)AddVectoredExceptionHandler(1u, Marshal.GetFunctionPointerForDelegate(_handlerDelegate));
Console.Error.WriteLine($"[LOADER][INFO] Exception handler installed: 0x{_exceptionHandler:X16}");
_unhandledFilterDelegate = UnhandledExceptionFilter;
_unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate);
SetUnhandledExceptionFilter(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate));
}
private unsafe int UnhandledExceptionFilter(void* exceptionInfo)
{
try
{
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
ulong rip = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, 248);
ulong rsp = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, 152);
Console.Error.WriteLine("[LOADER][FATAL] Unhandled exception filter fired.");
Console.Error.WriteLine($"[LOADER][FATAL] Code: 0x{exceptionRecord->ExceptionCode:X8}");
Console.Error.WriteLine($"[LOADER][FATAL] Exception Address: 0x{(ulong)(nint)exceptionRecord->ExceptionAddress:X16}");
Console.Error.WriteLine($"[LOADER][FATAL] RIP: 0x{rip:X16}");
Console.Error.WriteLine($"[LOADER][FATAL] RSP: 0x{rsp:X16}");
Console.Error.Flush();
}
catch
{
}
return 0;
}
private unsafe int VectoredHandler(void* exceptionInfo)
{
if (_vectoredHandlerDepth > 0)
{
Console.Error.WriteLine("[LOADER][TRACE] Nested VEH exception; passing through.");
Console.Error.Flush();
return 0;
}
_vectoredHandlerDepth++;
try
{
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
uint exceptionCode = exceptionRecord->ExceptionCode;
uint exceptionFlags = exceptionRecord->ExceptionFlags;
ulong exceptionAddress = (ulong)exceptionRecord->ExceptionAddress;
void* contextRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord;
if (contextRecord == null)
{
Console.Error.WriteLine("[LOADER][FATAL] ContextRecord is null!");
Console.Error.Flush();
return 0;
}
ulong rip = ReadCtxU64(contextRecord, 248);
ulong rsp = ReadCtxU64(contextRecord, 152);
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord))
{
return -1;
}
switch (exceptionCode)
{
case 3221225477u:
LogAccessViolationTrace(exceptionAddress, exceptionRecord);
break;
case 3221226505u:
{
ulong p0 = exceptionRecord->NumberParameters >= 1 ? (*exceptionRecord->ExceptionInformation) : 0;
ulong p1 = exceptionRecord->NumberParameters >= 2 ? exceptionRecord->ExceptionInformation[1] : 0;
Console.Error.WriteLine($"[LOADER][TRACE] VEH_FASTFAIL code=0x{exceptionCode:X8} ex=0x{exceptionAddress:X16} rip=0x{rip:X16} rsp=0x{rsp:X16} p0=0x{p0:X16} p1=0x{p1:X16}");
Console.Error.Flush();
break;
}
}
ulong rax = ReadCtxU64(contextRecord, 120);
ulong rbx = ReadCtxU64(contextRecord, 144);
ulong rcx = ReadCtxU64(contextRecord, 128);
ulong rdx = ReadCtxU64(contextRecord, 136);
ulong rsi = ReadCtxU64(contextRecord, 168);
ulong rdi = ReadCtxU64(contextRecord, 176);
ulong rbp = ReadCtxU64(contextRecord, 160);
ulong r8 = ReadCtxU64(contextRecord, 184);
ulong r9 = ReadCtxU64(contextRecord, 192);
ulong r10 = ReadCtxU64(contextRecord, 200);
ulong r11 = ReadCtxU64(contextRecord, 208);
ulong r12 = ReadCtxU64(contextRecord, 216);
ulong r13 = ReadCtxU64(contextRecord, 224);
ulong r14 = ReadCtxU64(contextRecord, 232);
ulong r15 = ReadCtxU64(contextRecord, 240);
Console.Error.WriteLine("[LOADER][INFO] =========================================");
Console.Error.WriteLine("[LOADER][INFO] NATIVE EXCEPTION CAUGHT!");
Console.Error.WriteLine($"[LOADER][INFO] Code: 0x{exceptionCode:X8}");
Console.Error.WriteLine($"[LOADER][INFO] Exception Address: 0x{exceptionAddress:X16}");
Console.Error.WriteLine($"[LOADER][INFO] RIP: 0x{rip:X16}");
if (TryFormatNearestRuntimeSymbol(rip, out string symbol))
{
Console.Error.WriteLine("[LOADER][INFO] RIP symbol: " + symbol);
}
Console.Error.WriteLine($"[LOADER][INFO] RAX: 0x{rax:X16}");
Console.Error.WriteLine($"[LOADER][INFO] RBX: 0x{rbx:X16}");
Console.Error.WriteLine($"[LOADER][INFO] RCX: 0x{rcx:X16}");
Console.Error.WriteLine($"[LOADER][INFO] RDX: 0x{rdx:X16}");
Console.Error.WriteLine($"[LOADER][INFO] RSI: 0x{rsi:X16}");
Console.Error.WriteLine($"[LOADER][INFO] RDI: 0x{rdi:X16}");
Console.Error.WriteLine($"[LOADER][INFO] RBP: 0x{rbp:X16}");
Console.Error.WriteLine($"[LOADER][INFO] RSP: 0x{rsp:X16}");
Console.Error.WriteLine($"[LOADER][INFO] R8 : 0x{r8:X16}");
Console.Error.WriteLine($"[LOADER][INFO] R9 : 0x{r9:X16}");
Console.Error.WriteLine($"[LOADER][INFO] R10: 0x{r10:X16}");
Console.Error.WriteLine($"[LOADER][INFO] R11: 0x{r11:X16}");
Console.Error.WriteLine($"[LOADER][INFO] R12: 0x{r12:X16}");
Console.Error.WriteLine($"[LOADER][INFO] R13: 0x{r13:X16}");
Console.Error.WriteLine($"[LOADER][INFO] R14: 0x{r14:X16}");
Console.Error.WriteLine($"[LOADER][INFO] R15: 0x{r15:X16}");
Console.Error.WriteLine($"[LOADER][INFO] Flags: 0x{exceptionFlags:X8}");
ulong accessType = 0;
ulong target = 0;
if (exceptionCode == 3221225477u && exceptionRecord->NumberParameters >= 2)
{
accessType = *exceptionRecord->ExceptionInformation;
target = exceptionRecord->ExceptionInformation[1];
string accessText = accessType switch
{
0uL => "read",
1uL => "write",
8uL => "execute",
_ => $"unknown({accessType})"
};
Console.Error.WriteLine("[LOADER][INFO] AV access: " + accessText);
Console.Error.WriteLine($"[LOADER][INFO] AV target: 0x{target:X16}");
if (VirtualQuery((void*)target, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) != 0)
{
Console.Error.WriteLine($"[LOADER][INFO] AV target region: base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} state=0x{mbi.State:X08} protect=0x{mbi.Protect:X08}");
}
}
try
{
Console.Error.WriteLine("[LOADER][INFO] Stack qwords (RSP..):");
for (int i = 0; i < 16; i++)
{
ulong stackAddr = rsp + (ulong)(i * 8);
ulong value = (ulong)Marshal.ReadInt64((nint)stackAddr);
Console.Error.WriteLine($"[LOADER][INFO] [rsp+0x{i * 8:X2}] @0x{stackAddr:X16} = 0x{value:X16}");
}
}
catch
{
Console.Error.WriteLine("[LOADER][WARNING] Could not read stack qwords.");
}
try
{
Console.Error.WriteLine("[LOADER][INFO] Frame chain (RBP walk):");
ulong frame = rbp;
for (int i = 0; i < 12; i++)
{
if (frame < 140733193388032L || frame > 140737488355327L)
{
break;
}
ulong next = (ulong)Marshal.ReadInt64((nint)frame);
ulong ret = (ulong)Marshal.ReadInt64((nint)(frame + 8));
string extra = TryFormatNearestRuntimeSymbol(ret, out string retSym) ? $" [{retSym}]" : string.Empty;
Console.Error.WriteLine($"[LOADER][INFO] frame#{i}: rbp=0x{frame:X16} ret=0x{ret:X16}{extra} next=0x{next:X16}");
if (next <= frame)
{
break;
}
frame = next;
}
}
catch
{
Console.Error.WriteLine("[LOADER][WARNING] Could not walk RBP frame chain.");
}
switch (exceptionCode)
{
case 3221225477u:
Console.Error.WriteLine("[LOADER][ERROR] Type: Access Violation");
Console.Error.WriteLine("[LOADER][ERROR] This usually means:");
Console.Error.WriteLine("[LOADER][ERROR] - Guest code called an unmapped import");
Console.Error.WriteLine("[LOADER][ERROR] - Guest code accessed unmapped memory");
Console.Error.WriteLine("[LOADER][ERROR] - Need to implement HLE for this NID");
try
{
byte[] code = new byte[16];
Marshal.Copy((nint)rip, code, 0, code.Length);
Console.Error.WriteLine("[LOADER][INFO] Code at RIP: " + BitConverter.ToString(code).Replace("-", " "));
if (code[0] == 100)
{
Console.Error.WriteLine("[LOADER][ERROR] Detected FS segment prefix - TLS access not patched!");
}
else if (code[0] == 101)
{
Console.Error.WriteLine("[LOADER][ERROR] Detected GS segment prefix - TLS access not patched!");
}
else if (code[0] == 197 || code[0] == 196)
{
Console.Error.WriteLine("[LOADER][INFO] Detected AVX instruction - check CPU support!");
Console.Error.WriteLine($"[LOADER][INFO] RBP: 0x{rbp:X16} (mod 16 = {rbp % 16})");
Console.Error.WriteLine($"[LOADER][INFO] RSP: 0x{rsp:X16} (mod 16 = {rsp % 16})");
}
if (rip > 16)
{
byte[] before = new byte[16];
Marshal.Copy((nint)(rip - 16), before, 0, before.Length);
Console.Error.WriteLine("[LOADER][INFO] Code before RIP: " + BitConverter.ToString(before).Replace("-", " "));
}
if (rip > 32)
{
byte[] window = new byte[64];
Marshal.Copy((nint)(rip - 32), window, 0, window.Length);
Console.Error.WriteLine("[LOADER][INFO] Code window [RIP-0x20..]: " + BitConverter.ToString(window).Replace("-", " "));
}
}
catch
{
Console.Error.WriteLine("[LOADER][ERROR] Could not read code at RIP");
}
DumpRecentImportTrace();
break;
case 2147483651u:
Console.Error.WriteLine("[LOADER][WARNING] Type: Breakpoint (int3)");
Console.Error.WriteLine("[LOADER][WARNING] Unexpected breakpoint in direct-bridge mode");
break;
case 3221225501u:
Console.Error.WriteLine("[LOADER][INFO] Type: Illegal Instruction");
break;
}
Console.Error.WriteLine("[LOADER][INFO] =========================================");
Console.Error.Flush();
return 0;
}
finally
{
_vectoredHandlerDepth--;
}
}
private unsafe void LogAccessViolationTrace(ulong exceptionAddress, EXCEPTION_RECORD* exceptionRecord)
{
ulong accessType = exceptionRecord->NumberParameters >= 1 ? (*exceptionRecord->ExceptionInformation) : 0;
ulong target = exceptionRecord->NumberParameters >= 2 ? exceptionRecord->ExceptionInformation[1] : 0;
if (_lastAvTraceRip == exceptionAddress && _lastAvTraceType == accessType && _lastAvTraceTarget == target)
{
_lastAvTraceRepeatCount++;
if (_lastAvTraceRepeatCount > 4 && _lastAvTraceRepeatCount % 128 != 0)
{
return;
}
Console.Error.WriteLine($"[LOADER][TRACE] VEH_AV repeat#{_lastAvTraceRepeatCount} at 0x{exceptionAddress:X16} type={accessType} target=0x{target:X16}");
Console.Error.Flush();
return;
}
_lastAvTraceRip = exceptionAddress;
_lastAvTraceType = accessType;
_lastAvTraceTarget = target;
_lastAvTraceRepeatCount = 1;
Console.Error.WriteLine($"[LOADER][TRACE] VEH_AV first-chance at 0x{exceptionAddress:X16} type={accessType} target=0x{target:X16}");
Console.Error.Flush();
}
private void DumpGuestInstructionStream(string name, ulong startRip, int maxInstructions)
{
if (_cpuContext == null || startRip < 0x10000 || maxInstructions <= 0)
{
return;
}
Console.Error.WriteLine($"[LOADER][INFO] {name} disasm @0x{startRip:X16}:");
ulong rip = startRip;
for (int i = 0; i < maxInstructions; i++)
{
if (!IcedDecoder.TryReadGuestBytes(_cpuContext.Memory, rip, maxLen: 15, out var bytes) ||
!IcedDecoder.TryDecode(rip, bytes, out var instruction))
{
Console.Error.WriteLine($"[LOADER][INFO] 0x{rip:X16}: <decode-failed>");
break;
}
Console.Error.WriteLine(
$"[LOADER][INFO] 0x{instruction.Rip:X16}: {instruction.Text} bytes={IcedDecoder.FormatBytes(instruction.Bytes)}");
rip += (ulong)instruction.Length;
}
}
private void DumpUnresolvedSentinelWindow(string name, ulong baseAddress, int size)
{
if (baseAddress < 0x10000 || size <= 0)
{
return;
}
ulong scanStart = baseAddress;
ulong scanEnd = baseAddress + (ulong)size;
List<ulong> hits = ScanSuspiciousResolverPointers(scanStart, scanEnd);
if (hits.Count == 0)
{
Console.Error.WriteLine($"[LOADER][INFO] {name} unresolved scan: none");
return;
}
Console.Error.WriteLine($"[LOADER][INFO] {name} unresolved scan hits: {hits.Count}");
for (int i = 0; i < hits.Count && i < 8; i++)
{
ulong slotAddress = hits[i];
if (TryReadQword(slotAddress, out var value))
{
Console.Error.WriteLine($"[LOADER][INFO] hit#{i}: slot=0x{slotAddress:X16} value=0x{value:X16}");
}
}
}
private void DumpSentinelPatternWindow(string name, ulong baseAddress, int size)
{
if (_cpuContext == null || baseAddress < 0x10000 || size <= 0)
{
return;
}
byte[] buffer = new byte[size];
if (!_cpuContext.Memory.TryRead(baseAddress, buffer))
{
Console.Error.WriteLine($"[LOADER][INFO] {name} sentinel-pattern scan: unreadable");
return;
}
List<string> hits = new();
for (int offset = 0; offset + 2 <= buffer.Length; offset++)
{
if (BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(offset, 2)) == 0xFFFE)
{
hits.Add($"+0x{offset:X}:u16");
}
if (offset + 4 <= buffer.Length &&
BinaryPrimitives.ReadUInt32LittleEndian(buffer.AsSpan(offset, 4)) == 0xFFFFFFFEu)
{
hits.Add($"+0x{offset:X}:u32");
}
if (offset + 8 <= buffer.Length &&
BinaryPrimitives.ReadUInt64LittleEndian(buffer.AsSpan(offset, 8)) == 0xFFFFFFFFFFFFFFFEuL)
{
hits.Add($"+0x{offset:X}:u64");
}
}
if (hits.Count == 0)
{
Console.Error.WriteLine($"[LOADER][INFO] {name} sentinel-pattern scan: none");
return;
}
Console.Error.WriteLine($"[LOADER][INFO] {name} sentinel-pattern hits: {string.Join(", ", hits.GetRange(0, Math.Min(hits.Count, 12)))}");
}
private void DumpReturnTargetCandidates(ulong rsp)
{
if (rsp < 0x10000)
{
return;
}
ulong start = rsp >= 0x10 ? rsp - 0x10 : rsp;
Console.Error.WriteLine($"[LOADER][INFO] Return-target candidates near RSP=0x{rsp:X16}:");
for (int offset = 0; offset <= 0x20; offset++)
{
ulong address = start + (ulong)offset;
try
{
ulong value = (ulong)Marshal.ReadInt64((nint)address);
Console.Error.WriteLine($"[LOADER][INFO] [0x{address:X16}] -> 0x{value:X16}");
}
catch
{
Console.Error.WriteLine($"[LOADER][INFO] [0x{address:X16}] -> <unreadable>");
break;
}
}
}
private void DumpObjectFieldTargets(string name, ulong objectAddress, int[] offsets, int windowSize)
{
if (objectAddress < 0x10000 || offsets.Length == 0)
{
return;
}
foreach (int offset in offsets)
{
ulong slotAddress = objectAddress + (ulong)offset;
if (!TryReadQword(slotAddress, out var target) || target < 0x10000)
{
continue;
}
Console.Error.WriteLine($"[LOADER][INFO] {name}+0x{offset:X2} target = {FormatPointerWithNearestSymbol(target)}");
DumpPointerWindow($"{name}+0x{offset:X2}", target, windowSize);
DumpUnresolvedSentinelWindow($"{name}+0x{offset:X2}", target, 0x80);
}
}
private void DumpSuspiciousGlobalSlots()
{
DumpAbsoluteSlot("callback_slot[0x80293BD08]", 0x000000080293BD08uL);
DumpAbsoluteSlot("callback_arg[0x8030FBBE8]", 0x00000008030FBBE8uL);
DumpAbsoluteSlot("tsc_freq_global[0x8030FD590]", 0x00000008030FD590uL);
DumpAbsoluteSlot("tsc_base_global[0x8030FD598]", 0x00000008030FD598uL);
DumpAbsoluteSlot("plt_got[0x8028F6100]", 0x00000008028F6100uL);
DumpAbsoluteSlot("plt_got[0x8028F6158]", 0x00000008028F6158uL);
DumpAbsoluteSlot("plt_got[0x8028F6160]", 0x00000008028F6160uL);
DumpAbsoluteSlot("plt_got[0x8028F64C0]", 0x00000008028F64C0uL);
DumpAbsoluteSlot("plt_got[0x8028F64C8]", 0x00000008028F64C8uL);
DumpAbsoluteSlot("plt_got[0x8028F6590]", 0x00000008028F6590uL);
DumpAbsoluteSlot("plt_got[0x8028F6708]", 0x00000008028F6708uL);
DumpUnresolvedSentinelWindow("PLT-GOT", 0x00000008028F6100uL, 0x700);
}
private void DumpAbsoluteSlot(string name, ulong slotAddress)
{
if (!TryReadQword(slotAddress, out var value))
{
Console.Error.WriteLine($"[LOADER][INFO] {name} @0x{slotAddress:X16} = <unreadable>");
return;
}
Console.Error.WriteLine($"[LOADER][INFO] {name} @0x{slotAddress:X16} = {FormatPointerWithNearestSymbol(value)}");
}
private void DumpPointerWindow(string name, ulong baseAddress, int size)
{
if (baseAddress < 0x10000 || size <= 0)
{
return;
}
Console.Error.WriteLine($"[LOADER][INFO] {name} window @0x{baseAddress:X16}:");
for (int offset = 0; offset < size; offset += 8)
{
ulong slotAddress = baseAddress + (ulong)offset;
if (!TryReadQword(slotAddress, out var value))
{
Console.Error.WriteLine($"[LOADER][INFO] +0x{offset:X2}: <unreadable>");
break;
}
Console.Error.WriteLine($"[LOADER][INFO] +0x{offset:X2}: {FormatPointerWithNearestSymbol(value)}");
}
}
private unsafe bool TryReadQword(ulong address, out ulong value)
{
value = 0;
if (address < 0x10000)
{
return false;
}
if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
{
return false;
}
ulong regionEnd = mbi.BaseAddress + mbi.RegionSize;
if (mbi.State != MEM_COMMIT || !IsReadableProtection(mbi.Protect) || regionEnd <= address || address > regionEnd - 8)
{
return false;
}
try
{
value = (ulong)Marshal.ReadInt64((nint)address);
return true;
}
catch
{
value = 0;
return false;
}
}
private string FormatPointerWithNearestSymbol(ulong value)
{
string text = $"0x{value:X16}";
if (TryFormatNearestRuntimeSymbol(value, out string symbol))
{
text += $" [{symbol}]";
}
return text;
}
private void InitializeRuntimeSymbolIndex(IReadOnlyDictionary<string, ulong> runtimeSymbols)
{
_runtimeSymbolsByName.Clear();
if (runtimeSymbols.Count == 0)
{
_runtimeSymbolsByAddress = Array.Empty<KeyValuePair<string, ulong>>();
return;
}
List<KeyValuePair<string, ulong>> list = new(runtimeSymbols.Count);
foreach (KeyValuePair<string, ulong> runtimeSymbol in runtimeSymbols)
{
if (runtimeSymbol.Value != 0L && !string.IsNullOrWhiteSpace(runtimeSymbol.Key))
{
list.Add(runtimeSymbol);
_runtimeSymbolsByName[runtimeSymbol.Key] = runtimeSymbol.Value;
}
}
list.Sort((a, b) => a.Value.CompareTo(b.Value));
_runtimeSymbolsByAddress = list.ToArray();
}
private bool TryFormatNearestRuntimeSymbol(ulong address, out string text)
{
text = string.Empty;
KeyValuePair<string, ulong>[] runtimeSymbolsByAddress = _runtimeSymbolsByAddress;
if (runtimeSymbolsByAddress.Length == 0)
{
return false;
}
int low = 0;
int high = runtimeSymbolsByAddress.Length - 1;
int best = -1;
while (low <= high)
{
int mid = low + ((high - low) >> 1);
ulong value = runtimeSymbolsByAddress[mid].Value;
if (value <= address)
{
best = mid;
low = mid + 1;
}
else
{
high = mid - 1;
}
}
if (best < 0)
{
return false;
}
KeyValuePair<string, ulong> symbol = runtimeSymbolsByAddress[best];
ulong delta = address - symbol.Value;
text = delta == 0
? $"{symbol.Key} (0x{symbol.Value:X16})"
: $"{symbol.Key}+0x{delta:X} (0x{symbol.Value:X16})";
return true;
}
private unsafe static bool TryHandleLazyCommittedPage(EXCEPTION_RECORD* exceptionRecord)
{
if (exceptionRecord->NumberParameters < 2)
{
return false;
}
ulong accessType = *exceptionRecord->ExceptionInformation;
ulong faultAddress = exceptionRecord->ExceptionInformation[1];
if (accessType == 8 && faultAddress < 4294967296L)
{
return false;
}
if (faultAddress < 65536 || faultAddress >= 140737488355328L)
{
return false;
}
if (VirtualQuery((void*)faultAddress, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
{
return false;
}
ulong pageBase = faultAddress & 0xFFFFFFFFFFFFF000uL;
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.AllocationProtect);
Console.Error.WriteLine($"[LOADER][TRACE] lazy-query: fault=0x{faultAddress:X16} state=0x{mbi.State:X08} base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} alloc=0x{mbi.AllocationProtect:X08} prot=0x{mbi.Protect:X08}");
bool committed = false;
ulong committedBase = 0;
ulong committedSize = 0;
if (mbi.State == 65536)
{
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL;
if (TryReserveThenCommit(largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
{
committed = true;
committedBase = largeBase;
committedSize = 2097152uL;
}
else
{
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
if (TryReserveThenCommit(region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
{
committed = true;
committedBase = region64kBase;
committedSize = 65536uL;
}
else if (TryReserveThenCommit(pageBase, 4096uL, pageBase, 4096uL, commitProtect))
{
committed = true;
committedBase = pageBase;
committedSize = 4096uL;
}
}
if (!committed)
{
return false;
}
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
Console.Error.WriteLine($"[LOADER][TRACE] lazy-reserve-commit: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
return true;
}
if (mbi.State != 8192)
{
return false;
}
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL;
if (TryCommitRange(largeCommitBase, 2097152uL, commitProtect))
{
committed = true;
committedBase = largeCommitBase;
committedSize = 2097152uL;
}
else
{
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
if (TryCommitRange(region64kBase, 65536uL, commitProtect))
{
committed = true;
committedBase = region64kBase;
committedSize = 65536uL;
}
else if (TryCommitRange(pageBase, 8192uL, commitProtect))
{
committed = true;
committedBase = pageBase;
committedSize = 8192uL;
}
else if (TryCommitRange(pageBase, 4096uL, commitProtect))
{
committed = true;
committedBase = pageBase;
committedSize = 4096uL;
}
}
if (!committed)
{
return false;
}
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
return true;
static unsafe bool TryCommitRange(ulong baseAddress, ulong length, uint protection)
{
if (length == 0)
{
return false;
}
return VirtualAlloc((void*)baseAddress, (nuint)length, 4096u, protection) != null;
}
static unsafe bool TryReserveRange(ulong baseAddress, ulong length)
{
if (length == 0)
{
return false;
}
return VirtualAlloc((void*)baseAddress, (nuint)length, 8192u, 4u) != null;
}
static bool TryReserveThenCommit(ulong reserveAddress, ulong reserveSize, ulong commitAddress, ulong commitSize, uint protection)
{
if (!TryReserveRange(reserveAddress, reserveSize))
{
return false;
}
return TryCommitRange(commitAddress, commitSize, protection);
}
}
private static uint ResolveLazyCommitProtection(ulong accessType, uint allocationProtect)
{
if (accessType == 8 || (allocationProtect & 0xF0) != 0)
{
return 64u;
}
return 4u;
}
}
@@ -0,0 +1,760 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.Core.Cpu;
using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu.Native;
public sealed partial class DirectExecutionBackend
{
private static ulong ImportDispatchGatewayManaged(nint backendHandle, int importIndex, nint argPackPtr)
{
try
{
if (!(GCHandle.FromIntPtr(backendHandle).Target is DirectExecutionBackend directExecutionBackend))
{
Console.Error.WriteLine(
$"[LOADER][ERROR] ImportDispatchGatewayManaged: invalid backend handle 0x{backendHandle:X16}");
return 18446744071562199042uL;
}
return directExecutionBackend.DispatchImport(importIndex, argPackPtr);
}
catch (Exception ex)
{
Console.Error.WriteLine(
$"[LOADER][ERROR] ImportDispatchGatewayManaged exception: {ex.GetType().Name}: {ex.Message}");
return 18446744071562199298uL;
}
}
private unsafe static int RawVectoredHandlerManaged(void* exceptionInfo)
{
return TryRecoverUnresolvedSentinel(exceptionInfo);
}
private unsafe static int RawUnhandledFilterManaged(void* exceptionInfo)
{
return TryRecoverUnresolvedSentinel(exceptionInfo);
}
private unsafe static int TryRecoverUnresolvedSentinel(void* exceptionInfo)
{
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
if (exceptionRecord->ExceptionCode != 3221225477u)
{
return 0;
}
void* contextRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord;
ulong value = ReadCtxU64(contextRecord, 248);
ulong value2 = (ulong)exceptionRecord->ExceptionAddress;
if (!IsUnresolvedSentinel(value) && !IsUnresolvedSentinel(value2))
{
return 0;
}
ulong rsp = ReadCtxU64(contextRecord, 152);
WriteCtxU64(contextRecord, 120, 0uL);
if (TryGetPlausibleReturnFromStack(rsp, out var returnRip, out var nextRsp))
{
WriteCtxU64(contextRecord, 152, nextRsp);
WriteCtxU64(contextRecord, 248, returnRip);
Interlocked.Increment(ref _rawSentinelRecoveries);
return -1;
}
return 0;
}
private unsafe ulong DispatchImport(int importIndex, nint argPackPtr)
{
long num = Interlocked.Increment(ref _importDispatchCount);
MarkExecutionProgress();
if (_cpuContext == null)
{
LastError = "Import dispatch called without active CPU context";
return 18446744071562199298uL;
}
if ((uint)importIndex >= (uint)_importEntries.Length)
{
LastError = $"Import dispatch index out of range: {importIndex}";
return 18446744071562199042uL;
}
ImportStubEntry importStubEntry = _importEntries[importIndex];
int num2 = Volatile.Read(in _rawSentinelRecoveries);
if (num2 != _lastReportedRawSentinelRecoveries)
{
Console.Error.WriteLine($"[LOADER][TRACE] Raw sentinel recoveries: {num2} (last import index={importIndex})");
_lastReportedRawSentinelRecoveries = num2;
}
_cpuContext.Rip = importStubEntry.Address;
_cpuContext[CpuRegister.Rdi] = *(ulong*)argPackPtr;
_cpuContext[CpuRegister.Rsi] = *(ulong*)(argPackPtr + 8);
_cpuContext[CpuRegister.Rdx] = *(ulong*)(argPackPtr + 16);
_cpuContext[CpuRegister.Rcx] = *(ulong*)(argPackPtr + 24);
_cpuContext[CpuRegister.R8] = *(ulong*)(argPackPtr + 32);
_cpuContext[CpuRegister.R9] = *(ulong*)(argPackPtr + 40);
_cpuContext[CpuRegister.Rbx] = *(ulong*)(argPackPtr + 48);
_cpuContext[CpuRegister.Rbp] = *(ulong*)(argPackPtr + 56);
_cpuContext[CpuRegister.R12] = *(ulong*)(argPackPtr + 64);
_cpuContext[CpuRegister.R13] = *(ulong*)(argPackPtr + 72);
_cpuContext[CpuRegister.R14] = *(ulong*)(argPackPtr + 80);
_cpuContext[CpuRegister.R15] = *(ulong*)(argPackPtr + 88);
_cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL;
ulong value = _cpuContext[CpuRegister.Rdi];
ulong value2 = _cpuContext[CpuRegister.Rsi];
ulong num3 = _cpuContext[CpuRegister.Rdx];
ulong num4 = _cpuContext[CpuRegister.Rcx];
ulong num5 = _cpuContext[CpuRegister.R8];
ulong num6 = _cpuContext[CpuRegister.R9];
ulong value3 = _cpuContext[CpuRegister.Rbx];
ulong value4 = _cpuContext[CpuRegister.Rbp];
ulong value5 = _cpuContext[CpuRegister.R12];
ulong value6 = _cpuContext[CpuRegister.R13];
ulong value7 = _cpuContext[CpuRegister.R14];
ulong value8 = _cpuContext[CpuRegister.R15];
ulong num7 = *(ulong*)(argPackPtr + 96);
if (!IsLikelyReturnAddress(num7))
{
for (int i = 1; i <= 4; i++)
{
ulong num8 = *(ulong*)(argPackPtr + 96 + i * 8);
if (IsLikelyReturnAddress(num8))
{
*(ulong*)(argPackPtr + 96) = num8;
num7 = num8;
Console.Error.WriteLine($"[LOADER][WARNING] Import#{num}: corrected suspicious return RIP using stack slot +0x{i * 8:X} -> 0x{num7:X16}");
break;
}
}
}
TrackDistinctImportNid(importStubEntry.Nid);
TrackStrlenPrelude(importStubEntry.Nid, num, num7);
bool logBootstrap = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_BOOTSTRAP"), "1", StringComparison.Ordinal);
if (logBootstrap && string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
{
string symbolText = "<unreadable>";
if (TryReadAsciiZ(value2, 256, out var sym))
{
symbolText = sym;
}
Console.Error.WriteLine(
$"[LOADER][TRACE] bootstrap_call#{num}: op=0x{value:X16} sym_ptr=0x{value2:X16} sym='{symbolText}' out_ptr=0x{num3:X16} ret=0x{num7:X16}");
}
if (!_forcedGuestExit && ShouldForceGuestExitOnImportLoop(importStubEntry.Nid, num7, num, value, value2) && TryForceGuestExitToHostStub(argPackPtr, num, num7, importStubEntry.Nid))
{
_cpuContext[CpuRegister.Rax] = 1uL;
return 1uL;
}
bool flag0 = ShouldSuppressStrlenTrace(importStubEntry.Nid);
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))
{
if (_moduleManager.TryGetExport(importStubEntry.Nid, out ExportedFunction export))
{
Console.Error.WriteLine($"[LOADER][TRACE] Import#{num}: {export.LibraryName}:{export.Name} ({importStubEntry.Nid})");
}
else
{
Console.Error.WriteLine($"[LOADER][TRACE] Import#{num}: {importStubEntry.Nid}");
}
}
if (!flag0)
{
RecordRecentImportTrace($"#{num} nid={importStubEntry.Nid} ret=0x{num7:X16} rdi=0x{_cpuContext[CpuRegister.Rdi]:X16} rsi=0x{_cpuContext[CpuRegister.Rsi]:X16} rdx=0x{_cpuContext[CpuRegister.Rdx]:X16}");
}
if (importStubEntry.Nid == "8zTFvBIAIN8" && num <= 256)
{
Console.Error.WriteLine($"[LOADER][TRACE] memset#{num}: dst=0x{_cpuContext[CpuRegister.Rdi]:X16} val=0x{_cpuContext[CpuRegister.Rsi] & 0xFF:X2} len=0x{_cpuContext[CpuRegister.Rdx]:X16} ret=0x{num7:X16}");
}
if (importStubEntry.Nid == "tsvEmnenz48" && num <= 64)
{
Console.Error.WriteLine($"[LOADER][TRACE] __cxa_atexit#{num}: func=0x{_cpuContext[CpuRegister.Rdi]:X16} arg=0x{_cpuContext[CpuRegister.Rsi]:X16} dso=0x{_cpuContext[CpuRegister.Rdx]:X16} ret=0x{num7:X16}");
}
if (importStubEntry.Nid == "bzQExy189ZI" || importStubEntry.Nid == "8G2LB+A3rzg")
{
Console.Error.WriteLine($"[LOADER][TRACE] {importStubEntry.Nid}#{num}: rdi=0x{_cpuContext[CpuRegister.Rdi]:X16} rsi=0x{_cpuContext[CpuRegister.Rsi]:X16} rdx=0x{_cpuContext[CpuRegister.Rdx]:X16} ret=0x{num7:X16}");
}
if (flag || flag2 || flag3)
{
Console.Error.WriteLine($"[LOADER][TRACE] ImportCtx#{num}: nid={importStubEntry.Nid} ret=0x{num7:X16} rdi=0x{_cpuContext[CpuRegister.Rdi]:X16} rsi=0x{_cpuContext[CpuRegister.Rsi]:X16} rdx=0x{_cpuContext[CpuRegister.Rdx]:X16} rcx=0x{_cpuContext[CpuRegister.Rcx]:X16}");
Console.Error.WriteLine($"[LOADER][TRACE] ImportNV#{num}: rbx=0x{value3:X16} rbp=0x{value4:X16} r12=0x{value5:X16} r13=0x{value6:X16} r14=0x{value7:X16} r15=0x{value8:X16}");
if (flag3)
{
ulong num9 = _cpuContext[CpuRegister.Rsp];
if (_cpuContext.TryReadUInt64(num9, out var value9) && _cpuContext.TryReadUInt64(num9 + 8, out var value10) && _cpuContext.TryReadUInt64(num9 + 16, out var value11) && _cpuContext.TryReadUInt64(num9 + 24, out var value12) && _cpuContext.TryReadUInt64(num9 + 32, out var value13) && _cpuContext.TryReadUInt64(num9 + 40, out var value14) && _cpuContext.TryReadUInt64(num9 + 48, out var value15) && _cpuContext.TryReadUInt64(num9 + 56, out var value16) && _cpuContext.TryReadUInt64(num9 + 64, out var value17))
{
Console.Error.WriteLine($"[LOADER][TRACE] ImportStackHead#{num}: rsp=0x{num9:X16} [0]=0x{value9:X16} [20]=0x{value13:X16} [40]=0x{value17:X16}");
Console.Error.WriteLine($"[LOADER][TRACE] ImportStack#{num}: rsp=0x{num9:X16} [0]=0x{value9:X16} [8]=0x{value10:X16} [10]=0x{value11:X16} [18]=0x{value12:X16} [20]=0x{value13:X16} [28]=0x{value14:X16} [30]=0x{value15:X16} [38]=0x{value16:X16} [40]=0x{value17:X16}");
}
}
if (flag3)
{
Console.Error.Flush();
}
}
if (importStubEntry.Nid == "Ou3iL1abvng")
{
try
{
byte[] array = new byte[64];
Marshal.Copy((nint)(num7 - 32), array, 0, array.Length);
Console.Error.WriteLine($"[LOADER][TRACE] __stack_chk_fail return-site @0x{num7:X16}: {BitConverter.ToString(array).Replace("-", " ")}");
}
catch
{
}
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();
}
ProbeReturnRip(0x0000000800EA020Eul, 999001);
ProbeReturnRip(0x0000000800EA0213ul, 999002);
ProbeReturnRip(0x0000000800EA040Aul, 999003);
try
{
OrbisGen2Result orbisGen2Result;
if (string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
{
orbisGen2Result = DispatchBootstrapBridge();
}
else if (string.Equals(importStubEntry.Nid, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal))
{
orbisGen2Result = DispatchKernelDynlibDlsym();
}
else
{
orbisGen2Result = _moduleManager.Dispatch(importStubEntry.Nid, _cpuContext);
}
switch (orbisGen2Result)
{
case OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND:
LastError = "Missing HLE export for NID: " + importStubEntry.Nid;
Console.Error.WriteLine($"[LOADER][WARN] Import#{num} unresolved: nid={importStubEntry.Nid} ret=0x{num7:X16}");
if (importStubEntry.Nid == "L-Q3LEjIbgA")
{
string value18 = string.Join(" ", importStubEntry.Nid.Select(delegate (char c)
{
int num10 = c;
return num10.ToString("X2");
}));
Console.Error.WriteLine($"[LOADER][WARN] map_direct nid raw len={importStubEntry.Nid.Length} chars=[{value18}]");
Delegate function;
bool value19 = _moduleManager.TryGetFunction(importStubEntry.Nid, out function);
ExportedFunction export2;
bool value20 = _moduleManager.TryGetExport(importStubEntry.Nid, out export2);
Console.Error.WriteLine($"[LOADER][WARN] map_direct lookup with import nid: function={value19}, export={value20}");
Console.Error.WriteLine(_moduleManager.TryGetExport("L-Q3LEjIbgA", out ExportedFunction export3) ? $"[LOADER][WARN] Canonical map_direct exists as {export3.LibraryName}:{export3.Name}, target={export3.Target}, ctx_target={_cpuContext.TargetGeneration}" : "[LOADER][WARN] Canonical map_direct export lookup also missing");
}
break;
default:
Console.Error.WriteLine($"[LOADER][WARN] Import#{num} result: {orbisGen2Result} ({importStubEntry.Nid})");
break;
case OrbisGen2Result.ORBIS_GEN2_OK:
break;
}
_cpuContext[CpuRegister.Rbx] = value3;
_cpuContext[CpuRegister.Rbp] = value4;
_cpuContext[CpuRegister.R12] = value5;
_cpuContext[CpuRegister.R13] = value6;
_cpuContext[CpuRegister.R14] = value7;
_cpuContext[CpuRegister.R15] = value8;
_cpuContext[CpuRegister.Rdi] = value;
_cpuContext[CpuRegister.Rsi] = value2;
if (flag || flag2 || flag3)
{
Console.Error.WriteLine($"[LOADER][TRACE] ImportRet#{num}: nid={importStubEntry.Nid} result={orbisGen2Result} rax=0x{_cpuContext[CpuRegister.Rax]:X16}");
if (flag3)
{
Console.Error.Flush();
}
}
return _cpuContext[CpuRegister.Rax];
}
catch (Exception ex)
{
LastError = $"HLE dispatch error for {importStubEntry.Nid}: {ex.GetType().Name}: {ex.Message}";
_cpuContext[CpuRegister.Rax] = 18446744071562199298uL;
return 18446744071562199298uL;
}
}
private unsafe bool TryForceGuestExitToHostStub(nint argPackPtr, long dispatchIndex, ulong returnRip, string nid)
{
ulong num = _entryReturnSentinelRip;
if (num < 65536)
{
return false;
}
try
{
*(ulong*)(argPackPtr + 96) = num;
}
catch
{
return false;
}
_forcedGuestExit = true;
LastError = $"Detected repeating import loop at import#{dispatchIndex} ({nid}) and forced guest exit.";
Console.Error.WriteLine($"[LOADER][ERROR] Import-loop guard fired at import#{dispatchIndex}: nid={nid} ret=0x{returnRip:X16} -> host_exit=0x{num:X16}");
DumpRecentImportTrace();
return true;
}
private bool ShouldForceGuestExitOnImportLoop(string nid, ulong returnRip, long dispatchIndex, ulong arg0, ulong arg1)
{
if (dispatchIndex < 1200)
{
return false;
}
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD"), "1", StringComparison.Ordinal))
{
return false;
}
RecordImportLoopSignature(BuildImportLoopSignature(nid, returnRip, arg0, arg1));
if (!HasRepeatingImportLoopPattern())
{
if (_importLoopPatternHits > 0)
{
_importLoopPatternHits--;
}
return false;
}
_importLoopPatternHits++;
return _importLoopPatternHits >= 6;
}
private ulong BuildImportLoopSignature(string nid, ulong returnRip, ulong arg0, ulong arg1)
{
if (!_importNidHashCache.TryGetValue(nid, out var value))
{
value = StableHash64(nid);
_importNidHashCache[nid] = value;
}
ulong num = returnRip >> 2;
ulong num2 = ((arg0 >> 4) * 11400714819323198485uL) ^ ((arg1 >> 4) * 14029467366897019727uL);
return num ^ value * 11400714819323198485uL ^ num2;
}
private void RecordImportLoopSignature(ulong signature)
{
_importLoopSignatures[_importLoopSignatureWriteIndex] = signature;
_importLoopSignatureWriteIndex = (_importLoopSignatureWriteIndex + 1) % _importLoopSignatures.Length;
if (_importLoopSignatureCount < _importLoopSignatures.Length)
{
_importLoopSignatureCount++;
}
}
private bool HasRepeatingImportLoopPattern()
{
int num = _importLoopSignatureCount;
if (num < 96)
{
return false;
}
int num2 = Math.Min(48, num / 4);
for (int i = 6; i <= num2; i++)
{
if (HasRepeatingImportLoopPattern(i, 4))
{
return true;
}
}
return false;
}
private bool HasRepeatingImportLoopPattern(int period, int repeats)
{
int num = period * repeats;
if (period <= 0 || repeats < 2 || _importLoopSignatureCount < num)
{
return false;
}
for (int i = 0; i < period; i++)
{
ulong importLoopSignatureFromTail = GetImportLoopSignatureFromTail(i);
for (int j = 1; j < repeats; j++)
{
if (GetImportLoopSignatureFromTail(i + j * period) != importLoopSignatureFromTail)
{
return false;
}
}
}
return true;
}
private ulong GetImportLoopSignatureFromTail(int offset)
{
int num = _importLoopSignatureWriteIndex - 1 - offset;
while (num < 0)
{
num += _importLoopSignatures.Length;
}
return _importLoopSignatures[num % _importLoopSignatures.Length];
}
private bool ShouldSuppressStrlenTrace(string nid)
{
return string.Equals(nid, "j4ViWNHEgww", StringComparison.Ordinal) && !_logStrlenImports;
}
private void TrackDistinctImportNid(string nid)
{
if (string.IsNullOrWhiteSpace(nid) || string.Equals(_lastDistinctImportNid, nid, StringComparison.Ordinal))
{
return;
}
_lastDistinctImportNid = nid;
_distinctImportNidHistory[_distinctImportNidHistoryWriteIndex] = nid;
_distinctImportNidHistoryWriteIndex = (_distinctImportNidHistoryWriteIndex + 1) % _distinctImportNidHistory.Length;
if (_distinctImportNidHistoryCount < _distinctImportNidHistory.Length)
{
_distinctImportNidHistoryCount++;
}
}
private void TrackStrlenPrelude(string nid, long dispatchIndex, ulong returnRip)
{
if (!string.Equals(nid, "j4ViWNHEgww", StringComparison.Ordinal))
{
_consecutiveStrlenImports = 0;
_strlenPreludeLogged = false;
return;
}
_consecutiveStrlenImports++;
if (_strlenPreludeLogged || _consecutiveStrlenImports < 24)
{
return;
}
_strlenPreludeLogged = true;
List<string> list = GetRecentDistinctImportPrelude(maxCount: 5, skipNid: "j4ViWNHEgww");
if (list.Count == 0)
{
Console.Error.WriteLine($"[LOADER][WARNING] Import#{dispatchIndex}: detected strlen burst (count={_consecutiveStrlenImports}) ret=0x{returnRip:X16}; no prelude NIDs recorded.");
return;
}
Console.Error.WriteLine($"[LOADER][WARNING] Import#{dispatchIndex}: detected strlen burst (count={_consecutiveStrlenImports}) ret=0x{returnRip:X16}; last5_nids={string.Join(" -> ", list)}");
}
private List<string> GetRecentDistinctImportPrelude(int maxCount, string skipNid)
{
List<string> list = new List<string>(maxCount);
if (maxCount <= 0 || _distinctImportNidHistoryCount == 0)
{
return list;
}
HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal);
for (int i = 0; i < _distinctImportNidHistoryCount && list.Count < maxCount; i++)
{
int num = _distinctImportNidHistoryWriteIndex - 1 - i;
while (num < 0)
{
num += _distinctImportNidHistory.Length;
}
string text = _distinctImportNidHistory[num % _distinctImportNidHistory.Length];
if (string.IsNullOrWhiteSpace(text) || string.Equals(text, skipNid, StringComparison.Ordinal) || !hashSet.Add(text))
{
continue;
}
if (_moduleManager.TryGetExport(text, out ExportedFunction export))
{
list.Add($"{export.LibraryName}:{export.Name}({text})");
}
else
{
list.Add(text);
}
}
list.Reverse();
return list;
}
private static ulong StableHash64(string text)
{
ulong num = 14695981039346656037uL;
for (int i = 0; i < text.Length; i++)
{
num ^= text[i];
num *= 1099511628211uL;
}
return num;
}
private OrbisGen2Result DispatchKernelDynlibDlsym()
{
if (_cpuContext == null)
{
return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
ulong symbolNameAddress = _cpuContext[CpuRegister.Rsi];
ulong outputAddress = _cpuContext[CpuRegister.Rdx];
if (!TryReadAsciiZ(symbolNameAddress, 512, out var symbolName))
{
_cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
return OrbisGen2Result.ORBIS_GEN2_OK;
}
if (!TryResolveRuntimeSymbolAddress(symbolName, out var resolvedAddress))
{
_cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
return OrbisGen2Result.ORBIS_GEN2_OK;
}
if (outputAddress == 0L || !TryWriteUInt64Compat(outputAddress, resolvedAddress))
{
_cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
return OrbisGen2Result.ORBIS_GEN2_OK;
}
_cpuContext[CpuRegister.Rax] = 0uL;
return OrbisGen2Result.ORBIS_GEN2_OK;
}
private OrbisGen2Result DispatchBootstrapBridge()
{
if (_cpuContext == null)
{
return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
ulong bridgeHandle = _cpuContext[CpuRegister.Rdi];
ulong symbolNameAddress = _cpuContext[CpuRegister.Rsi];
ulong outputAddress = _cpuContext[CpuRegister.Rdx];
_ = TryReadAsciiZ(symbolNameAddress, 512, out var symbolName);
OrbisGen2Result result = DispatchKernelDynlibDlsym();
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
{
return result;
}
bool logBootstrap = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_BOOTSTRAP"), "1", StringComparison.Ordinal);
if (logBootstrap)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] bootstrap_dispatch: handle=0x{bridgeHandle:X16} symbol='{symbolName}' out=0x{outputAddress:X16} rax=0x{_cpuContext[CpuRegister.Rax]:X16}");
}
if (_cpuContext[CpuRegister.Rax] == 0uL)
{
return OrbisGen2Result.ORBIS_GEN2_OK;
}
Console.Error.WriteLine(
$"[LOADER][WARN] bootstrap_bridge unresolved: handle=0x{bridgeHandle:X} symbol='{symbolName}' out=0x{outputAddress:X16}");
return OrbisGen2Result.ORBIS_GEN2_OK;
}
private bool TryResolveRuntimeSymbolAddress(string symbolName, out ulong address)
{
address = 0uL;
if (string.IsNullOrWhiteSpace(symbolName))
{
return false;
}
if (_runtimeSymbolsByName.TryGetValue(symbolName, out var value) && IsRuntimeSymbolAddressUsable(value))
{
address = value;
return true;
}
if (symbolName.StartsWith("_", StringComparison.Ordinal) && _runtimeSymbolsByName.TryGetValue(symbolName[1..], out value) && IsRuntimeSymbolAddressUsable(value))
{
address = value;
return true;
}
if (_runtimeSymbolsByName.TryGetValue("_" + symbolName, out value) && IsRuntimeSymbolAddressUsable(value))
{
address = value;
return true;
}
return false;
}
private static bool IsRuntimeSymbolAddressUsable(ulong value)
{
return value != 0 && !IsUnresolvedSentinel(value);
}
private bool TryReadAsciiZ(ulong address, int maxLength, out string value)
{
value = string.Empty;
if (_cpuContext == null || address == 0L || maxLength <= 0)
{
return false;
}
List<byte> list = new List<byte>(Math.Min(maxLength, 256));
Span<byte> destination = stackalloc byte[1];
for (int i = 0; i < maxLength; i++)
{
if (!TryReadByteCompat(address + (ulong)i, destination))
{
return false;
}
if (destination[0] == 0)
{
value = System.Text.Encoding.ASCII.GetString(list.ToArray());
return true;
}
list.Add(destination[0]);
}
value = System.Text.Encoding.ASCII.GetString(list.ToArray());
return true;
}
private bool TryReadByteCompat(ulong address, Span<byte> destination)
{
if (_cpuContext == null || destination.Length == 0)
{
return false;
}
if (_cpuContext.Memory.TryRead(address, destination))
{
return true;
}
try
{
destination[0] = Marshal.ReadByte((nint)address);
return true;
}
catch
{
return false;
}
}
private bool TryWriteUInt64Compat(ulong address, ulong value)
{
if (_cpuContext == null || address == 0L)
{
return false;
}
if (_cpuContext.TryWriteUInt64(address, value))
{
return true;
}
try
{
Marshal.WriteInt64((nint)address, unchecked((long)value));
return true;
}
catch
{
return false;
}
}
private void TryBypassStackChkFailTrap(long dispatchIndex, ulong returnRip)
{
if (_cpuContext == null || returnRip < 32)
{
return;
}
try
{
byte[] array = new byte[19];
ulong num = returnRip - 23;
Marshal.Copy((nint)num, array, 0, array.Length);
if (array[0] != 117 || array[1] != 16 || array[2] != 72 || array[3] != 137 || array[4] != 216 || array[5] != 72 || array[6] != 131 || array[7] != 196 || array[9] != 91 || array[10] != 65 || array[11] != 92 || array[12] != 65 || array[13] != 94 || array[14] != 65 || array[15] != 95 || array[16] != 93 || array[17] != 195 || array[18] != 232)
{
return;
}
ulong value = returnRip - 21;
ulong address = _cpuContext[CpuRegister.Rsp];
if (_cpuContext.TryWriteUInt64(address, value))
{
if (_stackChkBypassSites.Add(num) && TryPatchStackChkFailBranch(num))
{
Console.Error.WriteLine($"[LOADER][WARNING] Import#{dispatchIndex}: patched stack_chk_fail tail branch at 0x{num:X16} -> NOP NOP");
}
Console.Error.WriteLine($"[LOADER][WARNING] Import#{dispatchIndex}: redirected __stack_chk_fail return to epilogue 0x{value:X16}");
}
}
catch
{
}
}
private unsafe static bool TryPatchStackChkFailBranch(ulong branchAddress)
{
uint flNewProtect = default(uint);
if (!VirtualProtect((void*)branchAddress, 2u, 64u, &flNewProtect))
{
return false;
}
try
{
if (Marshal.ReadByte((nint)branchAddress) != 117)
{
return false;
}
Marshal.WriteByte((nint)branchAddress, 144);
Marshal.WriteByte((nint)(branchAddress + 1), 144);
FlushInstructionCache(GetCurrentProcess(), (void*)branchAddress, 2u);
return true;
}
catch
{
return false;
}
finally
{
VirtualProtect((void*)branchAddress, 2u, flNewProtect, &flNewProtect);
}
}
private unsafe void TryPatchEa020eLookupCall(long dispatchIndex, ulong returnRip)
{
if (_patchedEa020eLookupCall || returnRip != 0x0000000800EA01A6uL)
{
return;
}
const ulong num = 0x0000000800EA020EuL;
nint num2 = unchecked((nint)num);
uint flNewProtect = default(uint);
try
{
if (Marshal.ReadByte(num2) != 232 || !VirtualProtect((void*)num, 5u, 64u, &flNewProtect))
{
return;
}
for (int i = 0; i < 5; i++)
{
Marshal.WriteByte(num2 + i, 144);
}
FlushInstructionCache(GetCurrentProcess(), (void*)num, 5u);
_patchedEa020eLookupCall = true;
Console.Error.WriteLine($"[LOADER][WARNING] Import#{dispatchIndex}: patched hash-lookup call at 0x{num:X16} -> NOP*5");
}
catch
{
}
finally
{
if (flNewProtect != 0)
{
VirtualProtect((void*)num, 5u, flNewProtect, &flNewProtect);
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,22 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu.Native;
public interface INativeCpuBackend
{
string BackendName { get; }
string? LastError { get; }
bool TryExecute(
CpuContext context,
ulong entryPoint,
Generation generation,
IReadOnlyDictionary<ulong, string> importStubs,
IReadOnlyDictionary<string, ulong> runtimeSymbols,
CpuExecutionOptions executionOptions,
out OrbisGen2Result result);
}
+275
View File
@@ -0,0 +1,275 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
namespace SharpEmu.Core.Cpu.Native;
public static unsafe class JitStubs
{
public const int XsaveBufferSize = 2688;
public const ulong XsaveChkGuard = 0xDeadBeef5533CCAAu;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct JmpWithIndex
{
private fixed byte _code[16];
public static ReadOnlySpan<byte> Template => new byte[]
{
0x68, 0x00, 0x00, 0x00, 0x00, // push <index>
0xE9, 0x00, 0x00, 0x00, 0x00, // jmp <handler>
0x90, 0x90, 0x90, 0x90, 0x90, 0x90 // nop padding
};
public static int Size => 16;
public void Initialize()
{
fixed (byte* code = _code)
{
Template.CopyTo(new Span<byte>(code, 16));
}
}
public void SetIndex(uint index)
{
fixed (byte* code = _code)
{
*(uint*)(code + 1) = index;
}
}
public void SetHandler(void* handler)
{
fixed (byte* code = _code)
{
var funcAddr = (long)handler;
var ripAddr = (long)(code + 10); // After push + jmp
var offset64 = funcAddr - ripAddr;
var offset32 = (uint)(ulong)offset64;
*(uint*)(code + 6) = offset32;
}
}
public byte* GetCodePointer()
{
fixed (byte* code = _code)
{
return code;
}
}
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct Call9
{
private fixed byte _code[9];
public static ReadOnlySpan<byte> Template => new byte[]
{
0xE8, 0x00, 0x00, 0x00, 0x00, // call func
0x48, 0x89, 0xC0, // mov rax,rax
0x90 // nop
};
public static int Size => 9;
public void Initialize()
{
fixed (byte* code = _code)
{
Template.CopyTo(new Span<byte>(code, 9));
}
}
public void SetFunc(void* func)
{
fixed (byte* code = _code)
{
var funcAddr = (long)func;
var ripAddr = (long)(code + 5); // After call instruction
var offset64 = funcAddr - ripAddr;
var offset32 = (uint)(ulong)offset64;
*(uint*)(code + 1) = offset32;
}
}
public byte* GetCodePointer()
{
fixed (byte* code = _code)
{
return code;
}
}
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct JmpRax
{
private fixed byte _code[12];
public static ReadOnlySpan<byte> Template => new byte[]
{
0x48, 0xB8, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, // movabs rax,<addr>
0xFF, 0xE0 // jmp rax
};
public static int Size => 12;
public void Initialize()
{
fixed (byte* code = _code)
{
Template.CopyTo(new Span<byte>(code, 12));
}
}
public void SetTarget(void* target)
{
fixed (byte* code = _code)
{
*(ulong*)(code + 2) = (ulong)target;
}
}
public byte* GetCodePointer()
{
fixed (byte* code = _code)
{
return code;
}
}
}
public static class SafeCall
{
public static ReadOnlySpan<byte> Template => new byte[]
{
0x51,
0x52,
0x41, 0x50,
0x41, 0x51,
0x41, 0x52,
0x41, 0x53,
0x57,
0x56,
0x48, 0xBF, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11,
0x48, 0xBE, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11,
0x48, 0xB9, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11,
0xB0, 0x01,
0x86, 0x07,
0x84, 0xC0,
0x75, 0xF8,
0xB8, 0xFF, 0xFF, 0xFF, 0xFF,
0xBA, 0xFF, 0xFF, 0xFF, 0xFF,
0x0F, 0xAE, 0x26,
0x48, 0x83, 0xEC, 0x08,
0xFF, 0xD1,
0x48, 0x83, 0xC4, 0x08,
0x48, 0x89, 0xC1,
0xB8, 0xFF, 0xFF, 0xFF, 0xFF,
0xBA, 0xFF, 0xFF, 0xFF, 0xFF,
0x0F, 0xAE, 0x2E,
0x48, 0x89, 0xC8,
0xC6, 0x07, 0x00,
0x5E,
0x5F,
0x41, 0x5B,
0x41, 0x5A,
0x41, 0x59,
0x41, 0x58,
0x5A,
0x59,
0xC3
};
public static int Size => Template.Length; // 0x6c = 108 bytes
}
public static ReadOnlySpan<byte> TlsAccessPattern => new byte[]
{
0x64, 0x48, 0x8B, 0x04, 0x25, 0x00, 0x00, 0x00, 0x00
};
public static void CreateJmpWithIndex(byte* location, uint index, void* handler)
{
var code = location;
code[0] = 0x68;
*(uint*)(code + 1) = index;
code[5] = 0xE9;
var handlerAddr = (long)handler;
var ripAddr = (long)(code + 10); // After push + jmp
var offset64 = handlerAddr - ripAddr;
*(uint*)(code + 6) = (uint)(ulong)offset64;
for (int i = 10; i < 16; i++)
{
code[i] = 0x90;
}
}
public static void CreateCall9(byte* location, void* func)
{
var code = location;
code[0] = 0xE8;
var funcAddr = (long)func;
var ripAddr = (long)(code + 5);
var offset64 = funcAddr - ripAddr;
*(uint*)(code + 1) = (uint)(ulong)offset64;
code[5] = 0x48;
code[6] = 0x89;
code[7] = 0xC0;
code[8] = 0x90;
}
public static void CreateJmpRax(byte* location, void* target)
{
var code = location;
code[0] = 0x48;
code[1] = 0xB8;
*(ulong*)(code + 2) = (ulong)target;
code[10] = 0xFF;
code[11] = 0xE0;
}
public static List<nint> FindTlsAccessPatterns(byte* start, int length)
{
var results = new List<nint>();
var pattern = TlsAccessPattern;
var end = start + length - pattern.Length;
for (var ptr = start; ptr < end; ptr++)
{
if (MatchesPattern(ptr, pattern))
{
results.Add((nint)ptr);
}
}
return results;
}
private static bool MatchesPattern(byte* ptr, ReadOnlySpan<byte> pattern)
{
for (var i = 0; i < pattern.Length; i++)
{
if (ptr[i] != pattern[i])
return false;
}
return true;
}
}
+222
View File
@@ -0,0 +1,222 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu.Native;
public sealed unsafe class StubManager : IDisposable
{
private readonly List<nint> _allocatedStubs = new();
private readonly Dictionary<string, nint> _importHandlers = new();
private readonly Dictionary<ulong, nint> _stubAddresses = new();
private byte* _pltMemory;
private int _pltOffset;
private const int PltMemorySize = 1024 * 1024; // 1MB for stubs
public StubManager()
{
_pltMemory = (byte*)VirtualAlloc(
null,
(nuint)PltMemorySize,
AllocationType.Reserve | AllocationType.Commit,
MemoryProtection.ExecuteReadWrite);
if (_pltMemory == null)
{
throw new OutOfMemoryException("Failed to allocate executable memory for stubs");
}
_pltOffset = 0;
}
public nint CreateImportStub(ulong guestAddress, string nid, ImportHandler handler)
{
if (!_importHandlers.TryGetValue(nid, out var handlerPtr))
{
handlerPtr = CreateHandlerTrampoline(nid, handler);
_importHandlers[nid] = handlerPtr;
}
var stubPtr = _pltMemory + _pltOffset;
var stubIndex = (uint)(_stubAddresses.Count);
JitStubs.CreateJmpWithIndex(stubPtr, stubIndex, (void*)handlerPtr);
_pltOffset += JitStubs.JmpWithIndex.Size;
_stubAddresses[guestAddress] = (nint)stubPtr;
return (nint)stubPtr;
}
public nint CreatePltEntry(uint index, nint gotAddress)
{
var pltPtr = _pltMemory + _pltOffset;
var pltCode = new byte[]
{
0x49, 0xBB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // movabs r11,<addr>
0x41, 0xFF, 0x73, 0x08, // push QWORD PTR [r11+8]
0x41, 0xFF, 0x63, 0x10, // jmp QWORD PTR [r11+16]
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90 // nop padding
};
*(ulong*)(pltCode.AsSpan().Slice(2).GetPinnableReference()) = (ulong)gotAddress;
fixed (byte* src = pltCode)
{
Buffer.MemoryCopy(src, pltPtr, pltCode.Length, pltCode.Length);
}
_pltOffset += pltCode.Length;
return (nint)pltPtr;
}
public nint CreateTlsHandler(delegate*<void> tlsGetAddrFunc)
{
var handlerPtr = _pltMemory + _pltOffset;
JitStubs.CreateCall9(handlerPtr, (void*)tlsGetAddrFunc);
_pltOffset += JitStubs.Call9.Size;
return (nint)handlerPtr;
}
public nint CreateSafeCallTrampoline(delegate*<void> func, byte* regSaveArea, byte* lockVar)
{
var trampolinePtr = _pltMemory + _pltOffset;
var template = JitStubs.SafeCall.Template;
fixed (byte* src = template)
{
Buffer.MemoryCopy(src, trampolinePtr, template.Length, template.Length);
}
*(ulong*)(trampolinePtr + 0x0c + 2) = (ulong)lockVar;
*(ulong*)(trampolinePtr + 0x16 + 2) = (ulong)regSaveArea;
*(ulong*)(trampolinePtr + 0x20 + 2) = (ulong)func;
_pltOffset += template.Length;
return (nint)trampolinePtr;
}
public bool TryGetStubAddress(ulong guestAddress, out nint hostAddress)
{
return _stubAddresses.TryGetValue(guestAddress, out hostAddress);
}
public bool PatchTlsAccess(byte* guestCode, int codeSize, nint tlsHandler)
{
var locations = JitStubs.FindTlsAccessPatterns(guestCode, codeSize);
foreach (var location in locations)
{
JitStubs.CreateCall9((byte*)location, (void*)tlsHandler);
}
return locations.Count > 0;
}
private nint CreateHandlerTrampoline(string nid, ImportHandler handler)
{
var trampolinePtr = _pltMemory + _pltOffset;
var code = new List<byte>();
code.Add(0x57);
code.Add(0x56);
code.Add(0x55);
code.Add(0x53);
code.AddRange(new byte[] { 0x41, 0x54 });
code.AddRange(new byte[] { 0x41, 0x55 });
code.AddRange(new byte[] { 0x41, 0x56 });
code.AddRange(new byte[] { 0x41, 0x57 });
code.AddRange(new byte[] { 0x48, 0x83, 0xEC, 0x28 });
int contextOffset = code.Count;
code.AddRange(new byte[] { 0x48, 0xB9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 });
int handlerOffset = code.Count;
code.AddRange(new byte[] { 0x48, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 });
code.AddRange(new byte[] { 0xFF, 0xD0 });
code.AddRange(new byte[] { 0x48, 0x83, 0xC4, 0x28 });
code.AddRange(new byte[] { 0x41, 0x5F });
code.AddRange(new byte[] { 0x41, 0x5E });
code.AddRange(new byte[] { 0x41, 0x5D });
code.AddRange(new byte[] { 0x41, 0x5C });
code.Add(0x5B);
code.Add(0x5D);
code.Add(0x5E);
code.Add(0x5F);
code.Add(0xC3);
var codeArray = code.ToArray();
fixed (byte* src = codeArray)
{
Buffer.MemoryCopy(src, trampolinePtr, codeArray.Length, codeArray.Length);
}
*(ulong*)(trampolinePtr + contextOffset + 2) = 0;
var handlerPtr = Marshal.GetFunctionPointerForDelegate(handler);
*(ulong*)(trampolinePtr + handlerOffset + 2) = (ulong)handlerPtr;
_pltOffset += codeArray.Length;
_allocatedStubs.Add((nint)trampolinePtr);
return (nint)trampolinePtr;
}
public void Dispose()
{
if (_pltMemory != null)
{
VirtualFree(_pltMemory, 0, FreeType.Release);
_pltMemory = null;
}
_allocatedStubs.Clear();
_importHandlers.Clear();
_stubAddresses.Clear();
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern void* VirtualAlloc(void* lpAddress, nuint dwSize, AllocationType flAllocationType, MemoryProtection flProtect);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool VirtualFree(void* lpAddress, nuint dwSize, FreeType dwFreeType);
[Flags]
private enum AllocationType : uint
{
Commit = 0x1000,
Reserve = 0x2000,
}
[Flags]
private enum MemoryProtection : uint
{
ExecuteReadWrite = 0x40,
}
private enum FreeType : uint
{
Release = 0x8000,
}
public delegate void ImportHandler(CpuContext context);
}
+11
View File
@@ -0,0 +1,11 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Core.Cpu;
internal static class RuntimeStubNids
{
public const string BootstrapBridge = "__internal_bootstrap_bridge";
public const string KernelDynlibDlsym = "__internal_kernel_dynlib_dlsym";
}
+42
View File
@@ -0,0 +1,42 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu;
public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory
{
private readonly ICpuMemory _inner;
public TrackedCpuMemory(ICpuMemory inner)
{
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
}
public CpuMemoryAccessFailure? LastFailure { get; private set; }
public ICpuMemory Inner => _inner;
public bool TryRead(ulong virtualAddress, Span<byte> destination)
{
var result = _inner.TryRead(virtualAddress, destination);
if (!result)
{
LastFailure = new CpuMemoryAccessFailure(virtualAddress, destination.Length, isWrite: false);
}
return result;
}
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
{
var result = _inner.TryWrite(virtualAddress, source);
if (!result)
{
LastFailure = new CpuMemoryAccessFailure(virtualAddress, source.Length, isWrite: true);
}
return result;
}
}
+11
View File
@@ -0,0 +1,11 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Core;
public interface IFileSystem
{
bool Exists(string path);
bool TryReadAllBytes(string path, out byte[] data);
}
+76
View File
@@ -0,0 +1,76 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
namespace SharpEmu.Core.Loader;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct ElfHeader
{
private readonly byte _ident0;
private readonly byte _ident1;
private readonly byte _ident2;
private readonly byte _ident3;
private readonly byte _ident4;
private readonly byte _ident5;
private readonly byte _ident6;
private readonly byte _ident7;
private readonly byte _ident8;
private readonly byte _ident9;
private readonly byte _ident10;
private readonly byte _ident11;
private readonly byte _ident12;
private readonly byte _ident13;
private readonly byte _ident14;
private readonly byte _ident15;
private readonly ushort _type;
private readonly ushort _machine;
private readonly uint _version;
private readonly ulong _entryPoint;
private readonly ulong _programHeaderOffset;
private readonly ulong _sectionHeaderOffset;
private readonly uint _flags;
private readonly ushort _headerSize;
private readonly ushort _programHeaderEntrySize;
private readonly ushort _programHeaderCount;
private readonly ushort _sectionHeaderEntrySize;
private readonly ushort _sectionHeaderCount;
private readonly ushort _sectionHeaderStringIndex;
public bool HasElfMagic => _ident0 == 0x7F && _ident1 == (byte)'E' && _ident2 == (byte)'L' && _ident3 == (byte)'F';
public bool Is64Bit => _ident4 == 2;
public bool IsLittleEndian => _ident5 == 1;
public byte Abi => _ident7;
public byte AbiVersion => _ident8;
public ushort Type => _type;
public ushort Machine => _machine;
public uint Version => _version;
public ulong EntryPoint => _entryPoint;
public ulong ProgramHeaderOffset => _programHeaderOffset;
public ulong SectionHeaderOffset => _sectionHeaderOffset;
public uint Flags => _flags;
public ushort HeaderSize => _headerSize;
public ushort ProgramHeaderEntrySize => _programHeaderEntrySize;
public ushort ProgramHeaderCount => _programHeaderCount;
public ushort SectionHeaderEntrySize => _sectionHeaderEntrySize;
public ushort SectionHeaderCount => _sectionHeaderCount;
public ushort SectionHeaderStringIndex => _sectionHeaderStringIndex;
}
+21
View File
@@ -0,0 +1,21 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core;
using SharpEmu.Core.Memory;
using SharpEmu.HLE;
namespace SharpEmu.Core.Loader;
public interface ISelfLoader
{
SelfImage Load(ReadOnlySpan<byte> imageData, IVirtualMemory virtualMemory);
SelfImage Load(ReadOnlySpan<byte> imageData, IVirtualMemory virtualMemory, IFileSystem? fs, string? mountRoot);
SelfImage Load(ReadOnlySpan<byte> imageData, IVirtualMemory virtualMemory, IModuleManager moduleManager);
SelfImage Load(ReadOnlySpan<byte> imageData, IVirtualMemory virtualMemory, IModuleManager moduleManager, IFileSystem? fs, string? mountRoot);
SelfImage LoadAdditional(ReadOnlySpan<byte> imageData, IVirtualMemory virtualMemory, IModuleManager moduleManager, IFileSystem? fs, string? mountRoot);
}
+103
View File
@@ -0,0 +1,103 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Text.Json;
using SharpEmu.Core;
namespace SharpEmu.Core.Loader;
public sealed record ParamLoader(
string? TitleId,
string? ContentId,
string? ContentVersion,
string? MasterVersion,
string? TargetContentVersion,
LocalizedParameters? LocalizedParameters,
Disc? Disc
);
public sealed record LocalizedParameters(
string? DefaultLanguage,
Dictionary<string, LocalizedLanguage>? Languages
);
public sealed record LocalizedLanguage(string? TitleName);
public sealed record Disc(LocalizedParameters? LocalizedParameters);
public static class Ps5ParamJsonReader
{
public static (string? Title, string? TitleId, string? Version) TryReadPs5Param(IFileSystem fs, string paramJsonPath)
{
if (!fs.Exists(paramJsonPath))
return (null, null, null);
if (!fs.TryReadAllBytes(paramJsonPath, out var data))
return (null, null, null);
return TryReadPs5Param(data);
}
public static (string? Title, string? TitleId, string? Version) TryReadPs5Param(byte[] data)
{
if (data == null || data.Length == 0)
return (null, null, null);
try
{
using var doc = JsonDocument.Parse(data);
return TryReadPs5Param(doc.RootElement);
}
catch (JsonException)
{
return (null, null, null);
}
}
private static (string? Title, string? TitleId, string? Version) TryReadPs5Param(JsonElement root)
{
string? titleId = root.TryGetProperty("titleId", out var eTid) ? eTid.GetString() : null;
string? ver =
(root.TryGetProperty("contentVersion", out var cv) ? cv.GetString() : null)
?? (root.TryGetProperty("masterVersion", out var mv) ? mv.GetString() : null)
?? (root.TryGetProperty("targetContentVersion", out var tv) ? tv.GetString() : null);
string? title = ExtractTitleName(root);
return (title, titleId, ver);
}
private static string? ExtractTitleName(JsonElement root)
{
if (!root.TryGetProperty("localizedParameters", out var lp))
{
if (root.TryGetProperty("disc", out var disc) && disc.ValueKind == JsonValueKind.Object)
{
disc.TryGetProperty("localizedParameters", out lp);
}
}
if (lp.ValueKind != JsonValueKind.Object)
return null;
string? defLang = lp.TryGetProperty("defaultLanguage", out var dl) ? dl.GetString() : null;
if (!string.IsNullOrEmpty(defLang))
{
if (lp.TryGetProperty(defLang, out var langObj) && langObj.ValueKind == JsonValueKind.Object)
{
if (langObj.TryGetProperty("titleName", out var tn))
return tn.GetString();
}
}
if (lp.TryGetProperty("en-US", out var en) && en.ValueKind == JsonValueKind.Object)
{
if (en.TryGetProperty("titleName", out var tn2))
return tn2.GetString();
}
return null;
}
}
+70
View File
@@ -0,0 +1,70 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
namespace SharpEmu.Core.Loader;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct ProgramHeader
{
private readonly uint _type;
private readonly uint _flags;
private readonly ulong _offset;
private readonly ulong _virtualAddress;
private readonly ulong _physicalAddress;
private readonly ulong _fileSize;
private readonly ulong _memorySize;
private readonly ulong _alignment;
public uint Type => _type;
public ProgramHeaderType HeaderType => (ProgramHeaderType)_type;
public uint RawFlags => _flags;
public ProgramHeaderFlags Flags => (ProgramHeaderFlags)_flags;
public ulong Offset => _offset;
public ulong VirtualAddress => _virtualAddress;
public ulong PhysicalAddress => _physicalAddress;
public ulong FileSize => _fileSize;
public ulong MemorySize => _memorySize;
public ulong Alignment => _alignment;
}
public enum ProgramHeaderType : uint
{
Null = 0,
Load = 1,
Dynamic = 2,
Tls = 7,
SceRela = 0x60000000,
SceProcParam = 0x61000001,
SceDynLibData = 0x61000000,
SceRelro = 0x61000010,
}
[Flags]
public enum ProgramHeaderFlags : uint
{
None = 0,
Execute = 0x1,
Write = 0x2,
Read = 0x4,
}
+50
View File
@@ -0,0 +1,50 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Memory;
namespace SharpEmu.Core.Loader;
public sealed class SelfImage
{
private readonly ulong _imageBase;
public SelfImage(
bool isSelf,
ElfHeader elfHeader,
IReadOnlyList<ProgramHeader> programHeaders,
IReadOnlyList<VirtualMemoryRegion> mappedRegions,
IReadOnlyDictionary<ulong, string>? importStubs = null,
IReadOnlyDictionary<string, ulong>? runtimeSymbols = null,
ulong imageBase = 0,
ulong procParamAddress = 0)
{
ArgumentNullException.ThrowIfNull(programHeaders);
ArgumentNullException.ThrowIfNull(mappedRegions);
IsSelf = isSelf;
ElfHeader = elfHeader;
ProgramHeaders = programHeaders;
MappedRegions = mappedRegions;
ImportStubs = importStubs ?? new Dictionary<ulong, string>();
RuntimeSymbols = runtimeSymbols ?? new Dictionary<string, ulong>(StringComparer.Ordinal);
_imageBase = imageBase;
ProcParamAddress = procParamAddress;
}
public bool IsSelf { get; }
public ElfHeader ElfHeader { get; }
public IReadOnlyList<ProgramHeader> ProgramHeaders { get; }
public IReadOnlyList<VirtualMemoryRegion> MappedRegions { get; }
public IReadOnlyDictionary<ulong, string> ImportStubs { get; }
public IReadOnlyDictionary<string, ulong> RuntimeSymbols { get; }
public ulong EntryPoint => ElfHeader.EntryPoint + _imageBase;
public ulong ProcParamAddress { get; }
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,16 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Loader;
using SharpEmu.HLE;
namespace SharpEmu.Core.Memory;
public interface IVirtualMemory : ICpuMemory
{
void Clear();
void Map(ulong virtualAddress, ulong memorySize, ulong fileOffset, ReadOnlySpan<byte> fileData, ProgramHeaderFlags protection);
IReadOnlyList<VirtualMemoryRegion> SnapshotRegions();
}
@@ -0,0 +1,490 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
using SharpEmu.Core.Loader;
using SharpEmu.HLE;
namespace SharpEmu.Core.Memory;
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
{
private readonly object _gate = new();
private readonly List<MemoryRegion> _regions = new();
private bool _disposed;
private const ulong PageSize = 0x1000;
private const ulong LargeDataReserveThreshold = 0x4000_0000UL; // 1 GiB
private const ulong LazyReservePrimeBytes = 0x5000_0000UL; // 1.25 GiB
private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB
private const uint MEM_COMMIT = 0x1000;
private const uint MEM_RESERVE = 0x2000;
private const uint MEM_RELEASE = 0x8000;
private const uint PAGE_EXECUTE_READ = 0x20;
private const uint PAGE_EXECUTE_READWRITE = 0x40;
private const uint PAGE_EXECUTE = 0x10;
private const uint PAGE_EXECUTE_WRITECOPY = 0x80;
private const uint PAGE_READWRITE = 0x04;
private const uint PAGE_READONLY = 0x02;
[DllImport("kernel32.dll", SetLastError = true)]
private static extern void* VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool VirtualFree(void* lpAddress, nuint dwSize, uint dwFreeType);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool VirtualProtect(void* lpAddress, nuint dwSize, uint flNewProtect, out uint lpflOldProtect);
[DllImport("kernel32.dll")]
private static extern void FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
public bool TryAllocateAtExact(ulong desiredAddress, ulong size, bool executable, out ulong actualAddress)
{
actualAddress = 0;
if (size == 0)
{
return false;
}
var alignedSize = (size + 0xFFF) & ~0xFFFUL;
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
var allocationType = MEM_COMMIT | MEM_RESERVE;
var result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, allocationType, protection);
if (result == null)
{
return false;
}
actualAddress = (ulong)result;
if (actualAddress != desiredAddress)
{
VirtualFree(result, 0, MEM_RELEASE);
actualAddress = 0;
return false;
}
lock (_gate)
{
_regions.Add(new MemoryRegion
{
VirtualAddress = actualAddress,
Size = alignedSize,
IsExecutable = executable,
IsReservedOnly = false,
Protection = protection
});
}
var allocationKind = executable ? "executable memory" : "data memory";
Console.Error.WriteLine($"[VMEM] Allocated exact {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes)");
return true;
}
public ulong AllocateAt(ulong desiredAddress, ulong size, bool executable = true, bool allowAlternative = true)
{
if (size == 0)
throw new ArgumentOutOfRangeException(nameof(size), "Size must be greater than zero");
var alignedSize = (size + 0xFFF) & ~0xFFFUL;
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
var allocationType = MEM_COMMIT | MEM_RESERVE;
var reservedOnly = false;
var preferReserveOnly = !executable && alignedSize >= LargeDataReserveThreshold;
void* result = null;
if (preferReserveOnly)
{
result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, MEM_RESERVE, PAGE_READWRITE);
if (result == null && allowAlternative)
{
result = VirtualAlloc(null, (nuint)alignedSize, MEM_RESERVE, PAGE_READWRITE);
}
if (result != null)
{
reservedOnly = true;
}
}
if (result == null)
{
result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, allocationType, protection);
}
if (result == null)
{
if (!allowAlternative)
{
throw new InvalidOperationException($"Failed to allocate exact mapping at 0x{desiredAddress:X16} ({alignedSize} bytes)");
}
Console.Error.WriteLine($"[VMEM] Could not allocate at 0x{desiredAddress:X16}, trying any address...");
result = VirtualAlloc(null, (nuint)alignedSize, allocationType, protection);
if (result == null)
{
if (!executable)
{
result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, MEM_RESERVE, PAGE_READWRITE);
if (result == null && allowAlternative)
{
result = VirtualAlloc(null, (nuint)alignedSize, MEM_RESERVE, PAGE_READWRITE);
}
if (result != null)
{
reservedOnly = true;
}
}
if (result == null)
{
throw new OutOfMemoryException($"Failed to allocate {alignedSize} bytes of virtual memory");
}
}
}
var actualAddress = (ulong)result;
var lazyPrimeState = "n/a";
if (reservedOnly)
{
var primeBytes = Math.Min(alignedSize, LazyReservePrimeBytes);
if (primeBytes != 0)
{
ulong committedBytes = 0;
while (committedBytes < primeBytes)
{
var remaining = primeBytes - committedBytes;
var chunkBytes = Math.Min(remaining, LazyReservePrimeChunkBytes);
var commitAddress = (void*)(actualAddress + committedBytes);
var committed = VirtualAlloc(commitAddress, (nuint)chunkBytes, MEM_COMMIT, PAGE_READWRITE);
if (committed == null)
{
break;
}
committedBytes += chunkBytes;
}
if (committedBytes != 0)
{
lazyPrimeState = committedBytes == primeBytes
? $"ok:{committedBytes:X}"
: $"partial:{committedBytes:X}/{primeBytes:X}";
Console.Error.WriteLine($"[VMEM] Primed lazy region: 0x{actualAddress:X16} - 0x{actualAddress + committedBytes:X16} ({committedBytes} bytes)");
}
else
{
lazyPrimeState = $"fail:{primeBytes:X}";
Console.Error.WriteLine($"[VMEM] Failed to prime lazy region at 0x{actualAddress:X16} ({primeBytes} bytes), continuing with on-demand commit");
}
}
else
{
lazyPrimeState = "skip:0";
}
}
lock (_gate)
{
_regions.Add(new MemoryRegion
{
VirtualAddress = actualAddress,
Size = alignedSize,
IsExecutable = executable,
IsReservedOnly = reservedOnly,
Protection = protection
});
}
var allocationKind = reservedOnly
? "reserved data memory (lazy commit)"
: (executable ? "executable memory" : "data memory");
Console.Error.WriteLine($"[VMEM] Allocated {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes) lazy_prime={lazyPrimeState}");
return actualAddress;
}
public void Clear()
{
lock (_gate)
{
foreach (var region in _regions)
{
VirtualFree((void*)region.VirtualAddress, 0, MEM_RELEASE);
}
_regions.Clear();
}
}
public void Map(ulong virtualAddress, ulong memorySize, ulong fileOffset, ReadOnlySpan<byte> fileData, ProgramHeaderFlags protection)
{
if (memorySize == 0)
throw new ArgumentOutOfRangeException(nameof(memorySize));
if ((ulong)fileData.Length > memorySize)
throw new ArgumentOutOfRangeException(nameof(fileData), "File size cannot exceed memory size");
var mapStart = AlignDown(virtualAddress, PageSize);
var segmentEnd = checked(virtualAddress + memorySize);
var mapEnd = AlignUp(segmentEnd, PageSize);
var mapSize = checked(mapEnd - mapStart);
lock (_gate)
{
var existingRegion = FindRegion(mapStart, mapSize);
if (existingRegion == null)
{
var isExecutable = (protection & ProgramHeaderFlags.Execute) != 0;
AllocateAt(mapStart, mapSize, isExecutable, allowAlternative: false);
}
var stageProtection = (protection & ProgramHeaderFlags.Execute) != 0
? ProgramHeaderFlags.Read | ProgramHeaderFlags.Write | ProgramHeaderFlags.Execute
: ProgramHeaderFlags.Read | ProgramHeaderFlags.Write;
SetProtection(mapStart, mapSize, stageProtection);
if (!fileData.IsEmpty)
{
var destPtr = (void*)virtualAddress;
fixed (byte* srcPtr = fileData)
{
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)memorySize, (nuint)fileData.Length);
}
}
var zeroFillSize = memorySize - (ulong)fileData.Length;
if (zeroFillSize != 0)
{
NativeMemory.Clear((void*)(virtualAddress + (ulong)fileData.Length), (nuint)zeroFillSize);
}
SetProtection(mapStart, mapSize, protection);
Console.Error.WriteLine($"[VMEM] Mapped segment: 0x{virtualAddress:X16} - 0x{virtualAddress + memorySize:X16} (file: {fileData.Length} bytes, prot: {protection})");
}
}
private void SetProtection(ulong address, ulong size, ProgramHeaderFlags flags)
{
uint protection;
if ((flags & ProgramHeaderFlags.Execute) != 0)
{
protection = (flags & ProgramHeaderFlags.Write) != 0
? PAGE_EXECUTE_READWRITE
: PAGE_EXECUTE_READ;
}
else if ((flags & ProgramHeaderFlags.Write) != 0)
{
protection = PAGE_READWRITE;
}
else
{
protection = PAGE_READONLY;
}
if (!VirtualProtect((void*)address, (nuint)size, protection, out _))
{
throw new InvalidOperationException($"Failed to set memory protection at 0x{address:X16}");
}
if ((flags & ProgramHeaderFlags.Execute) != 0)
{
FlushInstructionCache(null, (void*)address, (nuint)size);
}
}
public IReadOnlyList<VirtualMemoryRegion> SnapshotRegions()
{
lock (_gate)
{
var snapshot = new VirtualMemoryRegion[_regions.Count];
for (var i = 0; i < _regions.Count; i++)
{
var r = _regions[i];
snapshot[i] = new VirtualMemoryRegion(
r.VirtualAddress,
r.Size,
0,
r.Size,
r.IsExecutable ? ProgramHeaderFlags.Execute | ProgramHeaderFlags.Read : ProgramHeaderFlags.Read);
}
return snapshot;
}
}
public bool TryRead(ulong virtualAddress, Span<byte> destination)
{
lock (_gate)
{
foreach (var region in _regions)
{
if (TryResolveRegionOffset(virtualAddress, (ulong)destination.Length, region, out var offset))
{
var srcPtr = (void*)(region.VirtualAddress + offset);
fixed (byte* destPtr = destination)
{
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
}
return true;
}
}
return false;
}
}
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
{
lock (_gate)
{
foreach (var region in _regions)
{
if (TryResolveRegionOffset(virtualAddress, (ulong)source.Length, region, out var offset))
{
var destPtr = (void*)(region.VirtualAddress + offset);
if (source.IsEmpty)
{
return true;
}
if (!VirtualProtect(destPtr, (nuint)source.Length, PAGE_EXECUTE_READWRITE, out var oldProtect))
{
return false;
}
try
{
fixed (byte* srcPtr = source)
{
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
}
}
finally
{
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _);
if (IsExecutableProtection(oldProtect))
{
FlushInstructionCache(null, destPtr, (nuint)source.Length);
}
}
return true;
}
}
return false;
}
}
public bool TryWriteUInt64(ulong virtualAddress, ulong value)
{
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
BitConverter.TryWriteBytes(buffer, value);
return TryWrite(virtualAddress, buffer);
}
public void* GetPointer(ulong virtualAddress)
{
lock (_gate)
{
foreach (var region in _regions)
{
if (virtualAddress >= region.VirtualAddress &&
virtualAddress < region.VirtualAddress + region.Size)
{
return (void*)virtualAddress;
}
}
return null;
}
}
public bool IsAccessible(ulong virtualAddress, ulong size)
{
lock (_gate)
{
foreach (var region in _regions)
{
if (TryResolveRegionOffset(virtualAddress, size, region, out _))
{
return true;
}
}
return false;
}
}
private MemoryRegion? FindRegion(ulong address, ulong size)
{
foreach (var region in _regions)
{
if (TryResolveRegionOffset(address, size, region, out _))
{
return region;
}
}
return null;
}
private static bool TryResolveRegionOffset(ulong address, ulong size, MemoryRegion region, out ulong offset)
{
offset = 0;
if (address < region.VirtualAddress)
{
return false;
}
offset = address - region.VirtualAddress;
if (offset > region.Size)
{
return false;
}
if (size > region.Size - offset)
{
return false;
}
return true;
}
private static bool IsExecutableProtection(uint protection)
{
return protection is PAGE_EXECUTE or PAGE_EXECUTE_READ or PAGE_EXECUTE_READWRITE or PAGE_EXECUTE_WRITECOPY;
}
private static ulong AlignDown(ulong value, ulong alignment)
{
var mask = alignment - 1;
return value & ~mask;
}
private static ulong AlignUp(ulong value, ulong alignment)
{
var mask = alignment - 1;
return checked((value + mask) & ~mask);
}
public void Dispose()
{
if (!_disposed)
{
Clear();
_disposed = true;
}
}
private class MemoryRegion
{
public ulong VirtualAddress { get; set; }
public ulong Size { get; set; }
public bool IsExecutable { get; set; }
public bool IsReservedOnly { get; set; }
public uint Protection { get; set; }
}
}
+127
View File
@@ -0,0 +1,127 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Loader;
namespace SharpEmu.Core.Memory;
public sealed class VirtualMemory : IVirtualMemory
{
private readonly object _gate = new();
private readonly List<MappedRegion> _regions = new();
public void Clear()
{
lock (_gate)
{
_regions.Clear();
}
}
public void Map(ulong virtualAddress, ulong memorySize, ulong fileOffset, ReadOnlySpan<byte> fileData, ProgramHeaderFlags protection)
{
if (memorySize == 0)
{
throw new ArgumentOutOfRangeException(nameof(memorySize), "Memory size must be greater than zero.");
}
if ((ulong)fileData.Length > memorySize)
{
throw new ArgumentOutOfRangeException(nameof(fileData), "File size cannot exceed memory size.");
}
if (memorySize > int.MaxValue)
{
throw new NotSupportedException("Virtual memory regions larger than 2 GB are not currently supported.");
}
var endAddress = checked(virtualAddress + memorySize);
var backingMemory = new byte[(int)memorySize];
fileData.CopyTo(backingMemory);
lock (_gate)
{
foreach (var existing in _regions)
{
if (virtualAddress < existing.EndAddress && endAddress > existing.Region.VirtualAddress)
{
throw new InvalidOperationException("Attempted to map an overlapping virtual memory region.");
}
}
_regions.Add(new MappedRegion(
new VirtualMemoryRegion(virtualAddress, memorySize, fileOffset, (ulong)fileData.Length, protection),
endAddress,
backingMemory));
}
}
public IReadOnlyList<VirtualMemoryRegion> SnapshotRegions()
{
lock (_gate)
{
var snapshot = new VirtualMemoryRegion[_regions.Count];
for (var i = 0; i < _regions.Count; i++)
{
snapshot[i] = _regions[i].Region;
}
return snapshot;
}
}
public bool TryRead(ulong virtualAddress, Span<byte> destination)
{
lock (_gate)
{
if (!TryResolveRegion(virtualAddress, destination.Length, out var region, out var offset))
{
return false;
}
region.BackingMemory.AsSpan(offset, destination.Length).CopyTo(destination);
return true;
}
}
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
{
lock (_gate)
{
if (!TryResolveRegion(virtualAddress, source.Length, out var region, out var offset))
{
return false;
}
source.CopyTo(region.BackingMemory.AsSpan(offset, source.Length));
return true;
}
}
private bool TryResolveRegion(ulong virtualAddress, int length, out MappedRegion region, out int offset)
{
foreach (var candidate in _regions)
{
if (virtualAddress < candidate.Region.VirtualAddress || virtualAddress >= candidate.EndAddress)
{
continue;
}
var candidateOffset = checked((int)(virtualAddress - candidate.Region.VirtualAddress));
if (candidateOffset + length > candidate.BackingMemory.Length)
{
break;
}
region = candidate;
offset = candidateOffset;
return true;
}
region = default;
offset = 0;
return false;
}
private readonly record struct MappedRegion(VirtualMemoryRegion Region, ulong EndAddress, byte[] BackingMemory);
}
@@ -0,0 +1,33 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Loader;
namespace SharpEmu.Core.Memory;
public readonly struct VirtualMemoryRegion
{
public VirtualMemoryRegion(
ulong virtualAddress,
ulong memorySize,
ulong fileOffset,
ulong fileSize,
ProgramHeaderFlags protection)
{
VirtualAddress = virtualAddress;
MemorySize = memorySize;
FileOffset = fileOffset;
FileSize = fileSize;
Protection = protection;
}
public ulong VirtualAddress { get; }
public ulong MemorySize { get; }
public ulong FileOffset { get; }
public ulong FileSize { get; }
public ProgramHeaderFlags Protection { get; }
}
+37
View File
@@ -0,0 +1,37 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Core;
public sealed class PhysicalFileSystem : IFileSystem
{
public bool Exists(string path)
{
if (string.IsNullOrWhiteSpace(path))
return false;
return File.Exists(path);
}
public bool TryReadAllBytes(string path, out byte[] data)
{
data = Array.Empty<byte>();
if (string.IsNullOrWhiteSpace(path))
return false;
if (!File.Exists(path))
return false;
try
{
data = File.ReadAllBytes(path);
return true;
}
catch
{
data = Array.Empty<byte>();
return false;
}
}
}
@@ -0,0 +1,26 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Loader;
using SharpEmu.HLE;
namespace SharpEmu.Core.Runtime;
public interface ISharpEmuRuntime : IDisposable
{
string? LastExecutionDiagnostics { get; }
string? LastExecutionTrace { get; }
string? LastSessionSummary { get; }
string? LastBasicBlockTrace { get; }
string? LastMilestoneLog { get; }
SelfImage LoadImage(string ebootPath);
OrbisGen2Result Run(string ebootPath);
OrbisGen2Result DispatchHleCall(string nid, CpuContext context);
}
@@ -0,0 +1,713 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core;
using SharpEmu.Core.Cpu;
using SharpEmu.Core.Cpu.Disasm;
using SharpEmu.Core.Loader;
using SharpEmu.Core.Memory;
using SharpEmu.HLE;
using SharpEmu.Libs.VideoOut;
using SharpEmu.Libs.Kernel;
using SharpEmu.Libs.AppContent;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace SharpEmu.Core.Runtime;
public sealed class SharpEmuRuntime : ISharpEmuRuntime
{
private static readonly HashSet<string> PreloadSkipModules = new(StringComparer.OrdinalIgnoreCase)
{
"libkernel.prx",
"libkernel_sys.prx",
};
private readonly ISelfLoader _selfLoader;
private readonly IVirtualMemory _virtualMemory;
private readonly ICpuDispatcher _cpuDispatcher;
private readonly IModuleManager _moduleManager;
private readonly ISymbolCatalog _symbolCatalog;
private readonly CpuExecutionOptions _cpuExecutionOptions;
private readonly IFileSystem _fileSystem;
private bool _disposed;
public string? LastExecutionDiagnostics { get; private set; }
public string? LastExecutionTrace { get; private set; }
public string? LastSessionSummary { get; private set; }
public string? LastBasicBlockTrace { get; private set; }
public string? LastMilestoneLog { get; private set; }
public SharpEmuRuntime(
ISelfLoader selfLoader,
IVirtualMemory virtualMemory,
ICpuDispatcher cpuDispatcher,
IModuleManager moduleManager,
ISymbolCatalog? symbolCatalog = null,
CpuExecutionOptions cpuExecutionOptions = default,
IFileSystem? fileSystem = null)
{
_selfLoader = selfLoader ?? throw new ArgumentNullException(nameof(selfLoader));
_virtualMemory = virtualMemory ?? throw new ArgumentNullException(nameof(virtualMemory));
_cpuDispatcher = cpuDispatcher ?? throw new ArgumentNullException(nameof(cpuDispatcher));
_moduleManager = moduleManager ?? throw new ArgumentNullException(nameof(moduleManager));
_symbolCatalog = symbolCatalog ?? Aerolib.Empty;
_cpuExecutionOptions = new CpuExecutionOptions
{
CpuEngine = cpuExecutionOptions.CpuEngine,
StrictDynlibResolution = cpuExecutionOptions.StrictDynlibResolution,
ImportTraceLimit = Math.Max(0, cpuExecutionOptions.ImportTraceLimit),
};
_fileSystem = fileSystem ?? new PhysicalFileSystem();
}
public static ISharpEmuRuntime CreateDefault(SharpEmuRuntimeOptions options = default)
{
var cpuExecutionOptions = new CpuExecutionOptions
{
CpuEngine = options.CpuEngine,
StrictDynlibResolution = options.StrictDynlibResolution,
ImportTraceLimit = Math.Max(0, options.ImportTraceLimit),
};
var moduleManager = new ModuleManager();
moduleManager.RegisterFromAssembly(typeof(VideoOutExports).Assembly, Generation.Gen4 | Generation.Gen5, Aerolib.Instance);
moduleManager.RegisterFromAssembly(typeof(KernelExports).Assembly, Generation.Gen4 | Generation.Gen5, Aerolib.Instance);
moduleManager.Freeze();
var virtualMemory = new PhysicalVirtualMemory();
var fileSystem = new PhysicalFileSystem();
return new SharpEmuRuntime(
new SelfLoader(),
virtualMemory,
new CpuDispatcher(virtualMemory, moduleManager),
moduleManager,
Aerolib.Instance,
cpuExecutionOptions,
fileSystem);
}
public SelfImage LoadImage(string ebootPath)
{
ArgumentException.ThrowIfNullOrWhiteSpace(ebootPath);
var fullPath = Path.GetFullPath(ebootPath);
if (!File.Exists(fullPath))
{
throw new FileNotFoundException("Executable file was not found.", fullPath);
}
var fileInfo = new FileInfo(fullPath);
if (fileInfo.Length > int.MaxValue)
{
throw new NotSupportedException("Images larger than 2 GB are not currently supported.");
}
var bytes = GC.AllocateUninitializedArray<byte>((int)fileInfo.Length);
using (var stream = File.OpenRead(fullPath))
{
stream.ReadExactly(bytes);
}
var mountRoot = Path.GetDirectoryName(fullPath);
return _selfLoader.Load(bytes.AsSpan(), _virtualMemory, _moduleManager, _fileSystem, mountRoot);
}
public OrbisGen2Result Run(string ebootPath)
{
Console.Error.WriteLine($"[RUNTIME] Loading: {ebootPath}");
LastExecutionDiagnostics = null;
LastExecutionTrace = null;
LastSessionSummary = null;
LastBasicBlockTrace = null;
LastMilestoneLog = null;
KernelModuleRegistry.Reset();
var image = LoadImage(ebootPath);
var normalizedEbootPath = Path.GetFullPath(ebootPath);
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
Console.Error.WriteLine($"[RUNTIME] Entry: 0x{image.EntryPoint:X16}");
var generation = image.ElfHeader.AbiVersion == 2 ? Generation.Gen5 : Generation.Gen4;
var activeImportStubs = new Dictionary<ulong, string>(image.ImportStubs);
var activeRuntimeSymbols = new Dictionary<string, ulong>(image.RuntimeSymbols, StringComparer.Ordinal);
LoadAdjacentSceModules(ebootPath, activeImportStubs, activeRuntimeSymbols);
var processImageName = Path.GetFileName(ebootPath);
if (string.IsNullOrWhiteSpace(processImageName))
{
processImageName = "eboot.bin";
}
Console.Error.WriteLine($"[RUNTIME] Dispatching, gen: {generation}");
Console.Error.WriteLine($"[RUNTIME] About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
var result = _cpuDispatcher.DispatchEntry(
image.EntryPoint,
generation,
activeImportStubs,
activeRuntimeSymbols,
processImageName,
_cpuExecutionOptions);
Console.Error.WriteLine($"[RUNTIME] DispatchEntry returned: {result}");
Console.Error.WriteLine($"[RUNTIME] Dispatch result: {result}");
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
LastBasicBlockTrace = _cpuDispatcher.LastBasicBlockTrace;
if (result == OrbisGen2Result.ORBIS_GEN2_ERROR_CPU_TRAP && _cpuDispatcher.LastTrapInfo is { } trapInfo)
{
var opcodeBytes = ReadOpcodePreview(trapInfo.InstructionPointer, 8);
var decodedTrapText = string.Empty;
var ud2Hint = string.Empty;
if (_cpuExecutionOptions.EnableDisasmDiagnostics && TryDecodeInstructionAt(trapInfo.InstructionPointer, out var trapInstruction))
{
decodedTrapText = BuildDecodedInstructionFields(in trapInstruction);
if (string.Equals(trapInstruction.Mnemonic, "Ud2", StringComparison.OrdinalIgnoreCase))
{
ud2Hint = ", trap=ud2";
}
}
else if (opcodeBytes.StartsWith("0F 0B", StringComparison.Ordinal))
{
ud2Hint = ", trap=ud2";
}
var longModeHint = IsInvalidLongModeOpcode(trapInfo.Opcode)
? ", hint=invalid opcode for x64 long mode; likely wrong jump target or decode desync"
: string.Empty;
var hint = string.Empty;
if (image.IsSelf &&
activeImportStubs.Count == 0 &&
trapInfo.InstructionPointer == 0 &&
trapInfo.Opcode == 0xCC)
{
hint = ", hint=SELF appears encrypted or unresolved; use a decrypted ELF/FSELF image";
}
var transferText = string.Empty;
if (_cpuDispatcher.LastControlTransferInfo is { } transferInfo)
{
var transferMode = transferInfo.IsIndirect ? "indirect" : "direct";
var transferBytes = ReadOpcodePreview(transferInfo.SourceInstructionPointer, 8);
var transferDecodedText = TryDecodeInstructionAt(transferInfo.SourceInstructionPointer, out var transferInstruction)
? BuildDecodedInstructionFields(in transferInstruction, fieldPrefix: "src_inst")
: string.Empty;
transferText =
$", last_transfer={transferInfo.Kind}:{transferMode} src=0x{transferInfo.SourceInstructionPointer:X16} op=0x{transferInfo.Opcode:X2} bytes={transferBytes} dst=0x{transferInfo.TargetInstructionPointer:X16}{transferDecodedText}";
}
var ripStubText = activeImportStubs.TryGetValue(trapInfo.InstructionPointer, out var trapStubNid)
? $", rip_stub={trapStubNid}"
: string.Empty;
var diagnosticsBuilder = new StringBuilder(1024);
diagnosticsBuilder.Append(
$"CPU trap at RIP=0x{trapInfo.InstructionPointer:X16}, opcode=0x{trapInfo.Opcode:X2}, bytes={opcodeBytes}{decodedTrapText}, import_stubs={activeImportStubs.Count}{ud2Hint}{longModeHint}{hint}{ripStubText}{transferText}");
if (!string.IsNullOrWhiteSpace(_cpuDispatcher.LastRecentControlTransferTrace))
{
diagnosticsBuilder.AppendLine();
diagnosticsBuilder.Append("Recent transfers:");
diagnosticsBuilder.AppendLine();
diagnosticsBuilder.Append(_cpuDispatcher.LastRecentControlTransferTrace);
}
if (!string.IsNullOrWhiteSpace(_cpuDispatcher.LastRecentInstructionWindow))
{
diagnosticsBuilder.AppendLine();
diagnosticsBuilder.Append("Recent instructions:");
diagnosticsBuilder.AppendLine();
diagnosticsBuilder.Append(_cpuDispatcher.LastRecentInstructionWindow);
}
LastExecutionDiagnostics = diagnosticsBuilder.ToString();
}
else if (result == OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT && _cpuDispatcher.LastMemoryFaultInfo is { } faultInfo)
{
var opcodeText = faultInfo.Opcode.HasValue ? $"0x{faultInfo.Opcode.Value:X2}" : "??";
var decodedFaultText = string.Empty;
if (_cpuExecutionOptions.EnableDisasmDiagnostics && TryDecodeInstructionAt(faultInfo.InstructionPointer, out var faultInstruction))
{
decodedFaultText = BuildDecodedInstructionFields(in faultInstruction);
if (!faultInfo.Opcode.HasValue && faultInstruction.Bytes.Length > 0)
{
opcodeText = $"0x{faultInstruction.Bytes[0]:X2}";
}
}
var accessType = faultInfo.Access.IsWrite ? "write" : "read";
var transferText = string.Empty;
if (_cpuDispatcher.LastControlTransferInfo is { } transferInfo)
{
var transferMode = transferInfo.IsIndirect ? "indirect" : "direct";
var transferBytes = ReadOpcodePreview(transferInfo.SourceInstructionPointer, 8);
var transferDecodedText = TryDecodeInstructionAt(transferInfo.SourceInstructionPointer, out var transferInstruction)
? BuildDecodedInstructionFields(in transferInstruction, fieldPrefix: "src_inst")
: string.Empty;
var rbxTarget = TryReadUInt64At(transferInfo.Rbx, out var rbxDeref)
? $"*rbx=0x{rbxDeref:X16}"
: "*rbx=??";
transferText =
$", last_transfer={transferInfo.Kind}:{transferMode} src=0x{transferInfo.SourceInstructionPointer:X16} op=0x{transferInfo.Opcode:X2} bytes={transferBytes} dst=0x{transferInfo.TargetInstructionPointer:X16}{transferDecodedText} rax=0x{transferInfo.Rax:X16} rbx=0x{transferInfo.Rbx:X16} {rbxTarget} rsp=0x{transferInfo.Rsp:X16} rbp=0x{transferInfo.Rbp:X16}";
}
var ripStubText = activeImportStubs.TryGetValue(faultInfo.InstructionPointer, out var faultStubNid)
? $", rip_stub={faultStubNid}"
: string.Empty;
LastExecutionDiagnostics =
$"Memory fault at RIP=0x{faultInfo.InstructionPointer:X16}, opcode={opcodeText}{decodedFaultText}, {accessType}@0x{faultInfo.Access.Address:X16} size={faultInfo.Access.Size}, import_stubs={activeImportStubs.Count}{ripStubText}{transferText}";
}
else if (result == OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED && _cpuDispatcher.LastNotImplementedInfo is { } notImplementedInfo)
{
var inferredNid = notImplementedInfo.Nid;
var decodedNotImplementedText = TryDecodeInstructionAt(notImplementedInfo.InstructionPointer, out var notImplementedInstruction)
? BuildDecodedInstructionFields(in notImplementedInstruction)
: string.Empty;
var ripStubText = string.Empty;
if (activeImportStubs.TryGetValue(notImplementedInfo.InstructionPointer, out var ripStubNid))
{
ripStubText = $", rip_stub={ripStubNid}";
if (string.IsNullOrWhiteSpace(inferredNid))
{
inferredNid = ripStubNid;
}
}
var inferredExportName = notImplementedInfo.ExportName;
var inferredLibraryName = notImplementedInfo.LibraryName;
if (!string.IsNullOrWhiteSpace(inferredNid) &&
_moduleManager.TryGetExport(inferredNid, out var export))
{
inferredExportName = string.IsNullOrWhiteSpace(inferredExportName) ? export.Name : inferredExportName;
inferredLibraryName = string.IsNullOrWhiteSpace(inferredLibraryName) ? export.LibraryName : inferredLibraryName;
}
var nidText = string.IsNullOrWhiteSpace(inferredNid) ? "?" : inferredNid;
var exportText = string.IsNullOrWhiteSpace(inferredExportName) ? "?" : inferredExportName;
var libraryText = string.IsNullOrWhiteSpace(inferredLibraryName) ? "?" : inferredLibraryName;
var detailText = string.IsNullOrWhiteSpace(notImplementedInfo.Detail) ? string.Empty : $", detail={notImplementedInfo.Detail}";
var transferText = string.Empty;
if (_cpuDispatcher.LastControlTransferInfo is { } transferInfo)
{
var transferMode = transferInfo.IsIndirect ? "indirect" : "direct";
var transferBytes = ReadOpcodePreview(transferInfo.SourceInstructionPointer, 8);
var transferDecodedText = TryDecodeInstructionAt(transferInfo.SourceInstructionPointer, out var transferInstruction)
? BuildDecodedInstructionFields(in transferInstruction, fieldPrefix: "src_inst")
: string.Empty;
var transferStubText = activeImportStubs.TryGetValue(transferInfo.TargetInstructionPointer, out var transferStubNid)
? $" stub={transferStubNid}"
: string.Empty;
transferText =
$", last_transfer={transferInfo.Kind}:{transferMode} src=0x{transferInfo.SourceInstructionPointer:X16} op=0x{transferInfo.Opcode:X2} bytes={transferBytes} dst=0x{transferInfo.TargetInstructionPointer:X16}{transferDecodedText}{transferStubText}";
}
var aerolibText = string.Empty;
if (!string.IsNullOrWhiteSpace(inferredExportName) &&
_symbolCatalog.TryGetByExportName(inferredExportName, out var symbol))
{
aerolibText = $", aerolib_nid={symbol.Nid}";
}
else if (!string.IsNullOrWhiteSpace(inferredNid) &&
_symbolCatalog.TryGetByNid(inferredNid, out var symbolByNid))
{
aerolibText = $", aerolib_export={symbolByNid.ExportName}";
}
LastExecutionDiagnostics =
$"Not implemented: source={notImplementedInfo.Source}, rip=0x{notImplementedInfo.InstructionPointer:X16}{decodedNotImplementedText}, nid={nidText}, export={exportText}, library={libraryText}, import_stubs={activeImportStubs.Count}{ripStubText}{aerolibText}{detailText}{transferText}";
}
return result;
}
private void LoadAdjacentSceModules(
string ebootPath,
IDictionary<ulong, string> importStubs,
IDictionary<string, ulong> runtimeSymbols)
{
var ebootDirectory = Path.GetDirectoryName(ebootPath);
if (string.IsNullOrWhiteSpace(ebootDirectory))
{
return;
}
var moduleDirectories = new[]
{
Path.Combine(ebootDirectory, "sce_module"),
Path.Combine(ebootDirectory, "sce_modules"),
}
.Distinct(StringComparer.OrdinalIgnoreCase)
.Where(Directory.Exists)
.ToArray();
if (moduleDirectories.Length == 0)
{
return;
}
var allModulePaths = moduleDirectories
.SelectMany(Directory.EnumerateFiles)
.Where(path =>
{
var extension = Path.GetExtension(path);
return string.Equals(extension, ".prx", StringComparison.OrdinalIgnoreCase) ||
string.Equals(extension, ".sprx", StringComparison.OrdinalIgnoreCase);
})
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
.ToArray();
var modulePaths = allModulePaths
.Where(ShouldPreloadModule)
.ToArray();
var skippedModules = allModulePaths
.Where(path => !ShouldPreloadModule(path))
.Select(Path.GetFileName)
.Where(name => !string.IsNullOrWhiteSpace(name))
.ToArray();
if (skippedModules.Length > 0)
{
Console.Error.WriteLine($"[RUNTIME] Skipping {skippedModules.Length} core module(s): {string.Join(", ", skippedModules)}");
}
if (modulePaths.Length == 0)
{
return;
}
Console.Error.WriteLine($"[RUNTIME] Module search directories: {string.Join(", ", moduleDirectories)}");
Console.Error.WriteLine($"[RUNTIME] Loading {modulePaths.Length} module(s)...");
var loadedModules = 0;
var failedModules = 0;
var mergedImportCount = 0;
var mergedSymbolCount = 0;
foreach (var modulePath in modulePaths)
{
try
{
var fileInfo = new FileInfo(modulePath);
if (!fileInfo.Exists || fileInfo.Length <= 0 || fileInfo.Length > int.MaxValue)
{
failedModules++;
continue;
}
var moduleBytes = GC.AllocateUninitializedArray<byte>((int)fileInfo.Length);
using (var stream = File.OpenRead(modulePath))
{
stream.ReadExactly(moduleBytes);
}
var moduleImage = _selfLoader.LoadAdditional(
moduleBytes.AsSpan(),
_virtualMemory,
_moduleManager,
_fileSystem,
Path.GetDirectoryName(modulePath));
mergedImportCount += MergeImportStubs(importStubs, moduleImage.ImportStubs, modulePath);
mergedSymbolCount += MergeRuntimeSymbols(runtimeSymbols, moduleImage.RuntimeSymbols);
RegisterLoadedModule(modulePath, moduleImage, isMain: false, isSystemModule: false);
loadedModules++;
Console.Error.WriteLine(
$"[RUNTIME] Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
}
catch (Exception ex)
{
failedModules++;
Console.Error.WriteLine($"[RUNTIME] Module load failed: {modulePath} ({ex.GetType().Name}: {ex.Message})");
}
}
Console.Error.WriteLine(
$"[RUNTIME] Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
}
private static int MergeImportStubs(
IDictionary<ulong, string> destination,
IReadOnlyDictionary<ulong, string> source,
string modulePath)
{
var added = 0;
foreach (var (address, nid) in source)
{
if (destination.TryGetValue(address, out var existingNid))
{
if (!string.Equals(existingNid, nid, StringComparison.Ordinal))
{
Console.Error.WriteLine(
$"[RUNTIME] Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
}
continue;
}
destination[address] = nid;
added++;
}
return added;
}
private static int MergeRuntimeSymbols(
IDictionary<string, ulong> destination,
IReadOnlyDictionary<string, ulong> source)
{
var added = 0;
foreach (var (name, address) in source)
{
if (string.IsNullOrWhiteSpace(name) || !IsUsableRuntimeSymbolAddress(address))
{
continue;
}
if (destination.TryGetValue(name, out var existingAddress))
{
if (IsPreferredRuntimeSymbolAddress(existingAddress, address))
{
destination[name] = address;
added++;
}
continue;
}
destination[name] = address;
added++;
}
return added;
}
private static bool IsPreferredRuntimeSymbolAddress(ulong existingAddress, ulong candidateAddress)
{
return !IsUsableRuntimeSymbolAddress(existingAddress) && IsUsableRuntimeSymbolAddress(candidateAddress);
}
private static bool IsUsableRuntimeSymbolAddress(ulong address)
{
return address >= 0x10000 && !IsUnresolvedRuntimeSentinel(address);
}
private static bool IsUnresolvedRuntimeSentinel(ulong value)
{
return value == 0xFFFEUL ||
value == 0xFFFFFFFEUL ||
value == 0xFFFFFFFFFFFFFFFEUL;
}
private static bool ShouldPreloadModule(string modulePath)
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_PRELOAD_ALL_SCE_MODULES"), "1", StringComparison.Ordinal))
{
return true;
}
var fileName = Path.GetFileName(modulePath);
if (string.IsNullOrWhiteSpace(fileName))
{
return false;
}
return !PreloadSkipModules.Contains(fileName);
}
private static void RegisterLoadedModule(string modulePath, SelfImage image, bool isMain, bool isSystemModule)
{
if (!TryComputeImageRange(image, out var baseAddress, out var size))
{
baseAddress = 0;
size = 0;
}
var handle = KernelModuleRegistry.RegisterModule(
modulePath,
baseAddress,
size,
image.EntryPoint,
isMain,
isSystemModule);
Console.Error.WriteLine(
$"[RUNTIME] Registered module handle={handle} name={Path.GetFileName(modulePath)} base=0x{baseAddress:X16} size=0x{size:X16}");
}
private static bool TryComputeImageRange(SelfImage image, out ulong baseAddress, out ulong size)
{
baseAddress = 0;
size = 0;
if (image.ProgramHeaders.Count == 0)
{
return false;
}
var imageBase = image.EntryPoint >= image.ElfHeader.EntryPoint
? image.EntryPoint - image.ElfHeader.EntryPoint
: 0UL;
var found = false;
ulong minAddress = ulong.MaxValue;
ulong maxAddress = 0;
for (var i = 0; i < image.ProgramHeaders.Count; i++)
{
var header = image.ProgramHeaders[i];
if (header.HeaderType != ProgramHeaderType.Load || header.MemorySize == 0)
{
continue;
}
var segmentStart = unchecked(imageBase + header.VirtualAddress);
var segmentEnd = unchecked(segmentStart + header.MemorySize);
if (!found || segmentStart < minAddress)
{
minAddress = segmentStart;
}
if (!found || segmentEnd > maxAddress)
{
maxAddress = segmentEnd;
}
found = true;
}
if (!found || maxAddress <= minAddress)
{
return false;
}
baseAddress = minAddress;
size = maxAddress - minAddress;
return true;
}
private static string BuildSessionSummary(CpuSessionSummary summary)
{
var resultText = summary.Result == OrbisGen2Result.ORBIS_GEN2_OK ? "OK" : summary.Result.ToString();
var exitText = summary.ExitCode.HasValue ? summary.ExitCode.Value.ToString() : "?";
return
$"Summary: result={resultText} reason={summary.Reason} exit={exitText} last_guest_rip=0x{summary.LastGuestRip:X16} last_stub_rip=0x{summary.LastStubRip:X16} instr={summary.TotalInstructions} imports={summary.ImportsHit} unique_nids={summary.UniqueNidsHit}";
}
private string ReadOpcodePreview(ulong instructionPointer, int maxBytes)
{
if (maxBytes <= 0)
{
return "??";
}
Span<byte> oneByte = stackalloc byte[1];
var parts = new string[maxBytes];
var count = 0;
for (var i = 0; i < maxBytes; i++)
{
if (!_virtualMemory.TryRead(instructionPointer + (ulong)i, oneByte))
{
break;
}
parts[count] = oneByte[0].ToString("X2");
count++;
}
return count == 0 ? "??" : string.Join(' ', parts, 0, count);
}
private bool TryReadUInt64At(ulong address, out ulong value)
{
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
if (!_virtualMemory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
return true;
}
private bool TryDecodeInstructionAt(ulong instructionPointer, out DecodedInst decodedInstruction)
{
if (!IcedDecoder.TryReadGuestBytes(_virtualMemory, instructionPointer, maxLen: 15, out var instructionBytes))
{
decodedInstruction = default;
return false;
}
return IcedDecoder.TryDecode(instructionPointer, instructionBytes, out decodedInstruction);
}
private static bool IsInvalidLongModeOpcode(byte opcode)
{
return opcode is
0x06 or // PUSH ES
0x07 or // POP ES
0x0E or // PUSH CS
0x16 or // PUSH SS
0x17 or // POP SS
0x1E or // PUSH DS
0x1F or // POP DS
0x27 or // DAA
0x2F or // DAS
0x37 or // AAA
0x3F or // AAS
0x60 or // PUSHA/PUSHAD
0x61 or // POPA/POPAD
0xD4 or // AAM
0xD5; // AAD
}
private static string BuildDecodedInstructionFields(in DecodedInst instruction, string fieldPrefix = "inst")
{
var text = $", {fieldPrefix}={instruction.Text}, {fieldPrefix}_len={instruction.Length}, {fieldPrefix}_mnemonic={instruction.Mnemonic}, {fieldPrefix}_flow={instruction.FlowControl}";
if (instruction.NearBranchTarget is { } target)
{
text += $", {fieldPrefix}_target=0x{target:X16}";
}
if (instruction.MemoryAddress is { } memoryAddress)
{
text += $", {fieldPrefix}_mem=0x{memoryAddress:X16}";
}
if (instruction.Bytes.Length > 0)
{
text += $", {fieldPrefix}_bytes={IcedDecoder.FormatBytes(instruction.Bytes)}";
}
return text;
}
public OrbisGen2Result DispatchHleCall(string nid, CpuContext context)
{
return _moduleManager.Dispatch(nid, context);
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
if (_cpuDispatcher is IDisposable disposableDispatcher)
{
disposableDispatcher.Dispose();
}
if (_virtualMemory is IDisposable disposableMemory)
{
disposableMemory.Dispose();
}
}
}
@@ -0,0 +1,15 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Core.Runtime;
using SharpEmu.Core.Cpu;
public readonly struct SharpEmuRuntimeOptions
{
public CpuExecutionEngine CpuEngine { get; init; }
public bool StrictDynlibResolution { get; init; }
public int ImportTraceLimit { get; init; }
}
+20
View File
@@ -0,0 +1,20 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\SharpEmu.HLE\SharpEmu.HLE.csproj" />
<ProjectReference Include="..\SharpEmu.Libs\SharpEmu.Libs.csproj" />
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Iced" />
</ItemGroup>
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
</Project>
+25
View File
@@ -0,0 +1,25 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"Iced": {
"type": "Direct",
"requested": "[1.21.0, )",
"resolved": "1.21.0",
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
},
"sharpemu.hle": {
"type": "Project"
},
"sharpemu.libs": {
"type": "Project",
"dependencies": {
"SharpEmu.HLE": "[1.0.0, )"
}
},
"sharpemu.logging": {
"type": "Project"
}
}
}
}
+156
View File
@@ -0,0 +1,156 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Linq;
namespace SharpEmu.HLE;
public sealed class Aerolib : ISymbolCatalog
{
private static readonly Lazy<Aerolib> _instance = new(() => new Aerolib());
private static readonly Aerolib EmptyCatalog = new Aerolib(empty: true);
private Dictionary<string, SysAbiSymbol> _byNid;
private Dictionary<string, SysAbiSymbol> _byExportName;
public static Aerolib Instance => _instance.Value;
private Aerolib(
Dictionary<string, SysAbiSymbol> byNid,
Dictionary<string, SysAbiSymbol> byExportName)
{
_byNid = byNid;
_byExportName = byExportName;
}
private Aerolib()
{
_byNid = new Dictionary<string, SysAbiSymbol>(StringComparer.Ordinal);
_byExportName = new Dictionary<string, SysAbiSymbol>(StringComparer.Ordinal);
LoadFromEmbeddedBinary();
}
private Aerolib(bool empty)
{
_byNid = new Dictionary<string, SysAbiSymbol>(StringComparer.Ordinal);
_byExportName = new Dictionary<string, SysAbiSymbol>(StringComparer.Ordinal);
}
public static ISymbolCatalog Empty => EmptyCatalog;
public string GetName(string nid)
{
if (string.IsNullOrEmpty(nid))
return nid ?? string.Empty;
if (_byNid.TryGetValue(nid, out var symbol))
return symbol.ExportName;
return nid;
}
public bool TryGetName(string nid, out string name)
{
if (string.IsNullOrEmpty(nid))
{
name = string.Empty;
return false;
}
if (_byNid.TryGetValue(nid, out var symbol))
{
name = symbol.ExportName;
return true;
}
name = string.Empty;
return false;
}
public bool ContainsNid(string nid)
{
if (string.IsNullOrEmpty(nid))
return false;
return _byNid.ContainsKey(nid);
}
public Dictionary<string, string> GetAllNidNames()
{
var result = new Dictionary<string, string>(_byNid.Count, StringComparer.Ordinal);
foreach (var kvp in _byNid)
{
result[kvp.Key] = kvp.Value.ExportName;
}
return result;
}
public int Count => _byNid.Count;
public bool TryGetByNid(string nid, out SysAbiSymbol symbol)
{
ArgumentException.ThrowIfNullOrWhiteSpace(nid);
return _byNid.TryGetValue(nid, out symbol);
}
public bool TryGetByExportName(string exportName, out SysAbiSymbol symbol)
{
ArgumentException.ThrowIfNullOrWhiteSpace(exportName);
return _byExportName.TryGetValue(exportName, out symbol);
}
private void LoadFromEmbeddedBinary()
{
try
{
var assembly = typeof(Aerolib).Assembly;
var resourceName = assembly.GetManifestResourceNames()
.FirstOrDefault(n => n.EndsWith("aerolib.bin", StringComparison.OrdinalIgnoreCase));
if (resourceName == null)
{
Console.Error.WriteLine("[AEROLIB] Embedded resource 'aerolib.bin' not found");
return;
}
using var stream = assembly.GetManifestResourceStream(resourceName);
if (stream == null)
{
Console.Error.WriteLine("[AEROLIB] Failed to open embedded resource stream");
return;
}
var data = new byte[stream.Length];
stream.ReadExactly(data);
int offset = 0;
uint count = BinaryPrimitives.ReadUInt32LittleEndian(data.AsSpan(offset, 4));
offset += 4;
_byNid = new Dictionary<string, SysAbiSymbol>((int)count, StringComparer.Ordinal);
_byExportName = new Dictionary<string, SysAbiSymbol>((int)count, StringComparer.Ordinal);
for (uint i = 0; i < count; i++)
{
byte nidLen = data[offset++];
string nid = System.Text.Encoding.UTF8.GetString(data, offset, nidLen);
offset += nidLen;
ushort nameLen = BinaryPrimitives.ReadUInt16LittleEndian(data.AsSpan(offset, 2));
offset += 2;
string name = System.Text.Encoding.UTF8.GetString(data, offset, nameLen);
offset += nameLen;
var symbol = new SysAbiSymbol(nid, name, name, Generation.Gen5);
_byNid[nid] = symbol;
_byExportName[name] = symbol;
}
Console.Error.WriteLine($"[AEROLIB] Loaded {_byNid.Count} NID entries from binary resource");
}
catch (Exception ex)
{
Console.Error.WriteLine($"[AEROLIB] Failed to load embedded aerolib.bin: {ex.Message}");
}
}
}
Binary file not shown.
+172
View File
@@ -0,0 +1,172 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
namespace SharpEmu.HLE;
public sealed class CpuContext
{
private readonly ulong[] _registers = new ulong[16];
private readonly ulong[] _xmmRegisters = new ulong[32];
private readonly ulong[] _ymmUpperRegisters = new ulong[32];
private bool _raxWritten;
public CpuContext(ICpuMemory memory, Generation generation)
{
Memory = memory ?? throw new ArgumentNullException(nameof(memory));
TargetGeneration = generation;
}
public ICpuMemory Memory { get; }
public Generation TargetGeneration { get; }
public ulong Rip { get; set; }
public ulong Rflags { get; set; }
public ulong FsBase { get; set; }
public ulong GsBase { get; set; }
public ulong this[CpuRegister register]
{
get => _registers[(int)register];
set
{
_registers[(int)register] = value;
if (register == CpuRegister.Rax)
{
_raxWritten = true;
}
}
}
public void ClearRaxWriteFlag()
{
_raxWritten = false;
}
public bool WasRaxWritten => _raxWritten;
public void GetXmmRegister(int registerIndex, out ulong low, out ulong high)
{
if ((uint)registerIndex >= 16)
{
throw new ArgumentOutOfRangeException(nameof(registerIndex));
}
var offset = registerIndex * 2;
low = _xmmRegisters[offset];
high = _xmmRegisters[offset + 1];
}
public void SetXmmRegister(int registerIndex, ulong low, ulong high)
{
if ((uint)registerIndex >= 16)
{
throw new ArgumentOutOfRangeException(nameof(registerIndex));
}
var offset = registerIndex * 2;
_xmmRegisters[offset] = low;
_xmmRegisters[offset + 1] = high;
}
public void GetYmmUpper(int registerIndex, out ulong low, out ulong high)
{
if ((uint)registerIndex >= 16)
{
throw new ArgumentOutOfRangeException(nameof(registerIndex));
}
var offset = registerIndex * 2;
low = _ymmUpperRegisters[offset];
high = _ymmUpperRegisters[offset + 1];
}
public void SetYmmUpper(int registerIndex, ulong low, ulong high)
{
if ((uint)registerIndex >= 16)
{
throw new ArgumentOutOfRangeException(nameof(registerIndex));
}
var offset = registerIndex * 2;
_ymmUpperRegisters[offset] = low;
_ymmUpperRegisters[offset + 1] = high;
}
public void ClearYmmUpper(int registerIndex)
{
SetYmmUpper(registerIndex, 0, 0);
}
public void ClearAllYmmUpper()
{
Array.Clear(_ymmUpperRegisters);
}
public void GetYmmRegister(
int registerIndex,
out ulong lowLow,
out ulong lowHigh,
out ulong highLow,
out ulong highHigh)
{
GetXmmRegister(registerIndex, out lowLow, out lowHigh);
GetYmmUpper(registerIndex, out highLow, out highHigh);
}
public void SetYmmRegister(
int registerIndex,
ulong lowLow,
ulong lowHigh,
ulong highLow,
ulong highHigh)
{
SetXmmRegister(registerIndex, lowLow, lowHigh);
SetYmmUpper(registerIndex, highLow, highHigh);
}
public bool TryReadUInt64(ulong address, out ulong value)
{
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
if (!Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
return true;
}
public bool TryWriteUInt64(ulong address, ulong value)
{
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
return Memory.TryWrite(address, buffer);
}
public bool PushUInt64(ulong value)
{
var rsp = this[CpuRegister.Rsp];
rsp -= sizeof(ulong);
this[CpuRegister.Rsp] = rsp;
return TryWriteUInt64(rsp, value);
}
public bool PopUInt64(out ulong value)
{
var rsp = this[CpuRegister.Rsp];
if (!TryReadUInt64(rsp, out value))
{
return false;
}
this[CpuRegister.Rsp] = rsp + sizeof(ulong);
return true;
}
}
+39
View File
@@ -0,0 +1,39 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE;
public enum CpuRegister : int
{
Rax = 0,
Rcx = 1,
Rdx = 2,
Rbx = 3,
Rsp = 4,
Rbp = 5,
Rsi = 6,
Rdi = 7,
R8 = 8,
R9 = 9,
R10 = 10,
R11 = 11,
R12 = 12,
R13 = 13,
R14 = 14,
R15 = 15,
}
+31
View File
@@ -0,0 +1,31 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE;
public sealed class ExportedFunction
{
public ExportedFunction(string libraryName, string nid, string name, Generation target, SysAbiFunction function)
{
ArgumentException.ThrowIfNullOrWhiteSpace(libraryName);
ArgumentException.ThrowIfNullOrWhiteSpace(nid);
ArgumentException.ThrowIfNullOrWhiteSpace(name);
ArgumentNullException.ThrowIfNull(function);
LibraryName = libraryName;
Nid = nid;
Name = name;
Target = target;
Function = function;
}
public string LibraryName { get; }
public string Nid { get; }
public string Name { get; }
public Generation Target { get; }
public SysAbiFunction Function { get; }
}
+14
View File
@@ -0,0 +1,14 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE;
[Flags]
public enum Generation
{
None = 0,
Gen4 = 1,
Gen5 = 2,
}
+11
View File
@@ -0,0 +1,11 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE;
public interface ICpuMemory
{
bool TryRead(ulong virtualAddress, Span<byte> destination);
bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source);
}
+21
View File
@@ -0,0 +1,21 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Reflection;
namespace SharpEmu.HLE;
public interface IModuleManager
{
int RegisterFromAssembly(Assembly assembly, Generation generation, ISymbolCatalog? symbolCatalog = null);
void Freeze();
bool TryGetFunction(string nid, out Delegate function);
bool TryGetExport(string nid, out ExportedFunction export);
bool TryGetExportByName(string exportName, out ExportedFunction export);
OrbisGen2Result Dispatch(string nid, CpuContext context);
}
+11
View File
@@ -0,0 +1,11 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE;
public interface ISymbolCatalog
{
bool TryGetByNid(string nid, out SysAbiSymbol symbol);
bool TryGetByExportName(string exportName, out SysAbiSymbol symbol);
}
+226
View File
@@ -0,0 +1,226 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
using System.Reflection;
namespace SharpEmu.HLE;
public sealed class ModuleManager : IModuleManager
{
private readonly ConcurrentDictionary<string, Delegate> _dispatchTable = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, ExportedFunction> _exportTable = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, ExportedFunction> _exportNameTable = new(StringComparer.Ordinal);
private readonly object _registrationGate = new();
private bool _isFrozen;
public int RegisterFromAssembly(Assembly assembly, Generation generation, ISymbolCatalog? symbolCatalog = null)
{
ArgumentNullException.ThrowIfNull(assembly);
lock (_registrationGate)
{
if (_isFrozen)
{
throw new InvalidOperationException("Module registration is frozen.");
}
var registeredCount = 0;
var instances = new Dictionary<Type, object>();
foreach (var type in assembly.GetTypes())
{
foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static))
{
var exportAttribute = method.GetCustomAttribute<SysAbiExportAttribute>(inherit: false);
if (exportAttribute is null)
{
continue;
}
var exportInfo = ResolveExportInfo(exportAttribute, method, generation, symbolCatalog);
if (exportInfo is null)
{
continue;
}
var handler = CreateHandler(type, method, instances);
if (!_dispatchTable.TryAdd(exportInfo.Value.Nid, handler))
{
continue;
}
_exportTable[exportInfo.Value.Nid] = new ExportedFunction(
exportInfo.Value.LibraryName,
exportInfo.Value.Nid,
exportInfo.Value.ExportName,
exportInfo.Value.Target,
(SysAbiFunction)handler);
_exportNameTable.TryAdd(exportInfo.Value.ExportName, _exportTable[exportInfo.Value.Nid]);
registeredCount++;
}
}
return registeredCount;
}
}
public void Freeze()
{
lock (_registrationGate)
{
_isFrozen = true;
}
}
public bool TryGetFunction(string nid, out Delegate function)
{
ArgumentException.ThrowIfNullOrWhiteSpace(nid);
return _dispatchTable.TryGetValue(nid, out function!);
}
public bool TryGetExport(string nid, out ExportedFunction export)
{
ArgumentException.ThrowIfNullOrWhiteSpace(nid);
return _exportTable.TryGetValue(nid, out export!);
}
public bool TryGetExportByName(string exportName, out ExportedFunction export)
{
ArgumentException.ThrowIfNullOrWhiteSpace(exportName);
return _exportNameTable.TryGetValue(exportName, out export!);
}
public OrbisGen2Result Dispatch(string nid, CpuContext context)
{
ArgumentException.ThrowIfNullOrWhiteSpace(nid);
ArgumentNullException.ThrowIfNull(context);
if (!_dispatchTable.TryGetValue(nid, out var function) || !_exportTable.TryGetValue(nid, out var export))
{
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
if ((export.Target & context.TargetGeneration) == 0)
{
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
context.ClearRaxWriteFlag();
int ret = ((SysAbiFunction)function).Invoke(context);
if (!context.WasRaxWritten)
{
context[CpuRegister.Rax] = unchecked((ulong)ret);
}
return (OrbisGen2Result)ret;
}
private static Delegate CreateHandler(Type ownerType, MethodInfo method, IDictionary<Type, object> instances)
{
ValidateSignature(method);
object? target = null;
if (!method.IsStatic)
{
if (!instances.TryGetValue(ownerType, out target))
{
target = Activator.CreateInstance(ownerType)
?? throw new InvalidOperationException($"Cannot instantiate module type: {ownerType.FullName}");
instances.Add(ownerType, target);
}
}
var parameterCount = method.GetParameters().Length;
if (parameterCount == 0)
{
var noArg = method.IsStatic
? (Func<int>)method.CreateDelegate(typeof(Func<int>))
: (Func<int>)method.CreateDelegate(typeof(Func<int>), target!);
SysAbiFunction adapter = _ => noArg();
return adapter;
}
return method.IsStatic
? method.CreateDelegate(typeof(SysAbiFunction))
: method.CreateDelegate(typeof(SysAbiFunction), target!);
}
private static void ValidateSignature(MethodInfo method)
{
if (method.ReturnType != typeof(int))
{
throw new InvalidOperationException(
$"Method {method.DeclaringType?.FullName}.{method.Name} must return int.");
}
var parameters = method.GetParameters();
if (parameters.Length == 0)
{
return;
}
if (parameters.Length == 1 && parameters[0].ParameterType == typeof(CpuContext))
{
return;
}
throw new InvalidOperationException(
$"Method {method.DeclaringType?.FullName}.{method.Name} must accept no arguments or one {nameof(CpuContext)} argument.");
}
private static ExportInfo? ResolveExportInfo(
SysAbiExportAttribute exportAttribute,
MethodInfo method,
Generation generation,
ISymbolCatalog? symbolCatalog)
{
var target = exportAttribute.Target == Generation.None
? generation
: exportAttribute.Target;
if ((target & generation) == 0)
{
return null;
}
var nid = exportAttribute.Nid;
var exportName = exportAttribute.ExportName;
if (string.IsNullOrWhiteSpace(nid) && !string.IsNullOrWhiteSpace(exportName) && symbolCatalog?.TryGetByExportName(exportName, out var byName) == true)
{
nid = byName.Nid;
}
if (!string.IsNullOrWhiteSpace(nid) && symbolCatalog?.TryGetByNid(nid, out var byNid) == true)
{
exportName = string.IsNullOrWhiteSpace(exportName) ? byNid.ExportName : exportName;
target = exportAttribute.Target == Generation.None ? byNid.Target : target;
}
if (string.IsNullOrWhiteSpace(nid))
{
throw new InvalidOperationException(
$"Method {method.DeclaringType?.FullName}.{method.Name} must define a NID or match one in symbols catalog.");
}
if (string.IsNullOrWhiteSpace(exportName))
{
exportName = method.Name;
}
if ((target & generation) == 0)
{
return null;
}
var libraryName = string.IsNullOrWhiteSpace(exportAttribute.LibraryName) ? "libKernel" : exportAttribute.LibraryName;
return new ExportInfo(nid, exportName, libraryName, target);
}
private readonly record struct ExportInfo(string Nid, string ExportName, string LibraryName, Generation Target);
}
+47
View File
@@ -0,0 +1,47 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE;
/// <summary>
/// Represents synthetic kernel-style result codes used by the Gen5 runtime.
/// Prefixed with ORBIS_GEN2 to distinguish from PS4-oriented ORBIS_* codes
/// used by other emulators such as shadPS4.
/// </summary>
public enum OrbisGen2Result : int
{
/// <summary>
/// Indicates successful completion.
/// </summary>
ORBIS_GEN2_OK = 0,
/// <summary>
/// Indicates that the requested export was not found.
/// </summary>
ORBIS_GEN2_ERROR_NOT_FOUND = unchecked((int)0x80020002),
/// <summary>
/// Indicates that one or more arguments were invalid.
/// </summary>
ORBIS_GEN2_ERROR_INVALID_ARGUMENT = unchecked((int)0x80020003),
/// <summary>
/// Indicates that an item already exists.
/// </summary>
ORBIS_GEN2_ERROR_ALREADY_EXISTS = unchecked((int)0x80020004),
/// <summary>
/// Indicates that behavior is recognized but not implemented yet.
/// </summary>
ORBIS_GEN2_ERROR_NOT_IMPLEMENTED = unchecked((int)0x8002FFFF),
/// <summary>
/// Indicates that memory access failed.
/// </summary>
ORBIS_GEN2_ERROR_MEMORY_FAULT = unchecked((int)0x80020101),
/// <summary>
/// Indicates that CPU execution trapped on an unsupported instruction.
/// </summary>
ORBIS_GEN2_ERROR_CPU_TRAP = unchecked((int)0x80020102),
}
+14
View File
@@ -0,0 +1,14 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Aerolib\aerolib.bin" />
</ItemGroup>
</Project>
+16
View File
@@ -0,0 +1,16 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE;
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
public sealed class SysAbiExportAttribute : Attribute
{
public string LibraryName { get; set; } = "libKernel";
public string Nid { get; set; } = string.Empty;
public string ExportName { get; set; } = string.Empty;
public Generation Target { get; set; } = Generation.None;
}
+6
View File
@@ -0,0 +1,6 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE;
public delegate int SysAbiFunction(CpuContext context);
+26
View File
@@ -0,0 +1,26 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE;
public readonly struct SysAbiSymbol
{
public SysAbiSymbol(string nid, string aliasName, string exportName, Generation target)
{
ArgumentException.ThrowIfNullOrWhiteSpace(nid);
ArgumentException.ThrowIfNullOrWhiteSpace(exportName);
Nid = nid;
AliasName = aliasName;
ExportName = exportName;
Target = target;
}
public string Nid { get; }
public string AliasName { get; }
public string ExportName { get; }
public Generation Target { get; }
}
+6
View File
@@ -0,0 +1,6 @@
{
"version": 2,
"dependencies": {
"net10.0": {}
}
}
@@ -0,0 +1,20 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.AppContent;
public static class AppContentExports
{
[SysAbiExport(
Nid = "xnd8BJzAxmk",
ExportName = "sceAppContentGetAddcontInfoList",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAppContent")]
public static int AppContentGetAddcontInfoList(CpuContext ctx)
{
_ = ctx;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}
+205
View File
@@ -0,0 +1,205 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System;
using System.Collections.Concurrent;
using System.Threading;
using SharpEmu.HLE;
namespace SharpEmu.Libs.CxxAbi;
public static class CxaGuardExports
{
private sealed class GuardState
{
public int OwnerThreadId { get; set; }
}
private static readonly ConcurrentDictionary<ulong, GuardState> _inProgress = new();
[SysAbiExport(
Nid = "3GPpjQdAMTw",
ExportName = "__cxa_guard_acquire",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int CxaGuardAcquire(CpuContext ctx)
{
var guardPtr = ctx[CpuRegister.Rdi];
if (guardPtr == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var currentThreadId = Environment.CurrentManagedThreadId;
var spinner = new SpinWait();
while (true)
{
if (!TryReadGuardInitialized(ctx, guardPtr, out var initialized))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
LogGuardState(ctx, "guard_acquire", guardPtr, initialized);
if (initialized)
{
ctx[CpuRegister.Rax] = 0;
LogGuardResult("guard_acquire", guardPtr, result: 0, initialized, inProgress: false, ownerThreadId: 0);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
var newState = new GuardState
{
OwnerThreadId = currentThreadId,
};
if (_inProgress.TryAdd(guardPtr, newState))
{
ctx[CpuRegister.Rax] = 1;
LogGuardResult("guard_acquire", guardPtr, result: 1, initialized, inProgress: true, ownerThreadId: currentThreadId);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (_inProgress.TryGetValue(guardPtr, out var state))
{
if (state.OwnerThreadId == currentThreadId)
{
ctx[CpuRegister.Rax] = 0;
LogGuardResult("guard_acquire", guardPtr, result: 0, initialized, inProgress: true, ownerThreadId: state.OwnerThreadId);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}
spinner.SpinOnce();
if (spinner.Count % 32 == 0)
{
Thread.Yield();
}
}
}
[SysAbiExport(
Nid = "",
ExportName = "__cxa_guard_release",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int CxaGuardRelease(CpuContext ctx)
{
var guardPtr = ctx[CpuRegister.Rdi];
if (guardPtr == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (_inProgress.TryGetValue(guardPtr, out var state) &&
state.OwnerThreadId != Environment.CurrentManagedThreadId)
{
ctx[CpuRegister.Rax] = 0;
LogGuardResult("guard_release", guardPtr, result: 0, initialized: false, inProgress: true, ownerThreadId: state.OwnerThreadId);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryWriteGuardInitialized(ctx, guardPtr, initialized: true))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
_inProgress.TryRemove(guardPtr, out _);
LogGuardState(ctx, "guard_release", guardPtr, initialized: true);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "",
ExportName = "__cxa_guard_abort",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int CxaGuardAbort(CpuContext ctx)
{
var guardPtr = ctx[CpuRegister.Rdi];
if (guardPtr == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (_inProgress.TryGetValue(guardPtr, out var state) &&
state.OwnerThreadId != Environment.CurrentManagedThreadId)
{
ctx[CpuRegister.Rax] = 0;
LogGuardResult("guard_abort", guardPtr, result: 0, initialized: false, inProgress: true, ownerThreadId: state.OwnerThreadId);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
_ = TryWriteGuardInitialized(ctx, guardPtr, initialized: false);
_inProgress.TryRemove(guardPtr, out _);
LogGuardState(ctx, "guard_abort", guardPtr, initialized: false);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool TryReadGuardInitialized(CpuContext ctx, ulong guardPtr, out bool initialized)
{
initialized = false;
var aligned = guardPtr & ~7UL;
var shift = (int)((guardPtr & 7UL) * 8);
if (!ctx.TryReadUInt64(aligned, out var word))
{
return false;
}
var b0 = (byte)((word >> shift) & 0xFF);
initialized = (b0 & 0x01) != 0;
return true;
}
private static bool TryWriteGuardInitialized(CpuContext ctx, ulong guardPtr, bool initialized)
{
var aligned = guardPtr & ~7UL;
var shift = (int)((guardPtr & 7UL) * 8);
var mask = 0xFFUL << shift;
if (!ctx.TryReadUInt64(aligned, out var word))
{
return false;
}
var b0 = (byte)((word >> shift) & 0xFF);
b0 = initialized ? (byte)(b0 | 0x01) : (byte)(b0 & ~0x01);
var newWord = (word & ~mask) | ((ulong)b0 << shift);
return ctx.TryWriteUInt64(aligned, newWord);
}
private static void LogGuardState(CpuContext ctx, string op, ulong guardPtr, bool initialized)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_GUARDS"), "1", StringComparison.Ordinal))
{
return;
}
var aligned = guardPtr & ~7UL;
var readable = ctx.TryReadUInt64(aligned, out var word);
Console.Error.WriteLine(
$"[LOADER][TRACE] {op}: guard=0x{guardPtr:X16} aligned=0x{aligned:X16} init={initialized} word={(readable ? $"0x{word:X16}" : "<unreadable>")}");
}
private static void LogGuardResult(string op, ulong guardPtr, int result, bool initialized, bool inProgress, int ownerThreadId)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_GUARDS"), "1", StringComparison.Ordinal))
{
return;
}
Console.Error.WriteLine(
$"[LOADER][TRACE] {op}: guard=0x{guardPtr:X16} result={result} init={initialized} in_progress={inProgress} owner_thread={ownerThreadId}");
}
}
+373
View File
@@ -0,0 +1,373 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Threading;
namespace SharpEmu.Libs.Kernel;
public static class KernelExports
{
private static long _nextThreadId = 1;
private static int _nextFileDescriptor = 2;
private static readonly object _cxaGate = new();
private static readonly List<CxaDestructorEntry> _cxaDestructors = new();
private readonly record struct CxaDestructorEntry(
ulong Function,
ulong Argument,
ulong ModuleHandle);
[SysAbiExport(
Nid = "WB66evu8bsU",
ExportName = "sceKernelGetCompiledSdkVersion",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelGetCompiledSdkVersion(CpuContext ctx)
{
_ = ctx;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "uMei1W9uyNo",
ExportName = "exit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int Exit(CpuContext ctx)
{
_ = ctx;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "XKRegsFpEpk",
ExportName = "catchReturnFromMain",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int CatchReturnFromMain(CpuContext ctx)
{
var status = unchecked((int)ctx[CpuRegister.Rdi]);
Console.Error.WriteLine($"[LOADER][INFO] catchReturnFromMain(status={status})");
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "bzQExy189ZI",
ExportName = "_init_env",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int InitEnv(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "8G2LB+A3rzg",
ExportName = "atexit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Atexit(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "tsvEmnenz48",
ExportName = "__cxa_atexit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int CxaAtexit(CpuContext ctx)
{
var destructorFunction = ctx[CpuRegister.Rdi];
var destructorArgument = ctx[CpuRegister.Rsi];
var moduleHandle = ctx[CpuRegister.Rdx];
if (destructorFunction == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
lock (_cxaGate)
{
_cxaDestructors.Add(new CxaDestructorEntry(
destructorFunction,
destructorArgument,
moduleHandle));
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "H2e8t5ScQGc",
ExportName = "__cxa_finalize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int CxaFinalize(CpuContext ctx)
{
var moduleHandle = ctx[CpuRegister.Rdi];
lock (_cxaGate)
{
if (moduleHandle == 0)
{
_cxaDestructors.Clear();
}
else
{
for (var i = _cxaDestructors.Count - 1; i >= 0; i--)
{
if (_cxaDestructors[i].ModuleHandle == moduleHandle)
{
_cxaDestructors.RemoveAt(i);
}
}
}
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "kbw4UHHSYy0",
ExportName = "__pthread_cxa_finalize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadCxaFinalize(CpuContext ctx)
{
_ = ctx;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "6Z83sYWFlA8",
ExportName = "_exit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int UnderscoreExit(CpuContext ctx)
{
_ = ctx;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "Ac86z8q7T8A",
ExportName = "sceKernelExitSblock",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelExitSblock(CpuContext ctx)
{
_ = ctx;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "6UgtwV+0zb4",
ExportName = "scePthreadCreate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadCreate(CpuContext ctx)
{
var threadIdAddress = ctx[CpuRegister.Rdi];
var nextThreadId = unchecked((ulong)Interlocked.Increment(ref _nextThreadId));
if (threadIdAddress != 0 && !ctx.TryWriteUInt64(threadIdAddress, nextThreadId))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "OxhIB8LB-PQ",
ExportName = "pthread_create",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadCreate(CpuContext ctx)
{
return PthreadCreate(ctx);
}
[SysAbiExport(
Nid = "onNY9Byn-W8",
ExportName = "scePthreadJoin",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadJoin(CpuContext ctx)
{
var returnValueAddress = ctx[CpuRegister.Rsi];
if (returnValueAddress != 0 && !ctx.TryWriteUInt64(returnValueAddress, 0))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "h9CcP3J0oVM",
ExportName = "pthread_join",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadJoin(CpuContext ctx)
{
return PthreadJoin(ctx);
}
[SysAbiExport(
Nid = "wuCroIGjt2g",
ExportName = "open",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int Open(CpuContext ctx)
{
ctx[CpuRegister.Rax] = unchecked((ulong)Interlocked.Increment(ref _nextFileDescriptor));
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "1G3lF1Gg1k8",
ExportName = "sceKernelOpen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelOpen(CpuContext ctx)
{
ctx[CpuRegister.Rax] = unchecked((ulong)Interlocked.Increment(ref _nextFileDescriptor));
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "hcuQgD53UxM",
ExportName = "printf",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Printf(CpuContext ctx)
{
ulong fmtPtr = ctx[CpuRegister.Rdi];
string fmt = ReadCString(ctx, fmtPtr, 4096);
ulong[] args =
{
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++)
{
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;
}
}
string outStr = sb.ToString();
Console.WriteLine($"[DEBUG][PRINF] {outStr}");
ctx[CpuRegister.Rax] = (ulong)outStr.Length;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "EMutwaQ34Jo",
ExportName = "perror",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Perror(CpuContext ctx)
{
ulong sPtr = ctx[CpuRegister.Rdi];
string msg;
if (sPtr == 0)
{
msg = "perror(NULL)";
}
else
{
msg = ReadCString(ctx, sPtr, 2048);
msg = $"perror(\"{msg}\")";
}
Console.WriteLine(msg);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static string ReadCString(CpuContext ctx, ulong address, int maxLen)
{
Span<byte> buf = stackalloc byte[maxLen];
if (!ctx.Memory.TryRead(address, buf))
return $"<unreadable 0x{address:X16}>";
int len = 0;
while (len < buf.Length && buf[len] != 0) len++;
try { return System.Text.Encoding.UTF8.GetString(buf.Slice(0, len)); }
catch { return System.Text.Encoding.ASCII.GetString(buf.Slice(0, len)); }
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,347 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Linq;
using System.Collections.Generic;
using System.IO;
namespace SharpEmu.Libs.Kernel;
public static class KernelModuleRegistry
{
private static readonly object _gate = new();
private static readonly Dictionary<int, ModuleEntry> _modulesByHandle = new();
private static readonly Dictionary<string, int> _handleByPath = new(StringComparer.OrdinalIgnoreCase);
private static readonly Dictionary<string, int> _handleByName = new(StringComparer.OrdinalIgnoreCase);
private static readonly Dictionary<int, int> _sysmoduleHandleById = new();
private static int _nextHandle = 1;
private static readonly Dictionary<int, string[]> KnownSysmoduleNames = new()
{
[0x0006] = new[] { "libSceFiber.sprx", "libSceFiber.prx" },
[0x0021] = new[] { "libSceRudp.sprx", "libSceRudp.prx" },
[0x0095] = new[] { "libSceIme.sprx", "libSceIme.prx" },
[0x0096] = new[] { "libSceImeDialog.sprx", "libSceImeDialog.prx" },
[0x00A9] = new[] { "libSceMouse.sprx", "libSceMouse.prx" },
};
public readonly record struct ModuleEntry(
int Handle,
string Name,
string Path,
ulong BaseAddress,
ulong EndAddress,
ulong EntryPoint,
bool IsMain,
bool IsSystemModule);
public static void Reset()
{
lock (_gate)
{
_modulesByHandle.Clear();
_handleByPath.Clear();
_handleByName.Clear();
_sysmoduleHandleById.Clear();
_nextHandle = 1;
}
}
public static int RegisterModule(
string? modulePath,
ulong baseAddress,
ulong size,
ulong entryPoint,
bool isMain,
bool isSystemModule = false)
{
var normalizedPath = NormalizePath(modulePath);
lock (_gate)
{
if (!string.IsNullOrWhiteSpace(normalizedPath) &&
_handleByPath.TryGetValue(normalizedPath, out var existingHandle) &&
_modulesByHandle.TryGetValue(existingHandle, out var existing))
{
var updated = existing with
{
BaseAddress = baseAddress,
EndAddress = ComputeEnd(baseAddress, size),
EntryPoint = entryPoint,
IsMain = existing.IsMain || isMain,
IsSystemModule = existing.IsSystemModule || isSystemModule,
};
_modulesByHandle[existingHandle] = updated;
_handleByName[updated.Name] = existingHandle;
return existingHandle;
}
var handle = _nextHandle++;
var name = ResolveName(normalizedPath, handle);
var entry = new ModuleEntry(
Handle: handle,
Name: name,
Path: normalizedPath,
BaseAddress: baseAddress,
EndAddress: ComputeEnd(baseAddress, size),
EntryPoint: entryPoint,
IsMain: isMain,
IsSystemModule: isSystemModule);
_modulesByHandle[handle] = entry;
if (!string.IsNullOrWhiteSpace(normalizedPath))
{
_handleByPath[normalizedPath] = handle;
}
_handleByName[name] = handle;
return handle;
}
}
public static int RegisterSyntheticModule(string moduleName, bool isSystemModule)
{
lock (_gate)
{
if (TryResolveHandleByNameLocked(moduleName, out var existingHandle))
{
var existing = _modulesByHandle[existingHandle];
_modulesByHandle[existingHandle] = existing with { IsSystemModule = existing.IsSystemModule || isSystemModule };
return existingHandle;
}
var handle = _nextHandle++;
var fallbackName = string.IsNullOrWhiteSpace(moduleName)
? $"module_{handle:X4}.sprx"
: moduleName;
var entry = new ModuleEntry(
Handle: handle,
Name: fallbackName,
Path: string.Empty,
BaseAddress: 0,
EndAddress: 0,
EntryPoint: 0,
IsMain: false,
IsSystemModule: isSystemModule);
_modulesByHandle[handle] = entry;
_handleByName[fallbackName] = handle;
return handle;
}
}
public static int MarkSysmoduleLoaded(int sysmoduleId)
{
lock (_gate)
{
if (_sysmoduleHandleById.TryGetValue(sysmoduleId, out var existingHandle) &&
_modulesByHandle.ContainsKey(existingHandle))
{
return existingHandle;
}
var handle = TryResolveKnownSysmoduleHandleLocked(sysmoduleId, out var knownHandle)
? knownHandle
: RegisterSyntheticModule($"sysmodule_0x{sysmoduleId:X4}.sprx", isSystemModule: true);
_sysmoduleHandleById[sysmoduleId] = handle;
return handle;
}
}
public static void MarkSysmoduleUnloaded(int sysmoduleId)
{
lock (_gate)
{
_sysmoduleHandleById.Remove(sysmoduleId);
}
}
public static bool IsSysmoduleLoaded(int sysmoduleId)
{
lock (_gate)
{
return _sysmoduleHandleById.ContainsKey(sysmoduleId);
}
}
public static bool TryGetModuleByAddress(ulong address, out ModuleEntry module)
{
lock (_gate)
{
ModuleEntry? best = null;
foreach (var entry in _modulesByHandle.Values)
{
if (entry.BaseAddress == 0 || entry.EndAddress <= entry.BaseAddress)
{
continue;
}
if (address < entry.BaseAddress || address >= entry.EndAddress)
{
continue;
}
if (best is null || (entry.EndAddress - entry.BaseAddress) < (best.Value.EndAddress - best.Value.BaseAddress))
{
best = entry;
}
}
if (best is null)
{
module = default;
return false;
}
module = best.Value;
return true;
}
}
public static bool TryGetModuleByHandle(int handle, out ModuleEntry module)
{
lock (_gate)
{
return _modulesByHandle.TryGetValue(handle, out module);
}
}
public static bool TryFindByPathOrName(string? modulePathOrName, out ModuleEntry module)
{
module = default;
if (string.IsNullOrWhiteSpace(modulePathOrName))
{
return false;
}
var normalizedPath = NormalizePath(modulePathOrName);
var fileName = ResolveName(normalizedPath, 0);
lock (_gate)
{
if (!string.IsNullOrWhiteSpace(normalizedPath) &&
_handleByPath.TryGetValue(normalizedPath, out var handleByPath) &&
_modulesByHandle.TryGetValue(handleByPath, out module))
{
return true;
}
if (TryResolveHandleByNameLocked(fileName, out var handleByName) &&
_modulesByHandle.TryGetValue(handleByName, out module))
{
return true;
}
}
return false;
}
public static int[] GetModuleHandles(bool includeSystemModules)
{
lock (_gate)
{
return _modulesByHandle.Values
.Where(entry => includeSystemModules || !entry.IsSystemModule)
.OrderBy(entry => entry.Handle)
.Select(entry => entry.Handle)
.ToArray();
}
}
public static bool TryGetFirstModule(out ModuleEntry module)
{
lock (_gate)
{
if (_modulesByHandle.Count == 0)
{
module = default;
return false;
}
var first = _modulesByHandle.Values.OrderBy(entry => entry.Handle).First();
module = first;
return true;
}
}
private static bool TryResolveKnownSysmoduleHandleLocked(int sysmoduleId, out int handle)
{
handle = 0;
if (!KnownSysmoduleNames.TryGetValue(sysmoduleId, out var candidates))
{
return false;
}
foreach (var candidate in candidates)
{
if (TryResolveHandleByNameLocked(candidate, out handle))
{
var existing = _modulesByHandle[handle];
_modulesByHandle[handle] = existing with { IsSystemModule = true };
return true;
}
}
return false;
}
private static bool TryResolveHandleByNameLocked(string moduleName, out int handle)
{
handle = 0;
if (string.IsNullOrWhiteSpace(moduleName))
{
return false;
}
if (_handleByName.TryGetValue(moduleName, out handle))
{
return true;
}
var altName = moduleName.EndsWith(".sprx", StringComparison.OrdinalIgnoreCase)
? moduleName[..^5] + ".prx"
: (moduleName.EndsWith(".prx", StringComparison.OrdinalIgnoreCase)
? moduleName[..^4] + ".sprx"
: string.Empty);
return !string.IsNullOrWhiteSpace(altName) && _handleByName.TryGetValue(altName, out handle);
}
private static ulong ComputeEnd(ulong baseAddress, ulong size)
{
if (size == 0)
{
return baseAddress;
}
return unchecked(baseAddress + size);
}
private static string NormalizePath(string? modulePath)
{
if (string.IsNullOrWhiteSpace(modulePath))
{
return string.Empty;
}
try
{
return Path.GetFullPath(modulePath).Replace('\\', '/');
}
catch
{
return modulePath.Replace('\\', '/');
}
}
private static string ResolveName(string normalizedPath, int handleHint)
{
if (!string.IsNullOrWhiteSpace(normalizedPath))
{
var fileName = Path.GetFileName(normalizedPath);
if (!string.IsNullOrWhiteSpace(fileName))
{
return fileName;
}
}
return handleHint <= 0
? "module.sprx"
: $"module_{handleHint:X4}.sprx";
}
}
@@ -0,0 +1,815 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Threading;
namespace SharpEmu.Libs.Kernel;
public static class KernelPthreadCompatExports
{
private const int MutexTypeNormal = 0;
private const int MutexTypeRecursive = 1;
private const ulong SyntheticMutexHandleBase = 0x00006000_0000_0000;
private const ulong SyntheticMutexAttrHandleBase = 0x00006001_0000_0000;
private static readonly object _stateGate = new();
private static readonly Dictionary<ulong, PthreadMutexState> _mutexStates = new();
private static readonly Dictionary<ulong, PthreadMutexAttrState> _mutexAttrStates = new();
private static readonly Dictionary<ulong, PthreadCondState> _condStates = new();
private static readonly HashSet<ulong> _condAttrStates = new();
private static long _nextSyntheticThreadId = 1;
private static long _nextSyntheticMutexHandleId = 1;
private static long _nextSyntheticMutexAttrHandleId = 1;
[ThreadStatic]
private static ulong _currentThreadId;
private sealed class PthreadMutexState
{
public SemaphoreSlim Semaphore { get; } = new(1, 1);
public ulong OwnerThreadId { get; set; }
public int RecursionCount { get; set; }
public int Type { get; set; } = MutexTypeNormal;
public int Protocol { get; set; }
}
private sealed class PthreadCondState
{
public int PendingSignals { get; set; }
public int Waiters { get; set; }
}
private readonly record struct PthreadMutexAttrState(int Type, int Protocol);
[SysAbiExport(
Nid = "aI+OeCz8xrQ",
ExportName = "scePthreadSelf",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadSelf(CpuContext ctx)
{
var currentThreadId = GetCurrentThreadId();
ctx[CpuRegister.Rax] = currentThreadId;
TracePthreadSelf(ctx, currentThreadId);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "3PtV6p3QNX4",
ExportName = "scePthreadEqual",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadEqual(CpuContext ctx)
{
var left = ctx[CpuRegister.Rdi];
var right = ctx[CpuRegister.Rsi];
ctx[CpuRegister.Rax] = left == right ? 1UL : 0UL;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "T72hz6ffq08",
ExportName = "scePthreadYield",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadYield(CpuContext ctx)
{
_ = ctx;
Thread.Yield();
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "EI-5-jlq2dE",
ExportName = "scePthreadGetthreadid",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadGetthreadid(CpuContext ctx)
{
var currentThreadId = GetCurrentThreadId();
var outAddress = ctx[CpuRegister.Rdi];
if (outAddress != 0 && !ctx.TryWriteUInt64(outAddress, currentThreadId))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = currentThreadId;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "cmo1RIYva9o",
ExportName = "scePthreadMutexInit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadMutexInit(CpuContext ctx) => PthreadMutexInitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi]);
[SysAbiExport(
Nid = "2Of0f+3mhhE",
ExportName = "scePthreadMutexDestroy",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadMutexDestroy(CpuContext ctx) => PthreadMutexDestroyCore(ctx, ctx[CpuRegister.Rdi]);
[SysAbiExport(
Nid = "9UK1vLZQft4",
ExportName = "scePthreadMutexLock",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadMutexLock(CpuContext ctx) => PthreadMutexLockCore(ctx, ctx[CpuRegister.Rdi], tryOnly: false);
[SysAbiExport(
Nid = "upoVrzMHFeE",
ExportName = "scePthreadMutexTrylock",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadMutexTrylock(CpuContext ctx) => PthreadMutexLockCore(ctx, ctx[CpuRegister.Rdi], tryOnly: true);
[SysAbiExport(
Nid = "tn3VlD0hG60",
ExportName = "scePthreadMutexUnlock",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadMutexUnlock(CpuContext ctx) => PthreadMutexUnlockCore(ctx, ctx[CpuRegister.Rdi], requireOwner: true);
[SysAbiExport(
Nid = "ttHNfU+qDBU",
ExportName = "pthread_mutex_init",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadMutexInit(CpuContext ctx) => PthreadMutexInitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi]);
[SysAbiExport(
Nid = "ltCfaGr2JGE",
ExportName = "pthread_mutex_destroy",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadMutexDestroy(CpuContext ctx) => PthreadMutexDestroyCore(ctx, ctx[CpuRegister.Rdi]);
[SysAbiExport(
Nid = "7H0iTOciTLo",
ExportName = "pthread_mutex_lock",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadMutexLock(CpuContext ctx) => PthreadMutexLockCore(ctx, ctx[CpuRegister.Rdi], tryOnly: false);
[SysAbiExport(
Nid = "K-jXhbt2gn4",
ExportName = "pthread_mutex_trylock",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadMutexTrylock(CpuContext ctx) => PthreadMutexLockCore(ctx, ctx[CpuRegister.Rdi], tryOnly: true);
[SysAbiExport(
Nid = "2Z+PpY6CaJg",
ExportName = "pthread_mutex_unlock",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadMutexUnlock(CpuContext ctx) => PthreadMutexUnlockCore(ctx, ctx[CpuRegister.Rdi], requireOwner: true);
[SysAbiExport(
Nid = "F8bUHwAG284",
ExportName = "scePthreadMutexattrInit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadMutexattrInit(CpuContext ctx) => PthreadMutexattrInitCore(ctx, ctx[CpuRegister.Rdi]);
[SysAbiExport(
Nid = "smWEktiyyG0",
ExportName = "scePthreadMutexattrDestroy",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadMutexattrDestroy(CpuContext ctx) => PthreadMutexattrDestroyCore(ctx, ctx[CpuRegister.Rdi]);
[SysAbiExport(
Nid = "iMp8QpE+XO4",
ExportName = "scePthreadMutexattrSettype",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadMutexattrSettype(CpuContext ctx) => PthreadMutexattrSettypeCore(ctx, ctx[CpuRegister.Rdi], unchecked((int)ctx[CpuRegister.Rsi]));
[SysAbiExport(
Nid = "1FGvU0i9saQ",
ExportName = "scePthreadMutexattrSetprotocol",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadMutexattrSetprotocol(CpuContext ctx) => PthreadMutexattrSetprotocolCore(ctx, ctx[CpuRegister.Rdi], unchecked((int)ctx[CpuRegister.Rsi]));
[SysAbiExport(
Nid = "dQHWEsJtoE4",
ExportName = "pthread_mutexattr_init",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadMutexattrInit(CpuContext ctx) => PthreadMutexattrInitCore(ctx, ctx[CpuRegister.Rdi]);
[SysAbiExport(
Nid = "HF7lK46xzjY",
ExportName = "pthread_mutexattr_destroy",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadMutexattrDestroy(CpuContext ctx) => PthreadMutexattrDestroyCore(ctx, ctx[CpuRegister.Rdi]);
[SysAbiExport(
Nid = "mDmgMOGVUqg",
ExportName = "pthread_mutexattr_settype",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadMutexattrSettype(CpuContext ctx) => PthreadMutexattrSettypeCore(ctx, ctx[CpuRegister.Rdi], unchecked((int)ctx[CpuRegister.Rsi]));
[SysAbiExport(
Nid = "2Tb92quprl0",
ExportName = "scePthreadCondInit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadCondInit(CpuContext ctx) => PthreadCondInitCore(ctx[CpuRegister.Rdi]);
[SysAbiExport(
Nid = "g+PZd2hiacg",
ExportName = "scePthreadCondDestroy",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadCondDestroy(CpuContext ctx) => PthreadCondDestroyCore(ctx[CpuRegister.Rdi]);
[SysAbiExport(
Nid = "WKAXJ4XBPQ4",
ExportName = "scePthreadCondWait",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadCondWait(CpuContext ctx) => PthreadCondWaitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], timed: false);
[SysAbiExport(
Nid = "BmMjYxmew1w",
ExportName = "scePthreadCondTimedwait",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadCondTimedwait(CpuContext ctx) => PthreadCondWaitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], timed: true);
[SysAbiExport(
Nid = "kDh-NfxgMtE",
ExportName = "scePthreadCondSignal",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadCondSignal(CpuContext ctx) => PthreadCondSignalCore(ctx[CpuRegister.Rdi], broadcast: false);
[SysAbiExport(
Nid = "JGgj7Uvrl+A",
ExportName = "scePthreadCondBroadcast",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadCondBroadcast(CpuContext ctx) => PthreadCondSignalCore(ctx[CpuRegister.Rdi], broadcast: true);
[SysAbiExport(
Nid = "Op8TBGY5KHg",
ExportName = "pthread_cond_wait",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadCondWait(CpuContext ctx) => PthreadCondWaitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], timed: false);
[SysAbiExport(
Nid = "mkx2fVhNMsg",
ExportName = "pthread_cond_broadcast",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadCondBroadcast(CpuContext ctx) => PthreadCondSignalCore(ctx[CpuRegister.Rdi], broadcast: true);
[SysAbiExport(
Nid = "m5-2bsNfv7s",
ExportName = "scePthreadCondattrInit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadCondattrInit(CpuContext ctx)
{
var attrAddress = ctx[CpuRegister.Rdi];
if (attrAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
lock (_stateGate)
{
_condAttrStates.Add(attrAddress);
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "waPcxYiR3WA",
ExportName = "scePthreadCondattrDestroy",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadCondattrDestroy(CpuContext ctx)
{
var attrAddress = ctx[CpuRegister.Rdi];
if (attrAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
lock (_stateGate)
{
_condAttrStates.Remove(attrAddress);
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static int PthreadMutexInitCore(CpuContext ctx, ulong mutexAddress, ulong attrAddress)
{
if (mutexAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var attr = ResolveMutexAttrState(ctx, attrAddress);
var state = new PthreadMutexState
{
Type = attr.Type,
Protocol = attr.Protocol,
};
var syntheticHandle = AllocateSyntheticHandle(SyntheticMutexHandleBase, ref _nextSyntheticMutexHandleId);
lock (_stateGate)
{
_mutexStates[mutexAddress] = state;
_mutexStates[syntheticHandle] = state;
}
_ = ctx.TryWriteUInt64(mutexAddress, syntheticHandle);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static int PthreadMutexDestroyCore(CpuContext ctx, ulong mutexAddress)
{
if (mutexAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var resolvedAddress = ResolveMutexHandle(ctx, mutexAddress);
PthreadMutexState? state;
lock (_stateGate)
{
_mutexStates.TryGetValue(resolvedAddress, out state);
_mutexStates.Remove(resolvedAddress);
if (resolvedAddress != mutexAddress)
{
_mutexStates.Remove(mutexAddress);
}
}
if (state is null)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
state.Semaphore.Dispose();
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static int PthreadMutexLockCore(CpuContext ctx, ulong mutexAddress, bool tryOnly)
{
if (mutexAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var resolvedAddress = ResolveMutexHandle(ctx, mutexAddress);
PthreadMutexState state;
lock (_stateGate)
{
if (!_mutexStates.TryGetValue(resolvedAddress, out state!))
{
state = new PthreadMutexState();
_mutexStates[resolvedAddress] = state;
}
if (resolvedAddress != mutexAddress)
{
_mutexStates[mutexAddress] = state;
}
}
var currentThreadId = GetCurrentThreadId();
lock (state)
{
if (state.OwnerThreadId == currentThreadId)
{
if (state.Type == MutexTypeRecursive)
{
state.RecursionCount++;
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_ALREADY_EXISTS);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_ALREADY_EXISTS;
}
}
var acquired = true;
if (tryOnly)
{
acquired = state.Semaphore.Wait(0);
}
else
{
state.Semaphore.Wait();
}
if (!acquired)
{
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_ALREADY_EXISTS);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_ALREADY_EXISTS;
}
lock (state)
{
state.OwnerThreadId = currentThreadId;
state.RecursionCount = 1;
}
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static int PthreadMutexUnlockCore(CpuContext ctx, ulong mutexAddress, bool requireOwner)
{
if (mutexAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var resolvedAddress = ResolveMutexHandle(ctx, mutexAddress);
PthreadMutexState? state;
lock (_stateGate)
{
_mutexStates.TryGetValue(resolvedAddress, out state);
}
if (state is null)
{
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, null, GetCurrentThreadId(), (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
var currentThreadId = GetCurrentThreadId();
var shouldRelease = false;
lock (state)
{
if (state.RecursionCount <= 0)
{
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (requireOwner && state.OwnerThreadId != currentThreadId)
{
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
state.RecursionCount--;
if (state.RecursionCount == 0)
{
state.OwnerThreadId = 0;
shouldRelease = true;
}
}
if (shouldRelease)
{
try
{
state.Semaphore.Release();
}
catch (SemaphoreFullException)
{
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
}
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static int PthreadMutexattrInitCore(CpuContext ctx, ulong attrAddress)
{
if (attrAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var syntheticHandle = AllocateSyntheticHandle(SyntheticMutexAttrHandleBase, ref _nextSyntheticMutexAttrHandleId);
lock (_stateGate)
{
_mutexAttrStates[attrAddress] = new PthreadMutexAttrState(MutexTypeNormal, 0);
_mutexAttrStates[syntheticHandle] = new PthreadMutexAttrState(MutexTypeNormal, 0);
}
_ = ctx.TryWriteUInt64(attrAddress, syntheticHandle);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static int PthreadMutexattrDestroyCore(CpuContext ctx, ulong attrAddress)
{
if (attrAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var resolvedAddress = ResolveMutexAttrHandle(ctx, attrAddress);
lock (_stateGate)
{
_mutexAttrStates.Remove(resolvedAddress);
if (resolvedAddress != attrAddress)
{
_mutexAttrStates.Remove(attrAddress);
}
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static int PthreadMutexattrSettypeCore(CpuContext ctx, ulong attrAddress, int type)
{
if (attrAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var resolvedAddress = ResolveMutexAttrHandle(ctx, attrAddress);
lock (_stateGate)
{
if (!_mutexAttrStates.TryGetValue(resolvedAddress, out var state))
{
state = new PthreadMutexAttrState(MutexTypeNormal, 0);
}
_mutexAttrStates[resolvedAddress] = state with { Type = type };
if (resolvedAddress != attrAddress)
{
_mutexAttrStates[attrAddress] = _mutexAttrStates[resolvedAddress];
}
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static int PthreadMutexattrSetprotocolCore(CpuContext ctx, ulong attrAddress, int protocol)
{
if (attrAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var resolvedAddress = ResolveMutexAttrHandle(ctx, attrAddress);
lock (_stateGate)
{
if (!_mutexAttrStates.TryGetValue(resolvedAddress, out var state))
{
state = new PthreadMutexAttrState(MutexTypeNormal, 0);
}
_mutexAttrStates[resolvedAddress] = state with { Protocol = protocol };
if (resolvedAddress != attrAddress)
{
_mutexAttrStates[attrAddress] = _mutexAttrStates[resolvedAddress];
}
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static ulong ResolveMutexHandle(CpuContext ctx, ulong mutexAddress)
{
if (mutexAddress == 0)
{
return 0;
}
lock (_stateGate)
{
if (_mutexStates.ContainsKey(mutexAddress))
{
return mutexAddress;
}
}
if (ctx.TryReadUInt64(mutexAddress, out var pointedHandle) && pointedHandle != 0)
{
lock (_stateGate)
{
if (_mutexStates.ContainsKey(pointedHandle))
{
return pointedHandle;
}
}
}
return mutexAddress;
}
private static ulong ResolveMutexAttrHandle(CpuContext ctx, ulong attrAddress)
{
if (attrAddress == 0)
{
return 0;
}
lock (_stateGate)
{
if (_mutexAttrStates.ContainsKey(attrAddress))
{
return attrAddress;
}
}
if (ctx.TryReadUInt64(attrAddress, out var pointedHandle) && pointedHandle != 0)
{
lock (_stateGate)
{
if (_mutexAttrStates.ContainsKey(pointedHandle))
{
return pointedHandle;
}
}
}
return attrAddress;
}
private static PthreadMutexAttrState ResolveMutexAttrState(CpuContext ctx, ulong attrAddress)
{
if (attrAddress == 0)
{
return default;
}
var resolvedAddress = ResolveMutexAttrHandle(ctx, attrAddress);
lock (_stateGate)
{
return _mutexAttrStates.TryGetValue(resolvedAddress, out var state)
? state
: default;
}
}
private static ulong AllocateSyntheticHandle(ulong baseAddress, ref long nextId)
{
var id = unchecked((ulong)Interlocked.Increment(ref nextId));
return baseAddress + (id << 4);
}
private static int PthreadCondInitCore(ulong condAddress)
{
if (condAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
lock (_stateGate)
{
_condStates[condAddress] = new PthreadCondState();
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static int PthreadCondDestroyCore(ulong condAddress)
{
if (condAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
lock (_stateGate)
{
_condStates.Remove(condAddress);
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static int PthreadCondWaitCore(CpuContext ctx, ulong condAddress, ulong mutexAddress, bool timed)
{
if (condAddress == 0 || mutexAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
lock (_stateGate)
{
if (!_condStates.TryGetValue(condAddress, out var state))
{
state = new PthreadCondState();
_condStates[condAddress] = state;
}
state.Waiters++;
if (state.PendingSignals > 0)
{
state.PendingSignals--;
state.Waiters--;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}
var unlockResult = PthreadMutexUnlockCore(ctx, mutexAddress, requireOwner: true);
if (unlockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
return unlockResult;
}
if (timed)
{
Thread.Sleep(1);
}
else
{
Thread.Yield();
}
var lockResult = PthreadMutexLockCore(ctx, mutexAddress, tryOnly: false);
lock (_stateGate)
{
if (_condStates.TryGetValue(condAddress, out var state))
{
state.Waiters = Math.Max(0, state.Waiters - 1);
}
}
return lockResult;
}
private static int PthreadCondSignalCore(ulong condAddress, bool broadcast)
{
if (condAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
lock (_stateGate)
{
if (!_condStates.TryGetValue(condAddress, out var state))
{
state = new PthreadCondState();
_condStates[condAddress] = state;
}
if (broadcast)
{
state.PendingSignals += Math.Max(1, state.Waiters);
}
else
{
state.PendingSignals++;
}
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static ulong GetCurrentThreadId()
{
if (_currentThreadId != 0)
{
return _currentThreadId;
}
_currentThreadId = unchecked((ulong)Interlocked.Increment(ref _nextSyntheticThreadId));
return _currentThreadId;
}
private static void TracePthreadSelf(CpuContext ctx, ulong currentThreadId)
{
if (!ShouldTracePthread())
{
return;
}
Console.Error.WriteLine(
$"[LOADER][TRACE] pthread_self: stale_rdi=0x{ctx[CpuRegister.Rdi]:X16} thread=0x{currentThreadId:X16}");
}
private static void TracePthreadMutex(CpuContext ctx, string operation, ulong mutexAddress, ulong resolvedAddress, PthreadMutexState? state, ulong currentThreadId, int result)
{
if (!ShouldTracePthread())
{
return;
}
_ = ctx.TryReadUInt64(mutexAddress, out var guestWord0);
_ = ctx.TryReadUInt64(mutexAddress + 8, out var guestWord1);
Console.Error.WriteLine(
$"[LOADER][TRACE] pthread_{operation}: mutex=0x{mutexAddress:X16} resolved=0x{resolvedAddress:X16} " +
$"guest[0]=0x{guestWord0:X16} guest[8]=0x{guestWord1:X16} " +
$"current=0x{currentThreadId:X16} owner=0x{(state?.OwnerThreadId ?? 0):X16} " +
$"recursion={(state?.RecursionCount ?? 0)} type={(state?.Type ?? 0)} result=0x{unchecked((uint)result):X8}");
}
private static bool ShouldTracePthread()
{
return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal);
}
}
@@ -0,0 +1,902 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Text;
using System.Threading;
namespace SharpEmu.Libs.Kernel;
public static class KernelPthreadExtendedCompatExports
{
private const int DefaultThreadPriority = 700;
private const ulong DefaultThreadAffinityMask = ulong.MaxValue;
private const int DefaultDetachState = 0;
private const ulong DefaultGuardSize = 0x1000UL;
private const ulong DefaultStackSize = 0x1_00000UL;
private const int DefaultInheritSched = 0;
private const int DefaultSchedPolicy = 0;
private const int DefaultSchedPriority = 0;
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<int, TlsKeyState> _tlsKeys = new();
private static int _nextTlsKey = 1;
[ThreadStatic]
private static Dictionary<int, ulong>? _threadLocalSpecific;
private sealed class ThreadState
{
public string Name { get; set; } = string.Empty;
public int Priority { get; set; } = DefaultThreadPriority;
public ulong AffinityMask { get; set; } = DefaultThreadAffinityMask;
public int DetachState { get; set; } = DefaultDetachState;
public PthreadAttrState Attributes { get; set; } = PthreadAttrState.Default;
}
private readonly record struct TlsKeyState(ulong Destructor);
private readonly record struct PthreadAttrState(
ulong AffinityMask,
int DetachState,
ulong StackAddress,
ulong StackSize,
ulong GuardSize,
int InheritSched,
int SchedPolicy,
int SchedPriority)
{
public static PthreadAttrState Default =>
new(
DefaultThreadAffinityMask,
DefaultDetachState,
0,
DefaultStackSize,
DefaultGuardSize,
DefaultInheritSched,
DefaultSchedPolicy,
DefaultSchedPriority);
}
[SysAbiExport(
Nid = "4qGrR6eoP9Y",
ExportName = "scePthreadDetach",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadDetach(CpuContext ctx)
{
var thread = ctx[CpuRegister.Rdi];
if (thread == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
lock (_stateGate)
{
var state = GetOrCreateThreadStateLocked(thread);
state.DetachState = 1;
state.Attributes = state.Attributes with { DetachState = 1 };
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "How7B8Oet6k",
ExportName = "scePthreadGetname",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadGetname(CpuContext ctx)
{
var thread = ctx[CpuRegister.Rdi];
var outNameAddress = ctx[CpuRegister.Rsi];
if (thread == 0 || outNameAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
string name;
lock (_stateGate)
{
name = GetOrCreateThreadStateLocked(thread).Name;
}
if (!TryWriteFixedUtf8CString(ctx, outNameAddress, name, 32))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "bt3CTBKmGyI",
ExportName = "scePthreadSetaffinity",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadSetaffinity(CpuContext ctx)
{
var thread = ctx[CpuRegister.Rdi];
var mask = ctx[CpuRegister.Rsi];
if (thread == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
lock (_stateGate)
{
var state = GetOrCreateThreadStateLocked(thread);
state.AffinityMask = mask;
state.Attributes = state.Attributes with { AffinityMask = mask };
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "1tKyG7RlMJo",
ExportName = "scePthreadGetprio",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadGetprio(CpuContext ctx)
{
var thread = ctx[CpuRegister.Rdi];
var outPriorityAddress = ctx[CpuRegister.Rsi];
if (thread == 0 || outPriorityAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
int priority;
lock (_stateGate)
{
priority = GetOrCreateThreadStateLocked(thread).Priority;
}
if (!TryWriteInt32(ctx, outPriorityAddress, priority))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = unchecked((uint)priority);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "W0Hpm2X0uPE",
ExportName = "scePthreadSetprio",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadSetprio(CpuContext ctx)
{
var thread = ctx[CpuRegister.Rdi];
var priority = unchecked((int)ctx[CpuRegister.Rsi]);
if (thread == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
lock (_stateGate)
{
GetOrCreateThreadStateLocked(thread).Priority = priority;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "nsYoNRywwNg",
ExportName = "scePthreadAttrInit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadAttrInit(CpuContext ctx)
{
var attrAddress = ctx[CpuRegister.Rdi];
if (attrAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
lock (_stateGate)
{
_attrStates[attrAddress] = PthreadAttrState.Default;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "62KCwEMmzcM",
ExportName = "scePthreadAttrDestroy",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadAttrDestroy(CpuContext ctx)
{
var attrAddress = ctx[CpuRegister.Rdi];
if (attrAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
lock (_stateGate)
{
_attrStates.Remove(attrAddress);
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "x1X76arYMxU",
ExportName = "scePthreadAttrGet",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadAttrGet(CpuContext ctx)
{
var thread = ctx[CpuRegister.Rdi];
var outAttrAddress = ctx[CpuRegister.Rsi];
if (thread == 0 || outAttrAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
lock (_stateGate)
{
var threadState = GetOrCreateThreadStateLocked(thread);
_attrStates[outAttrAddress] = threadState.Attributes;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "8+s5BzZjxSg",
ExportName = "scePthreadAttrGetaffinity",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadAttrGetaffinity(CpuContext ctx)
{
var attrAddress = ctx[CpuRegister.Rdi];
var outMaskAddress = ctx[CpuRegister.Rsi];
if (attrAddress == 0 || outMaskAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
PthreadAttrState state;
lock (_stateGate)
{
state = GetOrCreateAttrStateLocked(attrAddress);
}
if (!ctx.TryWriteUInt64(outMaskAddress, state.AffinityMask))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = state.AffinityMask;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "JaRMy+QcpeU",
ExportName = "scePthreadAttrGetdetachstate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadAttrGetdetachstate(CpuContext ctx)
{
var attrAddress = ctx[CpuRegister.Rdi];
var outStateAddress = ctx[CpuRegister.Rsi];
if (attrAddress == 0 || outStateAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
PthreadAttrState state;
lock (_stateGate)
{
state = GetOrCreateAttrStateLocked(attrAddress);
}
if (!TryWriteInt32(ctx, outStateAddress, state.DetachState))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = unchecked((uint)state.DetachState);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "txHtngJ+eyc",
ExportName = "scePthreadAttrGetguardsize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadAttrGetguardsize(CpuContext ctx)
{
var attrAddress = ctx[CpuRegister.Rdi];
var outGuardSizeAddress = ctx[CpuRegister.Rsi];
if (attrAddress == 0 || outGuardSizeAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
PthreadAttrState state;
lock (_stateGate)
{
state = GetOrCreateAttrStateLocked(attrAddress);
}
if (!ctx.TryWriteUInt64(outGuardSizeAddress, state.GuardSize))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = state.GuardSize;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "Ru36fiTtJzA",
ExportName = "scePthreadAttrGetstackaddr",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadAttrGetstackaddr(CpuContext ctx)
{
var attrAddress = ctx[CpuRegister.Rdi];
var outStackAddressPointer = ctx[CpuRegister.Rsi];
if (attrAddress == 0 || outStackAddressPointer == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
PthreadAttrState state;
lock (_stateGate)
{
state = GetOrCreateAttrStateLocked(attrAddress);
}
if (!ctx.TryWriteUInt64(outStackAddressPointer, state.StackAddress))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = state.StackAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "-fA+7ZlGDQs",
ExportName = "scePthreadAttrGetstacksize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadAttrGetstacksize(CpuContext ctx)
{
var attrAddress = ctx[CpuRegister.Rdi];
var outStackSizeAddress = ctx[CpuRegister.Rsi];
if (attrAddress == 0 || outStackSizeAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
PthreadAttrState state;
lock (_stateGate)
{
state = GetOrCreateAttrStateLocked(attrAddress);
}
if (!ctx.TryWriteUInt64(outStackSizeAddress, state.StackSize))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = state.StackSize;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "3qxgM4ezETA",
ExportName = "scePthreadAttrSetaffinity",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadAttrSetaffinity(CpuContext ctx)
{
var attrAddress = ctx[CpuRegister.Rdi];
var mask = ctx[CpuRegister.Rsi];
if (attrAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
lock (_stateGate)
{
var state = GetOrCreateAttrStateLocked(attrAddress);
_attrStates[attrAddress] = state with { AffinityMask = mask };
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "-Wreprtu0Qs",
ExportName = "scePthreadAttrSetdetachstate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadAttrSetdetachstate(CpuContext ctx)
{
var attrAddress = ctx[CpuRegister.Rdi];
var detachState = unchecked((int)ctx[CpuRegister.Rsi]);
if (attrAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
lock (_stateGate)
{
var state = GetOrCreateAttrStateLocked(attrAddress);
_attrStates[attrAddress] = state with { DetachState = detachState };
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "El+cQ20DynU",
ExportName = "scePthreadAttrSetguardsize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadAttrSetguardsize(CpuContext ctx)
{
var attrAddress = ctx[CpuRegister.Rdi];
var guardSize = ctx[CpuRegister.Rsi];
if (attrAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
lock (_stateGate)
{
var state = GetOrCreateAttrStateLocked(attrAddress);
_attrStates[attrAddress] = state with { GuardSize = guardSize };
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "eXbUSpEaTsA",
ExportName = "scePthreadAttrSetinheritsched",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadAttrSetinheritsched(CpuContext ctx)
{
var attrAddress = ctx[CpuRegister.Rdi];
var inheritSched = unchecked((int)ctx[CpuRegister.Rsi]);
if (attrAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
lock (_stateGate)
{
var state = GetOrCreateAttrStateLocked(attrAddress);
_attrStates[attrAddress] = state with { InheritSched = inheritSched };
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "DzES9hQF4f4",
ExportName = "scePthreadAttrSetschedparam",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadAttrSetschedparam(CpuContext ctx)
{
var attrAddress = ctx[CpuRegister.Rdi];
var schedParamAddress = ctx[CpuRegister.Rsi];
if (attrAddress == 0 || schedParamAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryReadInt32(ctx, schedParamAddress, out var schedPriority))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
lock (_stateGate)
{
var state = GetOrCreateAttrStateLocked(attrAddress);
_attrStates[attrAddress] = state with { SchedPriority = schedPriority };
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "4+h9EzwKF4I",
ExportName = "scePthreadAttrSetschedpolicy",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadAttrSetschedpolicy(CpuContext ctx)
{
var attrAddress = ctx[CpuRegister.Rdi];
var policy = unchecked((int)ctx[CpuRegister.Rsi]);
if (attrAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
lock (_stateGate)
{
var state = GetOrCreateAttrStateLocked(attrAddress);
_attrStates[attrAddress] = state with { SchedPolicy = policy };
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "UTXzJbWhhTE",
ExportName = "scePthreadAttrSetstacksize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadAttrSetstacksize(CpuContext ctx)
{
var attrAddress = ctx[CpuRegister.Rdi];
var stackSize = ctx[CpuRegister.Rsi];
if (attrAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
lock (_stateGate)
{
var state = GetOrCreateAttrStateLocked(attrAddress);
_attrStates[attrAddress] = state with { StackSize = stackSize };
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "6ULAa0fq4jA",
ExportName = "scePthreadRwlockInit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadRwlockInit(CpuContext ctx)
{
var rwlockAddress = ctx[CpuRegister.Rdi];
if (rwlockAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
lock (_stateGate)
{
if (_rwlockStates.Remove(rwlockAddress, out var existing))
{
existing.Dispose();
}
_rwlockStates[rwlockAddress] = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "BB+kb08Tl9A",
ExportName = "scePthreadRwlockDestroy",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadRwlockDestroy(CpuContext ctx)
{
var rwlockAddress = ctx[CpuRegister.Rdi];
if (rwlockAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
ReaderWriterLockSlim? state;
lock (_stateGate)
{
_rwlockStates.Remove(rwlockAddress, out state);
}
if (state is null)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
state.Dispose();
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "Ox9i0c7L5w0",
ExportName = "scePthreadRwlockRdlock",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadRwlockRdlock(CpuContext ctx) => PthreadRwlockLockCore(ctx[CpuRegister.Rdi], write: false);
[SysAbiExport(
Nid = "mqdNorrB+gI",
ExportName = "scePthreadRwlockWrlock",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadRwlockWrlock(CpuContext ctx) => PthreadRwlockLockCore(ctx[CpuRegister.Rdi], write: true);
[SysAbiExport(
Nid = "+L98PIbGttk",
ExportName = "scePthreadRwlockUnlock",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadRwlockUnlock(CpuContext ctx)
{
var rwlockAddress = ctx[CpuRegister.Rdi];
if (rwlockAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
ReaderWriterLockSlim? rwlock;
lock (_stateGate)
{
_rwlockStates.TryGetValue(rwlockAddress, out rwlock);
}
if (rwlock is null)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
try
{
if (rwlock.IsWriteLockHeld)
{
rwlock.ExitWriteLock();
}
else if (rwlock.IsReadLockHeld)
{
rwlock.ExitReadLock();
}
else
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
}
catch (SynchronizationLockException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "mqULNdimTn0",
ExportName = "pthread_key_create",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadKeyCreate(CpuContext ctx)
{
var outKeyAddress = ctx[CpuRegister.Rdi];
var destructor = ctx[CpuRegister.Rsi];
if (outKeyAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
int key;
lock (_stateGate)
{
while (_tlsKeys.ContainsKey(_nextTlsKey))
{
_nextTlsKey++;
}
key = _nextTlsKey++;
_tlsKeys[key] = new TlsKeyState(destructor);
}
if (!TryWriteInt32(ctx, outKeyAddress, key))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = unchecked((uint)key);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "6BpEZuDT7YI",
ExportName = "pthread_key_delete",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadKeyDelete(CpuContext ctx)
{
var key = unchecked((int)ctx[CpuRegister.Rdi]);
lock (_stateGate)
{
if (!_tlsKeys.Remove(key))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
}
_threadLocalSpecific?.Remove(key);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "WrOLvHU0yQM",
ExportName = "pthread_setspecific",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadSetspecific(CpuContext ctx)
{
var key = unchecked((int)ctx[CpuRegister.Rdi]);
var value = ctx[CpuRegister.Rsi];
lock (_stateGate)
{
if (!_tlsKeys.TryGetValue(key, out _))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
}
_threadLocalSpecific ??= new Dictionary<int, ulong>();
_threadLocalSpecific[key] = value;
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "0-KXaS70xy4",
ExportName = "pthread_getspecific",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadGetspecific(CpuContext ctx)
{
var key = unchecked((int)ctx[CpuRegister.Rdi]);
lock (_stateGate)
{
if (!_tlsKeys.TryGetValue(key, out _))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}
ctx[CpuRegister.Rax] =
_threadLocalSpecific is not null && _threadLocalSpecific.TryGetValue(key, out var value)
? value
: 0UL;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static int PthreadRwlockLockCore(ulong rwlockAddress, bool write)
{
if (rwlockAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
ReaderWriterLockSlim rwlock;
lock (_stateGate)
{
if (!_rwlockStates.TryGetValue(rwlockAddress, out rwlock!))
{
rwlock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
_rwlockStates[rwlockAddress] = rwlock;
}
}
try
{
if (write)
{
rwlock.EnterWriteLock();
}
else
{
rwlock.EnterReadLock();
}
}
catch (LockRecursionException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_ALREADY_EXISTS;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static ThreadState GetOrCreateThreadStateLocked(ulong thread)
{
if (_threadStates.TryGetValue(thread, out var state))
{
return state;
}
state = new ThreadState
{
Name = $"Thread-{thread:X}",
Priority = DefaultThreadPriority,
AffinityMask = DefaultThreadAffinityMask,
DetachState = DefaultDetachState,
Attributes = PthreadAttrState.Default,
};
_threadStates[thread] = state;
return state;
}
private static PthreadAttrState GetOrCreateAttrStateLocked(ulong attrAddress)
{
if (_attrStates.TryGetValue(attrAddress, out var state))
{
return state;
}
state = PthreadAttrState.Default;
_attrStates[attrAddress] = state;
return state;
}
private static bool TryWriteFixedUtf8CString(CpuContext ctx, ulong address, string value, int maxBytes)
{
if (maxBytes <= 0)
{
return false;
}
var utf8 = Encoding.UTF8.GetBytes(value);
var payloadLength = Math.Min(utf8.Length, maxBytes - 1);
var payload = new byte[payloadLength + 1];
utf8.AsSpan(0, payloadLength).CopyTo(payload);
payload[^1] = 0;
return ctx.Memory.TryWrite(address, payload);
}
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
{
Span<byte> bytes = stackalloc byte[sizeof(int)];
if (!ctx.Memory.TryRead(address, bytes))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
return true;
}
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
{
Span<byte> bytes = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
return ctx.Memory.TryWrite(address, bytes);
}
}
File diff suppressed because it is too large Load Diff
+14
View File
@@ -0,0 +1,14 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\SharpEmu.HLE\SharpEmu.HLE.csproj" />
</ItemGroup>
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
</Project>
@@ -0,0 +1,10 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.VideoOut;
public static class VideoOutExports
{
}
+10
View File
@@ -0,0 +1,10 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"sharpemu.hle": {
"type": "Project"
}
}
}
}
+159
View File
@@ -0,0 +1,159 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.IO;
namespace SharpEmu.Logging;
public sealed class ConsoleLogSink : ISharpEmuLogSink
{
private readonly object _sync = new();
public ConsoleLogSink(bool useColors = true, bool includeTimestamp = false)
{
UseColors = useColors;
IncludeTimestamp = includeTimestamp;
}
public bool UseColors { get; set; }
public bool IncludeTimestamp { get; set; }
public void Write(in LogEntry entry)
{
lock (_sync)
{
var writer = entry.Level >= LogLevel.Error ? Console.Error : Console.Out;
if (IncludeTimestamp)
{
writer.Write('[');
writer.Write(entry.Timestamp.ToString("HH:mm:ss.fff"));
writer.Write(']');
}
var levelLabel = ToLevelLabel(entry.Level);
WriteLevelSegment(writer, levelLabel, entry.Level);
writer.Write('[');
writer.Write(entry.Category);
writer.Write(']');
writer.Write(' ');
writer.Write(entry.SourceFileName);
if (entry.SourceLine > 0)
{
writer.Write(':');
writer.Write(entry.SourceLine);
}
writer.Write(' ');
WriteMessageSegment(writer, entry.Message);
writer.WriteLine();
if (entry.Exception is not null)
{
writer.WriteLine(entry.Exception);
}
}
}
private static string ToLevelLabel(LogLevel level)
{
return level switch
{
LogLevel.Trace => "TRACE",
LogLevel.Debug => "DEBUG",
LogLevel.Info => "INFO",
LogLevel.Warning => "WARNING",
LogLevel.Error => "ERROR",
LogLevel.Critical => "CRITICAL",
_ => "LOG",
};
}
private void WriteLevelSegment(TextWriter writer, string label, LogLevel level)
{
if (!UseColors)
{
writer.Write('[');
writer.Write(label);
writer.Write(']');
return;
}
var originalColor = Console.ForegroundColor;
Console.ForegroundColor = GetLevelColor(level);
writer.Write('[');
writer.Write(label);
writer.Write(']');
Console.ForegroundColor = originalColor;
}
private static ConsoleColor GetLevelColor(LogLevel level)
{
return level switch
{
LogLevel.Trace => ConsoleColor.DarkGray,
LogLevel.Debug => ConsoleColor.Gray,
LogLevel.Info => ConsoleColor.Blue,
LogLevel.Warning => ConsoleColor.Yellow,
LogLevel.Error => ConsoleColor.Red,
LogLevel.Critical => ConsoleColor.DarkRed,
_ => ConsoleColor.White,
};
}
private void WriteMessageSegment(TextWriter writer, string message)
{
if (!UseColors || !TryGetMessageColor(message, out var messageColor))
{
writer.Write(message);
return;
}
var originalColor = Console.ForegroundColor;
Console.ForegroundColor = messageColor;
writer.Write(message);
Console.ForegroundColor = originalColor;
}
private static bool TryGetMessageColor(string message, out ConsoleColor color)
{
if (string.IsNullOrWhiteSpace(message))
{
color = default;
return false;
}
if (ContainsIgnoreCase(message, "unresolved import") ||
ContainsIgnoreCase(message, "unresolved symbol") ||
ContainsIgnoreCase(message, "hot_unresolved_imports=") ||
ContainsIgnoreCase(message, "unresolved_imports_hit="))
{
color = ConsoleColor.Red;
return true;
}
if (ContainsIgnoreCase(message, "syscall") ||
ContainsIgnoreCase(message, "UnhandledSyscall"))
{
color = ConsoleColor.DarkYellow;
return true;
}
if (ContainsIgnoreCase(message, "Import trace") ||
ContainsIgnoreCase(message, "import_stub_return") ||
ContainsIgnoreCase(message, "last_import=") ||
ContainsIgnoreCase(message, "hot_imports=") ||
ContainsIgnoreCase(message, "resolved import"))
{
color = ConsoleColor.Blue;
return true;
}
color = default;
return false;
}
private static bool ContainsIgnoreCase(string text, string value)
{
return text.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0;
}
}
+9
View File
@@ -0,0 +1,9 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Logging;
public interface ISharpEmuLogSink
{
void Write(in LogEntry entry);
}
+14
View File
@@ -0,0 +1,14 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Logging;
public readonly record struct LogEntry(
DateTimeOffset Timestamp,
LogLevel Level,
string Category,
string Message,
string SourceFileName,
int SourceLine,
string SourceMemberName,
Exception? Exception = null);
+21
View File
@@ -0,0 +1,21 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Logging;
public enum LogLevel
{
Trace = 0,
Debug = 1,
Info = 2,
Warning = 3,
Error = 4,
Critical = 5,
None = 6,
}
@@ -0,0 +1,7 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
<Project Sdk="Microsoft.NET.Sdk">
</Project>
+168
View File
@@ -0,0 +1,168 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
using System.IO;
namespace SharpEmu.Logging;
public static class SharpEmuLog
{
private static readonly ConcurrentDictionary<string, SharpEmuLogger> LoggerByCategory =
new(StringComparer.Ordinal);
private static readonly object ConfigurationSync = new();
private static volatile LogLevel _minimumLevel = ResolveMinimumLevelFromEnvironment();
private static ISharpEmuLogSink _sink = new ConsoleLogSink(
useColors: ResolveColorEnabledFromEnvironment(),
includeTimestamp: false);
public static LogLevel MinimumLevel
{
get => _minimumLevel;
set => _minimumLevel = value;
}
public static ISharpEmuLogSink Sink
{
get
{
lock (ConfigurationSync)
{
return _sink;
}
}
set
{
ArgumentNullException.ThrowIfNull(value);
lock (ConfigurationSync)
{
_sink = value;
}
}
}
public static void Configure(LogLevel? minimumLevel = null, ISharpEmuLogSink? sink = null)
{
if (minimumLevel.HasValue)
{
_minimumLevel = minimumLevel.Value;
}
if (sink is not null)
{
Sink = sink;
}
}
public static SharpEmuLogger For(string category)
{
ArgumentException.ThrowIfNullOrWhiteSpace(category);
return LoggerByCategory.GetOrAdd(category, static key => new SharpEmuLogger(key));
}
public static bool TryParseLevel(string? text, out LogLevel level)
{
if (string.IsNullOrWhiteSpace(text))
{
level = default;
return false;
}
var normalized = text.Trim();
if (Enum.TryParse<LogLevel>(normalized, ignoreCase: true, out level))
{
return true;
}
if (string.Equals(normalized, "warn", StringComparison.OrdinalIgnoreCase))
{
level = LogLevel.Warning;
return true;
}
if (string.Equals(normalized, "fatal", StringComparison.OrdinalIgnoreCase))
{
level = LogLevel.Critical;
return true;
}
return false;
}
internal static bool IsEnabled(LogLevel level)
{
var minimum = _minimumLevel;
return minimum != LogLevel.None && level >= minimum;
}
internal static void Write(
LogLevel level,
string category,
string message,
Exception? exception,
string sourceFilePath,
int sourceLine,
string sourceMemberName)
{
if (!IsEnabled(level))
{
return;
}
var entry = new LogEntry(
DateTimeOffset.Now,
level,
category,
message,
Path.GetFileName(sourceFilePath),
sourceLine,
sourceMemberName,
exception);
ISharpEmuLogSink sink;
lock (ConfigurationSync)
{
sink = _sink;
}
sink.Write(in entry);
}
private static LogLevel ResolveMinimumLevelFromEnvironment()
{
var raw = Environment.GetEnvironmentVariable("SHARPEMU_LOG_LEVEL");
return TryParseLevel(raw, out var level) ? level : LogLevel.Info;
}
private static bool ResolveColorEnabledFromEnvironment()
{
if (Console.IsOutputRedirected)
{
return false;
}
var raw = Environment.GetEnvironmentVariable("SHARPEMU_LOG_NO_COLOR");
return !IsTrueLike(raw);
}
private static bool IsTrueLike(string? text)
{
if (string.IsNullOrWhiteSpace(text))
{
return false;
}
return text.Trim() switch
{
"1" => true,
"true" => true,
"TRUE" => true,
"yes" => true,
"YES" => true,
"on" => true,
"ON" => true,
_ => false,
};
}
}
+87
View File
@@ -0,0 +1,87 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.CompilerServices;
namespace SharpEmu.Logging;
public sealed class SharpEmuLogger
{
public SharpEmuLogger(string category)
{
Category = category ?? throw new ArgumentNullException(nameof(category));
}
public string Category { get; }
public bool IsEnabled(LogLevel level) => SharpEmuLog.IsEnabled(level);
public void Trace(
string message,
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLine = 0,
[CallerMemberName] string sourceMemberName = "")
=> Write(LogLevel.Trace, message, exception: null, sourceFilePath, sourceLine, sourceMemberName);
public void Debug(
string message,
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLine = 0,
[CallerMemberName] string sourceMemberName = "")
=> Write(LogLevel.Debug, message, exception: null, sourceFilePath, sourceLine, sourceMemberName);
public void Info(
string message,
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLine = 0,
[CallerMemberName] string sourceMemberName = "")
=> Write(LogLevel.Info, message, exception: null, sourceFilePath, sourceLine, sourceMemberName);
public void Warn(
string message,
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLine = 0,
[CallerMemberName] string sourceMemberName = "")
=> Warning(message, sourceFilePath, sourceLine, sourceMemberName);
public void Warning(
string message,
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLine = 0,
[CallerMemberName] string sourceMemberName = "")
=> Write(LogLevel.Warning, message, exception: null, sourceFilePath, sourceLine, sourceMemberName);
public void Error(
string message,
Exception? exception = null,
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLine = 0,
[CallerMemberName] string sourceMemberName = "")
=> Write(LogLevel.Error, message, exception, sourceFilePath, sourceLine, sourceMemberName);
public void Critical(
string message,
Exception? exception = null,
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLine = 0,
[CallerMemberName] string sourceMemberName = "")
=> Write(LogLevel.Critical, message, exception, sourceFilePath, sourceLine, sourceMemberName);
private void Write(
LogLevel level,
string message,
Exception? exception,
string sourceFilePath,
int sourceLine,
string sourceMemberName)
{
SharpEmuLog.Write(
level,
Category,
message,
exception,
sourceFilePath,
sourceLine,
sourceMemberName);
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"version": 2,
"dependencies": {
"net10.0": {}
}
}