Compare commits

...

17 Commits

Author SHA1 Message Date
ParantezTech ddfda7e220 [log] log to file for CLI and fix some issues 2026-07-13 14:30:30 +03:00
ParantezTech af106d4b38 reuse 2026-07-13 14:11:22 +03:00
ParantezTech 1dafc306b5 [emulator] Fix mitigated child process handling 2026-07-13 14:06:31 +03:00
ParantezTech f73c9e8c3f added contributing guidelines 2026-07-13 12:48:36 +03:00
Mike Saito 3d2c30b151 fix(core): lazy dlsym stub materialization, COW snapshots and deferred bootstrap logging (#94)
* fix(core): implement 4-tier lazy dlsym stub materialization and argument normalization

Enforce transactional and thread-safe resolution for standalone ELF bootstrapper pipelines. - Implement a 4-tier additive fallback cascade (T0: runtime symbols, T1: import entries scan, T2: Aerolib mapping, T3: runtime slack-pool lazy stub allocation at 0x7000_0000_0000). - Fix UnmanagedCallersOnly CLR runtime crashes on second bootstrap by adding NormalizeKernelDynlibDlsymArguments to detect and swap mirrored (symbol, handle) register inputs via rigorous pointer bounds verification. - Protect failure paths via CompleteKernelDynlibDlsymFailure, cleanly zero-filling target outputAddress buffers and returns Rax = -1 with zero managed logging execution in hot native paths.

* fix(core): harden lazy-stub diagnostics with COW snapshots and deferred bootstrap logging

Follow-up to lazy stub pool copy-on-write publishing in TryGetOrCreateLazyImportStub. - Snapshot _importEntries in ProbeReturnRip before near-call and PLT import lookup loops to prevent torn iteration during concurrent array replacement. - Defer SHARPEMU_LOG_BOOTSTRAP output: hot path records raw register slots in a ring buffer under lock; TryReadAsciiZ and Console.Error run only after import handler completion via DrainDeferredBootstrapTraces. - Normalize bootstrap dynlib register order at DispatchImport gateway entry before any logging or trace reads, so swapped RDI/RSI on Import#2 cannot fault the native gateway when bootstrap tracing is enabled. - Resolve lazy stub pool bounds from the full SelfLoader-mapped import region via VirtualQuery instead of a hardcoded 4 KiB cap. - Use ConcurrentDictionary for runtime symbol registration during concurrent dlsym. - Emit distinct [LOADER][WARN] reasons when import stub region resolution fails versus lazy stub pool exhaustion.
2026-07-13 12:25:37 +03:00
Berk 61d28e9e08 [CPU] optimize strcasecmp for hot path (#95) 2026-07-13 12:19:57 +03:00
Digote 4bd42795c7 [logging] Migrate HLE diagnostics to SharpEmuLog (#80)
Signed-off-by: Digote <45742711+Digote@users.noreply.github.com>
2026-07-13 12:11:27 +03:00
Mike Saito c03ca32a02 fix(vfs): harden getdirentries and APR filepath resolution (#77)
* fix(vfs): defer host cursor commit in getdirentries

Follow-up VFS hardening task applying the same guest-writes-first discipline established in the time subsystem to directory enumeration.

Deferred Commit in KernelGetdirentriesCore:

- Reordered guest output so the 512-byte dirent buffer is written first via TryWriteCompat, basep is updated second (when non-null) via TryWriteUInt64Compat, and directory.NextIndex is advanced only after both guest writes succeed.

- Removed the early basep write at method entry that could mutate guest memory before buffer validation and advance the host cursor before a successful dirent delivery, causing permanent entry loss on MEMORY_FAULT at bufferAddress.

- EOF handling: when currentIndex >= Entries.Length, write basep with the final offset and return 0 without mutating NextIndex, matching FreeBSD getdirentries(2) semantics and preventing infinite retry loops.

KernelGetdents path: basePointerAddress is passed as 0, so the transaction collapses to buffer write then host cursor advance with no basep side effect.

Out of scope: coalesced {id, size} writes in sceKernelAprResolveFilepathsToIdsAndFileSizes; NetCtl connected-state stubs.

Files: KernelMemoryCompatExports.cs

* fix(vfs): resolve-first bulk commit in APR filepath resolution

Refactored sceKernelAprResolveFilepathsToIdsAndFileSizes to stop writing ids and sizes into guest memory one element at a time.

- Removed the uint.MaxValue placeholder write at the start of each loop iteration.

- Path resolution and file size lookup now fill host-side buffers first; on EFAULT or NOT_FOUND the guest ids/sizes arrays are left untouched.

- ids and sizes are packed into contiguous byte buffers and written with one TryWriteCompat call per output array instead of separate TryWriteUInt32Compat / TryWriteUInt64Compat per index.

- AmprFileRegistry.Register is called only after guest writes succeed.

- AmprFileRegistry.ComputeFileId is internal so ids can be computed without registering paths during the resolve loop.

Files: KernelMemoryCompatExports.cs, AmprFileRegistry.cs
2026-07-13 12:10:20 +03:00
kuba 6e2878f2ff Add commit hash to video window title (#93) 2026-07-13 11:21:41 +03:00
ParantezTech 2e09b015bc Merge branch 'main' of https://github.com/par274/sharpemu 2026-07-13 01:10:08 +03:00
ParantezTech 7b9efd1539 [readme] update screenshot for dreaming sarah 2026-07-13 01:09:45 +03:00
Kaotic fbf2c2d00a [GUI] Add detachable console window (#91)
Add a Split button to detach the emulator console into a resizable window.

The detached console keeps search, auto-scroll, copy, and clear controls. The main console hides while detached and restores when the split window closes if it was previously open.
2026-07-13 00:16:21 +03:00
kuba 8c1507777c [agc] Reset transparent Chowdren effect-layer fills (#83)
Treat the exact untextured transparent-black premultiplied fill used by Chowdren as an overwrite. This prevents Dreaming Sarah fog and vignette render targets from accumulating across frames; SHARPEMU_DISABLE_TRANSPARENT_FILL_CLEAR=1 restores prior behavior.
2026-07-12 19:26:48 +03:00
Berk 8ebc43e758 [github] Fix placeholder formatting in game compatibility template (#84) 2026-07-12 18:46:03 +03:00
j92580498-max 5aadb7495a Libs: add libSceDiscMap HLE exports (ported from Kyty) (#79)
Port the libSceDiscMap stubs from Kyty (InoriRus/Kyty, MIT) into the
SysAbiExport model. Disc-installed titles probe these NIDs on most file
accesses to decide whether a read must be redirected to the disc drive;
answering that every request is already resident on internal storage
keeps I/O on the regular file system path instead of failing with
unresolved-import errors.

- sceDiscMapIsRequestOnHDD (lbQKqsERhtE): validates args, writes 1 to
  the result pointer, returns 0
- fJgP+wqifno / ioKMruft1ek: zero-fill the three output pointers,
  return 0 (names not present in ps5_names.txt; kept as descriptive
  Unknown exports like the existing sceKernelUnknown* convention)
- DISC_MAP_ERROR_INVALID_ARGUMENT (0x81100001) on null pointers,
  matching the documented libSceDiscMap error range
- optional tracing via SHARPEMU_LOG_DISCMAP=1

Co-authored-by: j92580498-max <252151737+j92580498-max@users.noreply.github.com>
2026-07-12 18:24:11 +03:00
Foued Attar cdee77521e Fix UnmanagedCallersOnly boot crash & Core Engine Improvements (CPU, HLE, AGC) (#81)
* [agc] WAIT_REG_MEM suspend/resume, draw packet fixes, new HLE exports, debug cleanup

Rebased onto upstream 79a7437 (par274/sharpemu, rewritten history).

- GpuWaitRegistry: DCBs suspended on unsatisfied WAIT_REG_MEM are re-polled
  against guest memory on every submit; fixed 64-bit and standard packet parse
  offsets, apply the mask, treat PM4 compare function 0 as "always".
- TryReadSubmittedDrawCount: accept the 5-dword ItDrawIndex2 form emitted by
  DcbDrawIndex (count at +4); menu draws were silently discarded before.
- sceAgcDriverSubmitMultiDcbs: reversed ABI (rdi=address array, rsi=dword
  sizes, rdx=count).
- VideoOut: vblank events, sceVideoOutGetFlipStatus, buffers registered via
  sceVideoOutRegisterBuffers are valid flip targets.
- New HLE: libc stdio (fopen/fread/fseek/ftell/fclose/fgets), Dinkumware
  _Getpctype ctype table, NpTrophy2 stubs, AMPR PAK sequential-read tracker,
  MsgDialog lifecycle, NGS2 alt NIDs + dummy vtable for handle objects,
  guarded memset intrinsic, abort()/strcasecmp null-arg recovery.
- Removed investigation-only code (INT3 breakpoints, qfont/mcpp dumps,
  error-candidate printf traces, unconditional debug logs).

First rendered frame: Quake (PPSA01880) presents a 1920x1080 guest frame.

* Implemented a guarded native intrinsic (rep movsb) in DirectExecutionBackend to bypass HLE dispatch overhead, while preserving memory safety checks.

* [hle] clock_gettime clock ids, AudioOut2 canary fix, NID rebinds, new offline stubs

- clock_gettime (lLMT9vJAck0): support CLOCK_SECOND and the *_PRECISE/*_FAST
  variants instead of returning EINVAL, which games treated as fatal and
  retried in a tight loop.
- AudioOut2: context param writes shrunk to the guest-observed layout (the
  old 0x80-byte reset smashed the stack canary at +0x60 and killed audio
  init); ContextQueryMemory writes the single u64 the caller expects.
- NGS2: dropped wrong alt-NID aliases (they hash to sceImeUpdate,
  sceMouseRead, sceSystemGestureUpdateAllTouchRecognizer - now bound in
  their real libraries); added sceNgs2PanInit; fixed VoiceGetState NIDs.
- New verified stubs: sceUltInitialize, sceNpUniversalDataSystemDestroyHandle,
  sceNpGetOnlineId, sceNpGetNpReachabilityState, sceImeKeyboardOpen,
  sceImeKeyboardGetResourceId, sceMouseOpen, sceKernelAprGetFileSize.
- Import gateway: unwind guest workers at dispatch during backend teardown;
  env-gated SHARPEMU_LOG_THREAD_MODE tracing.

* [cpu] Isolate guest execution on native worker threads

Guest entry stubs no longer run above CLR-managed frames: each run is handed
to a pooled raw OS thread whose loop is emitted native code. While guest code
executes there is not a single managed frame on the thread and it stays in
preemptive GC mode, so the GC never walks a frame chain interleaved with
guest stubs that carry no CLR unwind info (the ReversePInvokeBadTransition /
UnmanagedCallersOnly FailFast class of crashes on pumped guest threads).

- NativeGuestExecutor: CreateThread + emitted run loop (WaitForSingleObject,
  UnmanagedCallersOnly prologue/epilogue, entry stub call, SetEvent). The
  prologue rebinds guest TLS, the host-RSP slot, thread affinity and the
  Active* ambient per run, so workers carry no guest identity and pool
  freely; the orchestrating managed thread parks in a preemptive wait.
- All three entry sites route through RunGuestEntryStub: guest thread
  entries, blocked-continuation resumes, and the main ExecuteEntry.
- Teardown stops workers before any executable stub or TLS index they
  reference is freed; a worker that will not stop leaks its loop instead of
  freeing running code.
- Kill switch: SHARPEMU_DISABLE_NATIVE_GUEST_WORKERS=1 restores the inline
  calli path.
2026-07-12 18:04:12 +03:00
RedBlackAka d2fca37716 [GUI] Basic hotkey and full-screen support (#75) 2026-07-12 12:54:37 +03:00
37 changed files with 2720 additions and 344 deletions
@@ -88,7 +88,7 @@ body:
attributes:
label: Last Milestone / First Error
description: Paste the **last useful milestone line** or the **first `WARNING`, `ERROR`, or `CRITICAL` line connected to the stop** from the log. `DEBUG`/`TRACE` lines do not count unless a real error follows them.
placeholder: e.g. CpuEngine: native-only OR [ERROR] Native backend FAILED: ...
placeholder: "e.g. CpuEngine: native-only OR [ERROR] Native backend FAILED: ..."
validations:
required: true
Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 101 KiB

+70
View File
@@ -0,0 +1,70 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
# Contributing
Contributions are always welcome!
Before opening a pull request, please keep the following in mind:
- Keep PRs small and focused on a single topic.
- Discuss large architectural changes before implementing them.
- Follow the project's existing coding style.
- Do not submit generated code unless you fully understand and can maintain it.
- Do not introduce Sony proprietary code, firmware, keys, decrypted assets, or other copyrighted PlayStation materials.
- All reverse engineering should be based on publicly available information, clean-room techniques, or your own original research.
- Game-specific hacks should be avoided whenever possible. Prefer generic implementations that improve overall compatibility.
- New features should not break existing behavior.
- Ensure the project builds successfully before submitting a PR.
If you're unsure about a design decision, feel free to open a discussion or draft PR first.
## AI-Assisted Contributions
AI-assisted development is welcome and may be used for research, reverse engineering, code generation, or documentation.
However, contributors are expected to fully understand every line of code they submit. By opening a pull request, you confirm that you are able to explain, modify, debug, and maintain the submitted code without relying on the AI that generated it.
When submitting an AI-assisted PR:
- Clearly explain **what the change does**, **why it is needed**, and **what problem it solves**, using your own words.
- Describe **how you verified the change**, including the games, applications, or test cases used.
- Avoid excessive product-level logging. Use logging only when it provides meaningful diagnostic value.
- Comments should document design decisions or implementation details in your own words. Avoid generic AI-generated comments that merely restate what the code already does.
- Be prepared to answer review questions about the implementation. "The AI generated it" is not considered a sufficient explanation.
- Large AI-generated changes without a clear understanding of the implementation are unlikely to be accepted.
- If the implementation cannot be reasonably explained during code review, the pull request may be rejected regardless of whether it works.
The quality, correctness, maintainability, and long-term ownership of the submitted code remain the responsibility of the contributor.
## Coding Style
SharpEmu follows a consistent coding style across the project. Please ensure your contributions match the existing style.
- Use **4 spaces** for indentation (no tabs).
- Use **2 spaces** for XML-based files (e.g. `.csproj`, `.props`, `.targets`, `.xml`, `yml`, GitHub workflow files where applicable).
- Respect the project's `.editorconfig`.
- Ensure every text file ends with a **single trailing newline**
- Avoid formatting-only commits unless they are the purpose of the PR.
- Keep naming, formatting, and file organization consistent with the surrounding code.
- Prefer small, focused changes over large refactors.
### REUSE Compliance
This repository follows the REUSE Specification.
Every new file must contain the appropriate SPDX license header. Pull requests that do not comply with the project's REUSE requirements will fail CI and will not be merged.
### Recommended Development Environment
The repository includes an `.editorconfig` and Visual Studio solution files.
For the best experience, we recommend using:
- Visual Studio Code with;
- C#
- C# Dev Kit
Most editors that support `.editorconfig` will automatically apply the project's formatting rules.
+13
View File
@@ -122,3 +122,16 @@ Provided valuable references for filesystem handling and low-level C# implementa
# License
- [**GPL-2.0 license**](https://github.com/par274/sharpemu/blob/main/LICENSE)
## Contributing
Before opening an issue or pull request, please read our contribution guidelines:
**[CONTRIBUTING.md](./CONTRIBUTING.md)**
The guide covers:
- Coding style and formatting
- AI-assisted contributions
- Pull request expectations
- Testing guidelines
- Legal and reverse engineering policy
+372 -13
View File
@@ -8,12 +8,15 @@ using SharpEmu.HLE;
using SharpEmu.Logging;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
namespace SharpEmu.CLI;
internal static partial class Program
{
private static readonly SharpEmuLogger Log = SharpEmuLog.For("SharpEmu.CLI");
private static readonly object ConsoleMirrorSync = new();
private static StreamWriter? _consoleMirrorFile;
private const int DefaultImportTraceLimit = 32;
private const string MitigatedChildFlag = "--sharpemu-mitigated-child";
private const uint EXTENDED_STARTUPINFO_PRESENT = 0x00080000;
@@ -21,11 +24,15 @@ internal static partial class Program
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 int STARTF_USESTDHANDLES = 0x00000100;
private const uint HANDLE_FLAG_INHERIT = 0x00000001;
private const string MitigatedChildEnvironment = "SHARPEMU_MITIGATED_CHILD";
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 const int ATTACH_PARENT_PROCESS = -1;
private const int STD_INPUT_HANDLE = -10;
private const int STD_OUTPUT_HANDLE = -11;
private const int STD_ERROR_HANDLE = -12;
private const uint GENERIC_READ = 0x80000000;
@@ -43,6 +50,7 @@ internal static partial class Program
}
finally
{
DropConsoleFileMirror();
SharpEmuLog.Shutdown();
}
}
@@ -61,6 +69,10 @@ internal static partial class Program
// itself to a console before the first write.
EnsureCliConsole();
UseUtf8ConsoleOutput();
if (isMitigatedChild && TryGetLogFileArgument(args, out var earlyLogFilePath))
{
TryEnableConsoleFileMirror(earlyLogFilePath);
}
Console.Error.WriteLine($"[DEBUG] SharpEmu starting with {args.Length} args");
@@ -69,12 +81,17 @@ internal static partial class Program
return childExitCode;
}
if (!TryParseArguments(args, out var ebootPath, out var runtimeOptions, out var logLevel))
if (!TryParseArguments(args, out var ebootPath, out var runtimeOptions, out var logLevel, out var logFilePath))
{
PrintUsage();
return 1;
}
if (!isMitigatedChild && !string.IsNullOrWhiteSpace(logFilePath))
{
TryEnableConsoleFileMirror(logFilePath);
}
SharpEmuLog.MinimumLevel = logLevel;
Log.Info(BuildInfo.Banner);
@@ -234,6 +251,10 @@ internal static partial class Program
private static string[] NormalizeInternalArguments(string[] args, out bool isMitigatedChild)
{
isMitigatedChild = false;
var trustedMitigatedChild = string.Equals(
Environment.GetEnvironmentVariable(MitigatedChildEnvironment),
"1",
StringComparison.Ordinal);
if (args.Length == 0)
{
return args;
@@ -244,7 +265,7 @@ internal static partial class Program
{
if (string.Equals(arg, MitigatedChildFlag, StringComparison.Ordinal))
{
isMitigatedChild = true;
isMitigatedChild = trustedMitigatedChild;
continue;
}
@@ -283,9 +304,11 @@ internal static partial class Program
var commandLine = BuildCommandLine(processPath, childArgs);
var startupInfoEx = new STARTUPINFOEX();
startupInfoEx.StartupInfo.cb = Marshal.SizeOf<STARTUPINFOEX>();
ConfigureInheritedStdHandles(ref startupInfoEx.StartupInfo);
nint attributeList = 0;
nint mitigationPolicies = 0;
var previousChildEnvironment = Environment.GetEnvironmentVariable(MitigatedChildEnvironment);
try
{
nuint attributeListSize = 0;
@@ -293,7 +316,9 @@ internal static partial class Program
attributeList = Marshal.AllocHGlobal((nint)attributeListSize);
if (!InitializeProcThreadAttributeList(attributeList, 1, 0, ref attributeListSize))
{
return false;
childExitCode = 5;
Console.Error.WriteLine($"[ERROR] Failed to initialize mitigation attributes: {Marshal.GetLastWin32Error()}");
return true;
}
startupInfoEx.lpAttributeList = attributeList;
@@ -301,8 +326,7 @@ internal static partial class Program
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;
PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF;
mitigationPolicies = Marshal.AllocHGlobal(sizeof(ulong) * 2);
Marshal.WriteInt64(mitigationPolicies, unchecked((long)policy1));
@@ -317,24 +341,31 @@ internal static partial class Program
0,
0))
{
return false;
childExitCode = 5;
Console.Error.WriteLine($"[ERROR] Failed to apply mitigation attributes: {Marshal.GetLastWin32Error()}");
return true;
}
var cmdLineBuilder = new StringBuilder(commandLine);
nint jobHandle = 0;
if (!CreateProcessW(
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, "1");
var created = CreateProcessW(
processPath,
cmdLineBuilder,
0,
0,
false,
true,
EXTENDED_STARTUPINFO_PRESENT,
0,
Environment.CurrentDirectory,
ref startupInfoEx,
out var processInfo))
out var processInfo);
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, previousChildEnvironment);
if (!created)
{
return false;
childExitCode = 5;
Console.Error.WriteLine($"[ERROR] Failed to launch mitigated child process: {Marshal.GetLastWin32Error()}");
return true;
}
try
@@ -388,6 +419,8 @@ internal static partial class Program
}
finally
{
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, previousChildEnvironment);
if (attributeList != 0)
{
DeleteProcThreadAttributeList(attributeList);
@@ -401,6 +434,238 @@ internal static partial class Program
}
}
private static bool TryGetLogFileArgument(IReadOnlyList<string> args, out string path)
{
for (var i = 0; i < args.Count; i++)
{
var argument = args[i];
if (string.Equals(argument, "--log-file", StringComparison.OrdinalIgnoreCase))
{
if (i + 1 < args.Count &&
!string.IsNullOrWhiteSpace(args[i + 1]) &&
!args[i + 1].StartsWith("--", StringComparison.Ordinal) &&
ShouldConsumeLogFilePath(args, i + 1))
{
path = args[i + 1];
return true;
}
path = BuildDefaultLogFilePath(TryFindEbootPathToken(args));
return true;
}
const string logFilePrefix = "--log-file=";
if (argument.StartsWith(logFilePrefix, StringComparison.OrdinalIgnoreCase) &&
!string.IsNullOrWhiteSpace(argument[logFilePrefix.Length..]))
{
path = argument[logFilePrefix.Length..];
return true;
}
}
path = string.Empty;
return false;
}
private static string BuildDefaultLogFilePath(string? ebootPath)
{
var baseDirectory = AppContext.BaseDirectory;
var logsDirectory = Path.Combine(baseDirectory, "user", "logs");
var name = TryReadTitleId(ebootPath) ?? "UNKNOWN";
foreach (var invalid in Path.GetInvalidFileNameChars())
{
name = name.Replace(invalid, '_');
}
return Path.Combine(logsDirectory, $"{name}-{DateTime.Now:yyyyMMdd-HHmmss}.log");
}
private static string? TryReadTitleId(string? ebootPath)
{
if (string.IsNullOrWhiteSpace(ebootPath))
{
return null;
}
try
{
var directory = Path.GetDirectoryName(Path.GetFullPath(ebootPath));
if (string.IsNullOrEmpty(directory))
{
return null;
}
foreach (var paramPath in new[]
{
Path.Combine(directory, "sce_sys", "param.json"),
Path.Combine(directory, "param.json"),
})
{
if (!File.Exists(paramPath))
{
continue;
}
using var stream = File.OpenRead(paramPath);
using var document = JsonDocument.Parse(stream);
if (document.RootElement.TryGetProperty("titleId", out var titleIdElement) &&
titleIdElement.ValueKind == JsonValueKind.String)
{
var titleId = titleIdElement.GetString();
if (!string.IsNullOrWhiteSpace(titleId))
{
return titleId.Trim();
}
}
}
}
catch
{
// Logging should never block launch; unknown title ids use a stable fallback.
}
return null;
}
private static string? TryFindEbootPathToken(IReadOnlyList<string> args)
{
for (var i = args.Count - 1; i >= 0; i--)
{
var argument = args[i];
if (string.IsNullOrWhiteSpace(argument) ||
argument.StartsWith("--", StringComparison.Ordinal))
{
continue;
}
return argument;
}
return null;
}
private static bool ShouldConsumeLogFilePath(IReadOnlyList<string> args, int candidateIndex)
{
var candidate = args[candidateIndex];
if (LooksLikeLogFilePath(candidate))
{
return true;
}
for (var i = candidateIndex + 1; i < args.Count; i++)
{
var argument = args[i];
if (!string.IsNullOrWhiteSpace(argument) &&
!argument.StartsWith("--", StringComparison.Ordinal))
{
return true;
}
}
return false;
}
private static bool LooksLikeLogFilePath(string path)
{
var extension = Path.GetExtension(path);
return string.Equals(extension, ".log", StringComparison.OrdinalIgnoreCase) ||
string.Equals(extension, ".txt", StringComparison.OrdinalIgnoreCase);
}
private static void TryEnableConsoleFileMirror(string path)
{
lock (ConsoleMirrorSync)
{
if (_consoleMirrorFile is not null)
{
return;
}
try
{
var directory = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(directory))
{
Directory.CreateDirectory(directory);
}
var stream = new FileStream(
path,
FileMode.Create,
FileAccess.Write,
FileShare.ReadWrite,
bufferSize: 4096,
FileOptions.SequentialScan);
_consoleMirrorFile = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false))
{
AutoFlush = true,
};
Console.SetOut(new TeeTextWriter(Console.Out, _consoleMirrorFile));
Console.SetError(new TeeTextWriter(Console.Error, _consoleMirrorFile));
Console.Error.WriteLine($"[DEBUG] Log file: {Path.GetFullPath(path)}");
}
catch (Exception ex)
{
Console.Error.WriteLine($"[WARN] Could not open log file '{path}': {ex.Message}");
}
}
}
private static void DropConsoleFileMirror()
{
lock (ConsoleMirrorSync)
{
try
{
_consoleMirrorFile?.Flush();
_consoleMirrorFile?.Dispose();
}
catch
{
}
_consoleMirrorFile = null;
}
}
private static void ConfigureInheritedStdHandles(ref STARTUPINFO startupInfo)
{
if (!OperatingSystem.IsWindows())
{
return;
}
var input = GetStdHandle(STD_INPUT_HANDLE);
var output = GetStdHandle(STD_OUTPUT_HANDLE);
var error = GetStdHandle(STD_ERROR_HANDLE);
if (!IsHandleValid(output) && !IsHandleValid(error))
{
return;
}
if (IsHandleValid(input))
{
_ = SetHandleInformation(input, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
startupInfo.hStdInput = input;
}
if (IsHandleValid(output))
{
_ = SetHandleInformation(output, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
startupInfo.hStdOutput = output;
}
if (IsHandleValid(error))
{
_ = SetHandleInformation(error, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
startupInfo.hStdError = error;
}
startupInfo.dwFlags |= STARTF_USESTDHANDLES;
}
private static string BuildCommandLine(string processPath, IReadOnlyList<string> args)
{
var builder = new StringBuilder();
@@ -503,27 +768,30 @@ internal static partial class Program
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""");
Log.Info("Usage: SharpEmu.CLI [--strict] [--trace-imports[=N]] [--cpu-engine=<native>] [--log-level=<level>] [--log-file[=<path>]] <path-to-eboot.bin>");
Log.Info(@"Example: SharpEmu.CLI --cpu-engine=native --trace-imports=64 --log-level=debug --log-file ""E:\Games\...\eboot.bin""");
}
private static bool TryParseArguments(
string[] args,
out string ebootPath,
out SharpEmuRuntimeOptions runtimeOptions,
out LogLevel logLevel)
out LogLevel logLevel,
out string? logFilePath)
{
if (args.Length == 0)
{
ebootPath = string.Empty;
runtimeOptions = default;
logLevel = SharpEmuLog.MinimumLevel;
logFilePath = null;
return false;
}
var strictDynlibResolution = false;
var importTraceLimit = 0;
var cpuEngine = CpuExecutionEngine.NativeOnly;
logFilePath = null;
logLevel = SharpEmuLog.MinimumLevel;
var pathTokens = new List<string>(args.Length);
for (var i = 0; i < args.Length; i++)
@@ -553,6 +821,7 @@ internal static partial class Program
{
ebootPath = string.Empty;
runtimeOptions = default;
logFilePath = null;
return false;
}
@@ -566,6 +835,7 @@ internal static partial class Program
{
ebootPath = string.Empty;
runtimeOptions = default;
logFilePath = null;
return false;
}
@@ -573,6 +843,23 @@ internal static partial class Program
continue;
}
if (string.Equals(argument, "--log-file", StringComparison.OrdinalIgnoreCase))
{
if (i + 1 < args.Length &&
!string.IsNullOrWhiteSpace(args[i + 1]) &&
!args[i + 1].StartsWith("--", StringComparison.Ordinal) &&
ShouldConsumeLogFilePath(args, i + 1))
{
logFilePath = args[++i];
}
else
{
logFilePath = BuildDefaultLogFilePath(TryFindEbootPathToken(args));
}
continue;
}
const string logLevelPrefix = "--log-level=";
if (argument.StartsWith(logLevelPrefix, StringComparison.OrdinalIgnoreCase))
{
@@ -596,6 +883,7 @@ internal static partial class Program
ebootPath = string.Empty;
runtimeOptions = default;
logLevel = SharpEmuLog.MinimumLevel;
logFilePath = null;
return false;
}
@@ -618,11 +906,27 @@ internal static partial class Program
continue;
}
const string logFilePrefix = "--log-file=";
if (argument.StartsWith(logFilePrefix, StringComparison.OrdinalIgnoreCase))
{
logFilePath = argument[logFilePrefix.Length..];
if (string.IsNullOrWhiteSpace(logFilePath))
{
ebootPath = string.Empty;
runtimeOptions = default;
logLevel = SharpEmuLog.MinimumLevel;
return false;
}
continue;
}
if (argument.StartsWith("--", StringComparison.Ordinal))
{
ebootPath = string.Empty;
runtimeOptions = default;
logLevel = SharpEmuLog.MinimumLevel;
logFilePath = null;
return false;
}
@@ -634,6 +938,7 @@ internal static partial class Program
ebootPath = string.Empty;
runtimeOptions = default;
logLevel = SharpEmuLog.MinimumLevel;
logFilePath = null;
return false;
}
@@ -735,6 +1040,56 @@ internal static partial class Program
public nuint PeakJobMemoryUsed;
}
private sealed class TeeTextWriter : TextWriter
{
private readonly TextWriter _primary;
private readonly TextWriter _mirror;
public TeeTextWriter(TextWriter primary, TextWriter mirror)
{
_primary = primary;
_mirror = mirror;
}
public override Encoding Encoding => _primary.Encoding;
public override void Write(char value)
{
lock (ConsoleMirrorSync)
{
_primary.Write(value);
_mirror.Write(value);
}
}
public override void Write(string? value)
{
lock (ConsoleMirrorSync)
{
_primary.Write(value);
_mirror.Write(value);
}
}
public override void WriteLine(string? value)
{
lock (ConsoleMirrorSync)
{
_primary.WriteLine(value);
_mirror.WriteLine(value);
}
}
public override void Flush()
{
lock (ConsoleMirrorSync)
{
_primary.Flush();
_mirror.Flush();
}
}
}
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool InitializeProcThreadAttributeList(
@@ -819,6 +1174,10 @@ internal static partial class Program
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetStdHandle(int stdHandle, nint handle);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetHandleInformation(nint handle, uint mask, uint flags);
[DllImport("kernel32.dll", EntryPoint = "CreateFileW", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern nint CreateFileW(
string fileName,
+4 -1
View File
@@ -216,7 +216,10 @@
}
},
"sharpemu.hle": {
"type": "Project"
"type": "Project",
"dependencies": {
"SharpEmu.Logging": "[1.0.0, )"
}
},
"sharpemu.libs": {
"type": "Project",
@@ -38,6 +38,71 @@ public sealed partial class DirectExecutionBackend
}
}
private void RecordDeferredBootstrapTrace(
long dispatchIndex,
ulong op,
ulong symbolPointer,
ulong outputPointer,
ulong returnRip)
{
lock (_deferredBootstrapTraceGate)
{
_deferredBootstrapTrace[_deferredBootstrapTraceWriteIndex] = new DeferredBootstrapTraceEntry(
dispatchIndex,
op,
symbolPointer,
outputPointer,
returnRip);
_deferredBootstrapTraceWriteIndex =
(_deferredBootstrapTraceWriteIndex + 1) % _deferredBootstrapTrace.Length;
if (_deferredBootstrapTraceCount < _deferredBootstrapTrace.Length)
{
_deferredBootstrapTraceCount++;
}
}
}
private void DrainDeferredBootstrapTraces()
{
if (!_logBootstrap)
{
return;
}
DeferredBootstrapTraceEntry[] pending;
lock (_deferredBootstrapTraceGate)
{
if (_deferredBootstrapTraceCount == 0)
{
return;
}
pending = new DeferredBootstrapTraceEntry[_deferredBootstrapTraceCount];
var readIndex = (_deferredBootstrapTraceWriteIndex - _deferredBootstrapTraceCount +
_deferredBootstrapTrace.Length) % _deferredBootstrapTrace.Length;
for (var i = 0; i < _deferredBootstrapTraceCount; i++)
{
pending[i] = _deferredBootstrapTrace[(readIndex + i) % _deferredBootstrapTrace.Length];
}
_deferredBootstrapTraceCount = 0;
}
foreach (var entry in pending)
{
var symbolText = "<unreadable>";
if (TryReadAsciiZ(entry.SymbolPointer, 256, out var sym))
{
symbolText = sym;
}
Console.Error.WriteLine(
$"[LOADER][TRACE] bootstrap_call#{entry.DispatchIndex}: op=0x{entry.Op:X16} " +
$"sym_ptr=0x{entry.SymbolPointer:X16} sym='{symbolText}' " +
$"out_ptr=0x{entry.OutputPointer:X16} ret=0x{entry.ReturnRip:X16}");
}
}
private void DumpRecentImportTrace()
{
if (_recentImportTraceCount == 0)
@@ -170,14 +235,15 @@ public sealed partial class DirectExecutionBackend
ulong callRip = returnRip + (ulong)i;
ulong target = unchecked((ulong)((long)(callRip + 5) + rel32));
Log.Debug($"Import#{dispatchIndex} near-call @{callRip:X16}: target=0x{target:X16}");
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
var importEntries = _importEntries;
for (int importIndex = 0; importIndex < importEntries.Length; importIndex++)
{
if (_importEntries[importIndex].Address != target)
if (importEntries[importIndex].Address != target)
{
continue;
}
string nid = _importEntries[importIndex].Nid;
string nid = importEntries[importIndex].Nid;
if (_moduleManager.TryGetExport(nid, out var export))
{
Log.Debug(
@@ -204,14 +270,14 @@ public sealed partial class DirectExecutionBackend
{
Log.Debug(
$"Import#{dispatchIndex} near-call PLT slot: [0x{slot:X16}] = 0x{slotTarget:X16}");
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
for (int importIndex = 0; importIndex < importEntries.Length; importIndex++)
{
if (_importEntries[importIndex].Address != slotTarget)
if (importEntries[importIndex].Address != slotTarget)
{
continue;
}
string nid = _importEntries[importIndex].Nid;
string nid = importEntries[importIndex].Nid;
if (_moduleManager.TryGetExport(nid, out var export))
{
Log.Debug(
@@ -103,6 +103,15 @@ public sealed partial class DirectExecutionBackend
ulong rip = ReadCtxU64(contextRecord, 248);
ulong rsp = ReadCtxU64(contextRecord, 152);
// Thread-mode probe: a hardware exception raised while this thread is inside
// the managed import gateway means the VEH->managed reentry happened from
// cooperative GC mode — a ReversePInvokeBadTransition candidate.
if (LogThreadMode && _threadModeGatewayDepth > 0)
{
TraceThreadMode(
$"veh_in_gateway code=0x{exceptionCode:X8} rip=0x{rip:X16} gateway_depth={_threadModeGatewayDepth}");
}
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
{
return -1;
@@ -8,6 +8,7 @@ using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.Core.Cpu;
using SharpEmu.Core.Loader;
using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu.Native;
@@ -19,6 +20,16 @@ public sealed partial class DirectExecutionBackend
private static ulong ImportDispatchGatewayManaged(nint backendHandle, int importIndex, nint argPackPtr)
{
if (LogThreadMode)
{
_threadModeGatewayCalls++;
_threadModeGatewayDepth++;
if (!_threadModeGatewayFirstLogged)
{
_threadModeGatewayFirstLogged = true;
TraceThreadMode($"gateway_first import={importIndex} total={_threadModeGatewayCalls}");
}
}
try
{
if (!(GCHandle.FromIntPtr(backendHandle).Target is DirectExecutionBackend directExecutionBackend))
@@ -36,6 +47,13 @@ public sealed partial class DirectExecutionBackend
$"[LOADER][ERROR] ImportDispatchGatewayManaged exception: {ex.GetType().Name}: {ex.Message}");
return 18446744071562199298uL;
}
finally
{
if (LogThreadMode)
{
_threadModeGatewayDepth--;
}
}
}
private unsafe static int RawVectoredHandlerManaged(void* exceptionInfo)
@@ -69,6 +87,12 @@ public sealed partial class DirectExecutionBackend
WriteCtxU64(contextRecord, 152, nextRsp);
WriteCtxU64(contextRecord, 248, returnRip);
Interlocked.Increment(ref _rawSentinelRecoveries);
if (LogThreadMode)
{
TraceThreadMode(
$"sentinel_recover rip=0x{value:X16} -> 0x{returnRip:X16} rsp=0x{rsp:X16} -> 0x{nextRsp:X16} " +
$"gateway_depth={_threadModeGatewayDepth}");
}
return -1;
}
return 0;
@@ -87,12 +111,13 @@ public sealed partial class DirectExecutionBackend
LastError = "Import dispatch called without active CPU context";
return 18446744071562199298uL;
}
if ((uint)importIndex >= (uint)_importEntries.Length)
var importEntries = _importEntries;
if ((uint)importIndex >= (uint)importEntries.Length)
{
LastError = $"Import dispatch index out of range: {importIndex}";
return 18446744071562199042uL;
}
ImportStubEntry importStubEntry = _importEntries[importIndex];
ImportStubEntry importStubEntry = importEntries[importIndex];
int num2 = Volatile.Read(in _rawSentinelRecoveries);
if (num2 != _lastReportedRawSentinelRecoveries)
{
@@ -136,6 +161,14 @@ public sealed partial class DirectExecutionBackend
*(ulong*)(xmmSlot + 8));
}
cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL;
if (string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
{
NormalizeKernelDynlibDlsymArguments(cpuContext, out _, out _);
*(ulong*)argPackPtr = cpuContext[CpuRegister.Rdi];
*(ulong*)(argPackPtr + 8) = cpuContext[CpuRegister.Rsi];
*(ulong*)(argPackPtr + 16) = cpuContext[CpuRegister.Rdx];
}
ulong value = cpuContext[CpuRegister.Rdi];
ulong value2 = cpuContext[CpuRegister.Rsi];
ulong num3 = cpuContext[CpuRegister.Rdx];
@@ -186,16 +219,6 @@ public sealed partial class DirectExecutionBackend
TraceGuestContext(
$"import dispatch={num} nid={importStubEntry.Nid} ret=0x{num7:X16} managed={Environment.CurrentManagedThreadId} guest=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} fiber=0x{GuestThreadExecution.CurrentFiberAddress:X16} active={HasActiveExecutionThread}");
}
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 (!isGuestWorker &&
!ActiveForcedGuestExit &&
ShouldForceGuestExitOnImportLoop(importStubEntry.Nid, num7, num, value, value2) &&
@@ -204,6 +227,17 @@ public sealed partial class DirectExecutionBackend
cpuContext[CpuRegister.Rax] = 1uL;
return 1uL;
}
// Backend teardown in progress: unwind this worker to the host instead of
// dispatching. RunGuestThread parks it as Blocked with no wake handler, its
// host thread exits, and RequestGuestThreadTeardown can finish before any
// executable memory is freed.
if (isGuestWorker &&
_guestTeardownRequested &&
TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, "backend teardown"))
{
cpuContext[CpuRegister.Rax] = 0uL;
return 0uL;
}
bool flag0 = ShouldSuppressStrlenTrace(importStubEntry.Nid);
bool flag = num7 >= 2156221920u && num7 <= 2156225024u;
bool flag2 = num7 >= 2156351360u && num7 <= 2156352080u;
@@ -349,6 +383,11 @@ public sealed partial class DirectExecutionBackend
{
if (string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
{
if (_logBootstrap)
{
RecordDeferredBootstrapTrace(num, value, value2, num3, num7);
}
orbisGen2Result = DispatchBootstrapBridge();
}
else if (string.Equals(importStubEntry.Nid, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal) ||
@@ -511,6 +550,7 @@ public sealed partial class DirectExecutionBackend
cpuContext.GetXmmRegister(0, out var returnXmm0Low, out var returnXmm0High);
*(ulong*)(argPackPtr - 0x80) = returnXmm0Low;
*(ulong*)(argPackPtr - 0x80 + 8) = returnXmm0High;
DrainDeferredBootstrapTraces();
return cpuContext[CpuRegister.Rax];
}
catch (Exception ex)
@@ -519,6 +559,7 @@ public sealed partial class DirectExecutionBackend
Console.Error.WriteLine($"[LOADER][ERROR] {LastError}");
Console.Error.WriteLine($"[LOADER][ERROR] {ex.StackTrace}");
cpuContext[CpuRegister.Rax] = 18446744071562199298uL;
DrainDeferredBootstrapTraces();
return 18446744071562199298uL;
}
}
@@ -671,6 +712,7 @@ public sealed partial class DirectExecutionBackend
"eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
"qvMUCyyaCSI" or // sceKernelAprSubmitCommandBufferAndGetId
"Q2V+iqvjgC0" or // vsnprintf
"AV6ipCNa4Rw" or // strcasecmp
"q1cHNfGycLI" or // scePadRead
"xk0AcarP3V4" or // scePadOpen
"yH17Q6NWtVg" or // sceUserServiceGetEvent
@@ -835,6 +877,7 @@ public sealed partial class DirectExecutionBackend
"WkkeywLJcgU" or // wcslen
"Ovb2dSJOAuE" or // strcmp
"aesyjrHVWy4" or // strncmp
"AV6ipCNa4Rw" or // strcasecmp
"pNtJdE3x49E" or // wcscmp
"fV2xHER+bKE" or // wcscoll
"E8wCoUEbfzk" or // wcsncmp
@@ -1275,30 +1318,485 @@ public sealed partial class DirectExecutionBackend
{
return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
ulong symbolNameAddress = cpuContext[CpuRegister.Rsi];
ulong outputAddress = cpuContext[CpuRegister.Rdx];
if (!TryReadAsciiZ(symbolNameAddress, 512, out var symbolName))
NormalizeKernelDynlibDlsymArguments(cpuContext, out var symbolNameAddress, out var outputAddress);
try
{
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
return OrbisGen2Result.ORBIS_GEN2_OK;
if (!TryReadAsciiZ(symbolNameAddress, 512, out var symbolName) ||
!TryResolveDlsymGuestAddress(symbolName, out var resolvedAddress) ||
outputAddress == 0 ||
!TryWriteUInt64Compat(outputAddress, resolvedAddress))
{
return CompleteKernelDynlibDlsymFailure(cpuContext, outputAddress);
}
}
if (!TryResolveRuntimeSymbolAddress(symbolName, out var resolvedAddress) &&
!TryResolveRuntimeSymbolAlias(symbolName, out resolvedAddress))
catch
{
Console.Error.WriteLine(
$"[LOADER][WARN] sceKernelDlsym failed: handle=0x{cpuContext[CpuRegister.Rdi]:X} symbol='{symbolName}'");
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
return OrbisGen2Result.ORBIS_GEN2_OK;
}
if (outputAddress == 0L || !TryWriteUInt64Compat(outputAddress, resolvedAddress))
{
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
return OrbisGen2Result.ORBIS_GEN2_OK;
return CompleteKernelDynlibDlsymFailure(cpuContext, outputAddress);
}
cpuContext[CpuRegister.Rax] = 0uL;
return OrbisGen2Result.ORBIS_GEN2_OK;
}
private static void NormalizeKernelDynlibDlsymArguments(
CpuContext cpuContext,
out ulong symbolNameAddress,
out ulong outputAddress)
{
var handle = cpuContext[CpuRegister.Rdi];
symbolNameAddress = cpuContext[CpuRegister.Rsi];
outputAddress = cpuContext[CpuRegister.Rdx];
// Standalone bootstrap loaders sometimes call through the bridge with
// (symbol_ptr, handle, out) while sceKernelDlsym is (handle, symbol_ptr, out).
// Heuristic only: valid when RSI looks like a small handle and RDI is a guest pointer.
if (symbolNameAddress < 0x10000 &&
IsPlausibleDynlibSymbolPointer(handle))
{
symbolNameAddress = handle;
handle = cpuContext[CpuRegister.Rsi];
cpuContext[CpuRegister.Rdi] = handle;
cpuContext[CpuRegister.Rsi] = symbolNameAddress;
}
}
private static bool IsPlausibleDynlibSymbolPointer(ulong address)
{
return address >= 0x10000 && address < 0x0000_8000_0000_0000UL;
}
private OrbisGen2Result CompleteKernelDynlibDlsymFailure(CpuContext cpuContext, ulong outputAddress)
{
if (outputAddress != 0)
{
_ = TryWriteUInt64Compat(outputAddress, 0);
}
cpuContext[CpuRegister.Rax] = ulong.MaxValue;
return OrbisGen2Result.ORBIS_GEN2_OK;
}
private void ResetLazyDlsymStubState()
{
lock (_lazyDlsymStubGate)
{
_lazyDlsymStubCache.Clear();
_lazyImportStubPoolMapped = false;
_lazyImportStubPoolBase = 0;
_lazyImportStubNextSlot = 0;
_lazyImportStubPoolLimit = 0;
}
}
private bool TryResolveDlsymGuestAddress(string symbolName, out ulong guestAddress)
{
guestAddress = 0;
if (string.IsNullOrWhiteSpace(symbolName))
{
return false;
}
// Tier 0: ELF runtime symbols and aliases.
if (TryResolveRuntimeSymbolAddress(symbolName, out guestAddress) ||
TryResolveRuntimeSymbolAlias(symbolName, out guestAddress))
{
return true;
}
// Tier 1: existing import stub entries (duplicate prevention).
if (TryFindImportStubGuestAddress(symbolName, out guestAddress))
{
return true;
}
var hasAerolibSymbol = Aerolib.Instance.TryGetByExportName(symbolName, out var hleSymbol);
if (hasAerolibSymbol)
{
if (HleDataSymbols.TryGetAddress(hleSymbol.Nid, out _))
{
return false;
}
if (TryResolveRuntimeSymbolAddress(hleSymbol.Nid, out guestAddress) ||
TryResolveRuntimeSymbolAddress(hleSymbol.ExportName, out guestAddress) ||
TryFindImportStubGuestAddress(hleSymbol.Nid, out guestAddress) ||
TryFindImportStubGuestAddress(hleSymbol.ExportName, out guestAddress))
{
return true;
}
}
else if (Aerolib.Instance.TryGetByNid(symbolName, out hleSymbol))
{
if (HleDataSymbols.TryGetAddress(hleSymbol.Nid, out _))
{
return false;
}
if (TryFindImportStubGuestAddress(hleSymbol.Nid, out guestAddress) ||
TryFindImportStubGuestAddress(hleSymbol.ExportName, out guestAddress))
{
return true;
}
hasAerolibSymbol = true;
}
// Tier 3: lazy materialization for callable HLE symbols missing from ELF imports.
if (!TryResolveLazyDispatchTarget(symbolName, hasAerolibSymbol, in hleSymbol, out var dispatchNid, out var export))
{
return false;
}
return TryGetOrCreateLazyImportStub(
dispatchNid,
symbolName,
hasAerolibSymbol ? hleSymbol : null,
export,
out guestAddress);
}
private bool TryFindImportStubGuestAddress(string identifier, out ulong guestAddress)
{
guestAddress = 0;
if (string.IsNullOrWhiteSpace(identifier))
{
return false;
}
var importEntries = _importEntries;
for (var i = 0; i < importEntries.Length; i++)
{
var entry = importEntries[i];
if (!ImportStubEntryMatchesIdentifier(entry, identifier))
{
continue;
}
if (entry.Address >= 0x10000)
{
guestAddress = entry.Address;
return true;
}
}
return false;
}
private static bool ImportStubEntryMatchesIdentifier(in ImportStubEntry entry, string identifier)
{
if (string.Equals(entry.Nid, identifier, StringComparison.Ordinal))
{
return true;
}
if (entry.Export is not { } export)
{
return false;
}
return string.Equals(export.Name, identifier, StringComparison.Ordinal) ||
string.Equals(export.Nid, identifier, StringComparison.Ordinal);
}
private static bool IsKernelDynlibDlsymIdentifier(string identifier)
{
return string.Equals(identifier, "sceKernelDlsym", StringComparison.Ordinal) ||
string.Equals(identifier, KernelDynlibDlsymAerolibNid, StringComparison.Ordinal) ||
string.Equals(identifier, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal);
}
private bool TryResolveLazyDispatchTarget(
string symbolName,
bool hasAerolibSymbol,
in SysAbiSymbol hleSymbol,
out string dispatchNid,
out ExportedFunction? export)
{
export = null;
dispatchNid = string.Empty;
if (IsKernelDynlibDlsymIdentifier(symbolName))
{
dispatchNid = KernelDynlibDlsymAerolibNid;
_ = _moduleManager.TryGetExport(dispatchNid, out export);
return true;
}
if (!hasAerolibSymbol)
{
return false;
}
if (HleDataSymbols.TryGetAddress(hleSymbol.Nid, out _))
{
return false;
}
if (_moduleManager.TryGetExport(hleSymbol.Nid, out export))
{
dispatchNid = hleSymbol.Nid;
return true;
}
return false;
}
private bool TryGetOrCreateLazyImportStub(
string dispatchNid,
string symbolName,
SysAbiSymbol? hleSymbol,
ExportedFunction? export,
out ulong guestAddress)
{
guestAddress = 0;
if (string.IsNullOrWhiteSpace(dispatchNid))
{
return false;
}
lock (_lazyDlsymStubGate)
{
if (TryGetCachedLazyDlsymAddress(dispatchNid, symbolName, out guestAddress))
{
return true;
}
if (!EnsureLazyImportStubPoolMapped())
{
LogLazyImportStubMaterializationFailure(
"import stub region unresolved",
dispatchNid,
symbolName);
return false;
}
if (!TryAllocateLazyImportStubSlot(out guestAddress))
{
LogLazyImportStubMaterializationFailure(
"lazy import stub pool exhausted",
dispatchNid,
symbolName);
return false;
}
var previousEntries = _importEntries;
var importIndex = previousEntries.Length;
var newEntries = new ImportStubEntry[importIndex + 1];
if (importIndex > 0)
{
Array.Copy(previousEntries, newEntries, importIndex);
}
newEntries[importIndex] = new ImportStubEntry(guestAddress, dispatchNid, export);
var hostTrampoline = CreateImportHandlerTrampoline(importIndex);
if (hostTrampoline == 0 ||
!PatchImportStub((nint)(long)guestAddress, hostTrampoline))
{
guestAddress = 0;
LogLazyImportStubMaterializationFailure(
"failed to patch lazy import stub trampoline",
dispatchNid,
symbolName);
return false;
}
_importEntries = newEntries;
RegisterDlsymRuntimeSymbolKeys(symbolName, dispatchNid, hleSymbol, guestAddress);
return true;
}
}
private bool TryGetCachedLazyDlsymAddress(string dispatchNid, string symbolName, out ulong guestAddress)
{
if (_lazyDlsymStubCache.TryGetValue(dispatchNid, out guestAddress) ||
_lazyDlsymStubCache.TryGetValue(symbolName, out guestAddress))
{
return guestAddress >= 0x10000;
}
return false;
}
private void RegisterDlsymRuntimeSymbolKeys(
string symbolName,
string dispatchNid,
SysAbiSymbol? hleSymbol,
ulong guestAddress)
{
RegisterDlsymRuntimeSymbolKey(symbolName, guestAddress);
RegisterDlsymRuntimeSymbolKey(dispatchNid, guestAddress);
if (IsKernelDynlibDlsymIdentifier(symbolName) || IsKernelDynlibDlsymIdentifier(dispatchNid))
{
RegisterDlsymRuntimeSymbolKey(RuntimeStubNids.KernelDynlibDlsym, guestAddress);
RegisterDlsymRuntimeSymbolKey(KernelDynlibDlsymAerolibNid, guestAddress);
RegisterDlsymRuntimeSymbolKey("sceKernelDlsym", guestAddress);
}
if (hleSymbol.HasValue)
{
RegisterDlsymRuntimeSymbolKey(hleSymbol.Value.Nid, guestAddress);
RegisterDlsymRuntimeSymbolKey(hleSymbol.Value.ExportName, guestAddress);
}
}
private void RegisterDlsymRuntimeSymbolKey(string key, ulong guestAddress)
{
if (string.IsNullOrWhiteSpace(key) || !IsRuntimeSymbolAddressUsable(guestAddress))
{
return;
}
_runtimeSymbolsByName[key] = guestAddress;
_lazyDlsymStubCache[key] = guestAddress;
}
private void LogLazyImportStubMaterializationFailure(string reason, string dispatchNid, string symbolName)
{
Console.Error.WriteLine(
$"[LOADER][WARN] Lazy import stub materialization failed ({reason}): " +
$"nid={dispatchNid} symbol='{symbolName}'");
}
private unsafe bool TryResolveImportStubRegionBounds(
ImportStubEntry[] importEntries,
out ulong regionBase,
out ulong regionLimit)
{
regionBase = 0;
regionLimit = 0;
for (var candidateIndex = 0; candidateIndex < 64; candidateIndex++)
{
var candidateBase = ImportStubRegionCanonicalBase -
(ulong)candidateIndex * ImportStubRegionAddressStride;
if (VirtualQuery((void*)candidateBase, out var memoryInfo, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0 ||
memoryInfo.RegionSize == 0 ||
memoryInfo.State != 4096)
{
continue;
}
var candidateLimit = candidateBase + memoryInfo.RegionSize;
var hasStub = false;
for (var i = 0; i < importEntries.Length; i++)
{
var entryAddress = importEntries[i].Address;
if (entryAddress < candidateBase || entryAddress >= candidateLimit)
{
continue;
}
if ((entryAddress - candidateBase) % LazyImportStubSlotSize != 0)
{
continue;
}
hasStub = true;
break;
}
if (!hasStub)
{
continue;
}
regionBase = candidateBase;
regionLimit = candidateLimit;
return true;
}
ulong maxStubEnd = 0;
for (var i = 0; i < importEntries.Length; i++)
{
var entryAddress = importEntries[i].Address;
if (entryAddress < ImportStubRegionCanonicalBase)
{
continue;
}
if ((entryAddress - ImportStubRegionCanonicalBase) % LazyImportStubSlotSize != 0)
{
continue;
}
var entryEnd = entryAddress + LazyImportStubSlotSize;
if (entryEnd > maxStubEnd)
{
maxStubEnd = entryEnd;
regionBase = ImportStubRegionCanonicalBase;
}
}
if (regionBase == 0 || maxStubEnd <= regionBase)
{
return false;
}
var spanBytes = maxStubEnd - regionBase;
regionLimit = regionBase + AlignUp(spanBytes, ImportStubRegionPageSize);
return regionLimit > regionBase;
}
private bool EnsureLazyImportStubPoolMapped()
{
if (_lazyImportStubPoolMapped && _lazyImportStubPoolBase != 0)
{
return true;
}
var importEntries = _importEntries;
if (!TryResolveImportStubRegionBounds(importEntries, out var importStubRegionBase, out var importStubRegionLimit))
{
return false;
}
ulong nextSlot = importStubRegionBase;
for (var i = 0; i < importEntries.Length; i++)
{
var entryAddress = importEntries[i].Address;
if (entryAddress < importStubRegionBase || entryAddress >= importStubRegionLimit)
{
continue;
}
var entryEnd = entryAddress + LazyImportStubSlotSize;
if (entryEnd > nextSlot)
{
nextSlot = entryEnd;
}
}
if (nextSlot >= importStubRegionLimit)
{
return false;
}
_lazyImportStubPoolBase = importStubRegionBase;
_lazyImportStubNextSlot = nextSlot;
_lazyImportStubPoolLimit = importStubRegionLimit;
_lazyImportStubPoolMapped = true;
return true;
}
private bool TryAllocateLazyImportStubSlot(out ulong guestAddress)
{
guestAddress = 0;
if (!_lazyImportStubPoolMapped ||
_lazyImportStubNextSlot < _lazyImportStubPoolBase ||
_lazyImportStubNextSlot + LazyImportStubSlotSize > _lazyImportStubPoolLimit)
{
return false;
}
guestAddress = _lazyImportStubNextSlot;
_lazyImportStubNextSlot += LazyImportStubSlotSize;
return true;
}
private bool TryResolveRuntimeSymbolAlias(string symbolName, out ulong address)
{
address = 0;
@@ -1363,29 +1861,20 @@ public sealed partial class DirectExecutionBackend
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)
try
{
return result;
OrbisGen2Result result = DispatchKernelDynlibDlsym();
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
{
return result;
}
}
if (_logBootstrap)
catch
{
Console.Error.WriteLine(
$"[LOADER][TRACE] bootstrap_dispatch: handle=0x{bridgeHandle:X16} symbol='{symbolName}' out=0x{outputAddress:X16} rax=0x{cpuContext[CpuRegister.Rax]:X16}");
return CompleteKernelDynlibDlsymFailure(cpuContext, outputAddress);
}
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;
}
@@ -1426,6 +1915,7 @@ public sealed partial class DirectExecutionBackend
{
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++)
@@ -0,0 +1,582 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu.Native;
public sealed partial class DirectExecutionBackend
{
// Guest entry stubs must not run above CLR-managed frames on a CLR-created thread:
// the suspension/stack-walk machinery then has to traverse a frame chain that is
// interleaved with guest stubs carrying no CLR unwind info, and any window in which
// the thread-mode bookkeeping disagrees with the actual stack (import dispatch, VEH
// redirection) fail-fasts the runtime — the audio_output_thread ~7th-cycle
// "attempted to call a UnmanagedCallersOnly method from managed code" crash.
//
// Guest execution therefore runs on dedicated raw OS threads whose run loop is
// emitted native code. While guest code executes there is not a single managed
// frame on the thread and it sits in preemptive mode, so the GC ignores it
// entirely. The only managed activity on the thread is the reverse-P/Invoke import
// dispatch and the per-run prologue/epilogue, which the CLR supports on foreign
// threads (lazy attach, preemptive between calls). The managed orchestrator
// (RunGuestThread / the main execution thread) parks in a preemptive wait for the
// duration of the run, so its own frame chain is never walked across guest frames.
private static readonly bool NativeGuestWorkersDisabled =
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_NATIVE_GUEST_WORKERS"), "1", StringComparison.Ordinal);
private readonly object _nativeWorkerGate = new();
private readonly List<NativeGuestExecutor> _allNativeWorkers = new();
private readonly Stack<NativeGuestExecutor> _idleNativeWorkers = new();
private bool _nativeWorkersDisposed;
private int _nativeWorkerCreationFailedLogged;
private const uint StackSizeParamIsAReservation = 0x00010000u;
[DllImport("kernel32.dll", SetLastError = true)]
private static extern nint CreateThread(
nint lpThreadAttributes,
nuint dwStackSize,
nint lpStartAddress,
nint lpParameter,
uint dwCreationFlags,
out uint lpThreadId);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint WaitForSingleObject(nint hHandle, uint dwMilliseconds);
// Runs an emitted guest entry stub. Preferred path is a pooled native worker
// thread; falls back to the historical inline calli (guest frames above this
// thread's managed frames) when workers are disabled or unavailable.
//
// Callers set the Active* thread-statics before emitting the stub and read the
// yield/forced-exit flags right after this returns, so the worker outcome is
// copied back into this thread's statics before returning.
private unsafe int RunGuestEntryStub(void* entryStub, ulong hostRspSlot)
{
var worker = RentNativeGuestExecutor();
if (worker is null)
{
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
return CallNativeEntry(entryStub);
}
try
{
var state = _activeGuestThreadState;
var nativeReturn = worker.Run(
_activeCpuContext!,
state,
GuestThreadExecution.CurrentGuestThreadHandle,
_activeEntryReturnSentinelRip,
_activeGuestReturnSlotAddress,
(nint)hostRspSlot,
(nint)entryStub,
state?.AffinityMask ?? 0,
out var yieldRequested,
out var yieldReason,
out var forcedExit);
_activeGuestThreadYieldRequested = yieldRequested;
_activeGuestThreadYieldReason = yieldReason;
_activeForcedGuestExit = forcedExit;
return nativeReturn;
}
finally
{
ReturnNativeGuestExecutor(worker);
}
}
private NativeGuestExecutor? RentNativeGuestExecutor()
{
if (NativeGuestWorkersDisabled)
{
return null;
}
lock (_nativeWorkerGate)
{
if (_nativeWorkersDisposed)
{
return null;
}
if (_idleNativeWorkers.Count > 0)
{
return _idleNativeWorkers.Pop();
}
}
var worker = NativeGuestExecutor.TryCreate(this);
if (worker is null)
{
if (Interlocked.Exchange(ref _nativeWorkerCreationFailedLogged, 1) == 0)
{
Console.Error.WriteLine(
"[LOADER][WARN] Failed to create a native guest worker thread; falling back to inline guest execution.");
}
return null;
}
lock (_nativeWorkerGate)
{
if (_nativeWorkersDisposed)
{
worker.Dispose();
return null;
}
_allNativeWorkers.Add(worker);
}
return worker;
}
private void ReturnNativeGuestExecutor(NativeGuestExecutor worker)
{
lock (_nativeWorkerGate)
{
if (!_nativeWorkersDisposed)
{
_idleNativeWorkers.Push(worker);
return;
}
}
worker.Dispose();
}
private void DisposeNativeGuestExecutors()
{
NativeGuestExecutor[] workers;
lock (_nativeWorkerGate)
{
if (_nativeWorkersDisposed)
{
return;
}
_nativeWorkersDisposed = true;
workers = _allNativeWorkers.ToArray();
_allNativeWorkers.Clear();
_idleNativeWorkers.Clear();
}
foreach (var worker in workers)
{
worker.Dispose();
}
}
// A pooled raw OS thread that executes guest entry stubs. The run loop is emitted
// native code (no CLR unwind info required — nothing ever unwinds through it):
//
// loop: WaitForSingleObject(work);
// if (*stopFlag) { SetEvent(done); ExitThread(0); }
// rax = RunPrologue(self); // managed: binds per-run ambient, returns stub
// if (rax != 0) eax = rax(); // guest entry stub — zero managed frames below
// RunEpilogue(self, eax); // managed: captures outcome, restores ambient
// SetEvent(done); goto loop;
//
// Workers carry no per-guest identity of their own: the prologue rebinds guest TLS,
// the host-RSP slot, the Active* thread-statics and the GuestThreadExecution ambient
// on every run, so a worker can be reused for any guest thread.
private sealed unsafe class NativeGuestExecutor : IDisposable
{
private const uint LoopStubSize = 512u;
private const uint WorkerStackReservation = 4u * 1024u * 1024u;
private static nint _waitForSingleObjectAddress;
private static nint _setEventAddress;
private static nint _exitThreadAddress;
private readonly DirectExecutionBackend _backend;
private readonly AutoResetEvent _workAvailable = new(false);
private readonly AutoResetEvent _workCompleted = new(false);
private GCHandle _selfHandle;
private void* _controlBlock;
private void* _loopStub;
private nint _threadHandle;
private uint _nativeThreadId;
// Single in-flight run; publication is ordered by the work/done event pair.
private CpuContext? _runContext;
private GuestThreadState? _runState;
private ulong _runGuestThreadHandle;
private ulong _runSentinelRip;
private ulong _runReturnSlotAddress;
private nint _runHostRspSlot;
private nint _runEntryStub;
private ulong _runAffinityMask;
private int _runNativeResult;
private bool _runYieldRequested;
private string? _runYieldReason;
private bool _runForcedExit;
private bool _runPrologueFailed;
// Prologue -> epilogue carry, only touched on the worker thread.
private DirectExecutionBackend? _prevBackend;
private CpuContext? _prevContext;
private ulong _prevSentinel;
private ulong _prevReturnSlot;
private bool _prevForcedExit;
private bool _prevYieldRequested;
private string? _prevYieldReason;
private GuestThreadState? _prevState;
private ulong _prevGuestThreadHandle;
private nint _prevHostRspSlot;
private int _prevHostThreadId;
private bool _entered;
private NativeGuestExecutor(DirectExecutionBackend backend)
{
_backend = backend;
}
public static NativeGuestExecutor? TryCreate(DirectExecutionBackend backend)
{
if (!EnsureKernel32Exports())
{
return null;
}
var executor = new NativeGuestExecutor(backend);
if (!executor.Initialize())
{
executor.Dispose();
return null;
}
return executor;
}
private static bool EnsureKernel32Exports()
{
if (_exitThreadAddress != 0)
{
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0;
}
nint kernel32 = GetModuleHandle("kernel32.dll");
if (kernel32 == 0)
{
return false;
}
_waitForSingleObjectAddress = GetProcAddress(kernel32, "WaitForSingleObject");
_setEventAddress = GetProcAddress(kernel32, "SetEvent");
_exitThreadAddress = GetProcAddress(kernel32, "ExitThread");
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0 && _exitThreadAddress != 0;
}
private bool Initialize()
{
_selfHandle = GCHandle.Alloc(this);
_controlBlock = VirtualAlloc(null, 4096u, 12288u, 4u);
if (_controlBlock == null)
{
return false;
}
_loopStub = VirtualAlloc(null, LoopStubSize, 12288u, 64u);
if (_loopStub == null)
{
return false;
}
var prologuePtr = (nint)(delegate* unmanaged<nint, nint>)&RunPrologue;
var epiloguePtr = (nint)(delegate* unmanaged<nint, int, void>)&RunEpilogue;
var executorHandle = GCHandle.ToIntPtr(_selfHandle);
var workHandle = _workAvailable.SafeWaitHandle.DangerousGetHandle();
var doneHandle = _workCompleted.SafeWaitHandle.DangerousGetHandle();
byte* code = (byte*)_loopStub;
int offset = 0;
void Emit(byte value) => code[offset++] = value;
void EmitMovRcxImm64(ulong value)
{
Emit(0x48); Emit(0xB9);
*(ulong*)(code + offset) = value;
offset += sizeof(ulong);
}
void EmitMovRaxImm64(ulong value)
{
Emit(0x48); Emit(0xB8);
*(ulong*)(code + offset) = value;
offset += sizeof(ulong);
}
void EmitCallRax()
{
Emit(0xFF); Emit(0xD0);
}
void EmitSetDoneEvent()
{
EmitMovRcxImm64((ulong)doneHandle);
EmitMovRaxImm64((ulong)_setEventAddress);
EmitCallRax();
}
// Thread entry leaves RSP ≡ 8 (mod 16); after this every call below happens
// at RSP ≡ 0 with shadow space available.
Emit(0x48); Emit(0x83); Emit(0xEC); Emit(0x28); // sub rsp, 0x28
int loopStart = offset;
EmitMovRcxImm64((ulong)workHandle);
Emit(0xBA); // mov edx, INFINITE
*(uint*)(code + offset) = 0xFFFFFFFFu;
offset += sizeof(uint);
EmitMovRaxImm64((ulong)_waitForSingleObjectAddress);
EmitCallRax();
EmitMovRaxImm64((ulong)_controlBlock);
Emit(0x83); Emit(0x38); Emit(0x00); // cmp dword [rax], 0
Emit(0x0F); Emit(0x85); // jne stop
int stopJump = offset;
offset += sizeof(int);
EmitMovRcxImm64((ulong)executorHandle);
EmitMovRaxImm64((ulong)prologuePtr);
EmitCallRax(); // rax = entry stub, or 0 on failure
Emit(0x48); Emit(0x85); Emit(0xC0); // test rax, rax
Emit(0x0F); Emit(0x84); // je skipEntry
int skipJump = offset;
offset += sizeof(int);
EmitCallRax(); // guest entry stub -> eax
int skipEntryOffset = offset;
Emit(0x89); Emit(0xC2); // mov edx, eax
EmitMovRcxImm64((ulong)executorHandle);
EmitMovRaxImm64((ulong)epiloguePtr);
EmitCallRax();
EmitSetDoneEvent();
Emit(0xE9); // jmp loop
*(int*)(code + offset) = loopStart - (offset + sizeof(int));
offset += sizeof(int);
int stopOffset = offset;
// Signal done so a Run() racing teardown cannot park forever; a stopped
// worker is never re-rented, so the stale signal is unobservable.
EmitSetDoneEvent();
Emit(0x31); Emit(0xC9); // xor ecx, ecx
EmitMovRaxImm64((ulong)_exitThreadAddress);
EmitCallRax();
Emit(0xCC); // int3 (ExitThread never returns)
*(int*)(code + stopJump) = stopOffset - (stopJump + sizeof(int));
*(int*)(code + skipJump) = skipEntryOffset - (skipJump + sizeof(int));
uint oldProtect = 0;
if (!VirtualProtect(_loopStub, LoopStubSize, 32u, &oldProtect))
{
return false;
}
FlushInstructionCache(GetCurrentProcess(), _loopStub, LoopStubSize);
_threadHandle = CreateThread(
0,
WorkerStackReservation,
(nint)_loopStub,
0,
StackSizeParamIsAReservation,
out _nativeThreadId);
if (_threadHandle == 0)
{
return false;
}
if (LogThreadMode)
{
TraceThreadMode($"worker_created native_tid={_nativeThreadId} loop=0x{(ulong)_loopStub:X16}");
}
return true;
}
public int Run(
CpuContext context,
GuestThreadState? state,
ulong guestThreadHandle,
ulong sentinelRip,
ulong returnSlotAddress,
nint hostRspSlot,
nint entryStub,
ulong affinityMask,
out bool yieldRequested,
out string? yieldReason,
out bool forcedExit)
{
_runContext = context;
_runState = state;
_runGuestThreadHandle = guestThreadHandle;
_runSentinelRip = sentinelRip;
_runReturnSlotAddress = returnSlotAddress;
_runHostRspSlot = hostRspSlot;
_runEntryStub = entryStub;
_runAffinityMask = affinityMask;
_runPrologueFailed = true;
_runYieldRequested = false;
_runYieldReason = null;
_runForcedExit = false;
_workAvailable.Set();
_workCompleted.WaitOne();
_runContext = null;
_runState = null;
yieldRequested = _runYieldRequested;
yieldReason = _runYieldReason;
forcedExit = _runForcedExit;
if (_runPrologueFailed)
{
throw new InvalidOperationException("Native guest worker failed to bind the run ambient (prologue fault)");
}
return _runNativeResult;
}
[UnmanagedCallersOnly]
private static nint RunPrologue(nint executorHandle)
{
try
{
var executor = (NativeGuestExecutor)GCHandle.FromIntPtr(executorHandle).Target!;
return executor.EnterRun();
}
catch (Exception ex)
{
try
{
Console.Error.WriteLine(
$"[LOADER][ERROR] Native guest worker prologue failed: {ex.GetType().Name}: {ex.Message}");
}
catch
{
}
return 0;
}
}
[UnmanagedCallersOnly]
private static void RunEpilogue(nint executorHandle, int nativeResult)
{
try
{
var executor = (NativeGuestExecutor)GCHandle.FromIntPtr(executorHandle).Target!;
executor.ExitRun(nativeResult);
}
catch (Exception ex)
{
try
{
Console.Error.WriteLine(
$"[LOADER][ERROR] Native guest worker epilogue failed: {ex.GetType().Name}: {ex.Message}");
}
catch
{
}
}
}
private nint EnterRun()
{
var backend = _backend;
_prevBackend = _activeExecutionBackend;
_prevContext = _activeCpuContext;
_prevSentinel = _activeEntryReturnSentinelRip;
_prevReturnSlot = _activeGuestReturnSlotAddress;
_prevForcedExit = _activeForcedGuestExit;
_prevYieldRequested = _activeGuestThreadYieldRequested;
_prevYieldReason = _activeGuestThreadYieldReason;
_prevState = _activeGuestThreadState;
_prevHostRspSlot = TlsGetValue(backend._hostRspSlotTlsIndex);
_prevGuestThreadHandle = GuestThreadExecution.EnterGuestThread(_runGuestThreadHandle);
_entered = true;
_activeExecutionBackend = backend;
_activeCpuContext = _runContext;
_activeEntryReturnSentinelRip = _runSentinelRip;
_activeGuestReturnSlotAddress = _runReturnSlotAddress;
_activeForcedGuestExit = false;
_activeGuestThreadYieldRequested = false;
_activeGuestThreadYieldReason = null;
_activeGuestThreadState = _runState;
backend.BindTlsBase(_runContext!);
TlsSetValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
if (_runState is { } state)
{
_prevHostThreadId = Volatile.Read(ref state.HostThreadId);
Volatile.Write(ref state.HostThreadId, unchecked((int)GetCurrentThreadId()));
}
if (_runAffinityMask != 0)
{
backend.ApplyGuestThreadAffinity(_runAffinityMask);
}
_runPrologueFailed = false;
if (LogThreadMode)
{
TraceThreadMode(
$"worker_enter guest=0x{_runGuestThreadHandle:X16} stub=0x{(ulong)_runEntryStub:X16}");
}
return _runEntryStub;
}
private void ExitRun(int nativeResult)
{
_runNativeResult = nativeResult;
_runYieldRequested = _activeGuestThreadYieldRequested;
_runYieldReason = _activeGuestThreadYieldReason;
_runForcedExit = _activeForcedGuestExit;
if (!_entered)
{
return;
}
_entered = false;
if (_runState is { } state)
{
Volatile.Write(ref state.HostThreadId, _prevHostThreadId);
}
TlsSetValue(_backend._hostRspSlotTlsIndex, _prevHostRspSlot);
GuestThreadExecution.RestoreGuestThread(_prevGuestThreadHandle);
_activeExecutionBackend = _prevBackend;
_activeCpuContext = _prevContext;
_activeEntryReturnSentinelRip = _prevSentinel;
_activeGuestReturnSlotAddress = _prevReturnSlot;
_activeForcedGuestExit = _prevForcedExit;
_activeGuestThreadYieldRequested = _prevYieldRequested;
_activeGuestThreadYieldReason = _prevYieldReason;
_activeGuestThreadState = _prevState;
_prevBackend = null;
_prevContext = null;
_prevState = null;
_prevYieldReason = null;
if (LogThreadMode)
{
TraceThreadMode(
$"worker_exit guest=0x{_runGuestThreadHandle:X16} result=0x{nativeResult:X8} yield={_runYieldRequested}");
}
}
public void Dispose()
{
if (_controlBlock != null)
{
*(int*)_controlBlock = 1;
}
try
{
_workAvailable.Set();
}
catch (ObjectDisposedException)
{
}
var exited = _threadHandle == 0;
if (_threadHandle != 0)
{
exited = WaitForSingleObject(_threadHandle, 1000u) == 0u;
CloseHandle(_threadHandle);
_threadHandle = 0;
}
if (!exited)
{
// The worker is still parked in guest code (teardown should have unwound
// it first). Leak the stub, control block, events and GC handle rather
// than have the thread execute freed memory.
Console.Error.WriteLine(
$"[LOADER][WARN] Native guest worker tid={_nativeThreadId} did not stop; leaking its run loop.");
return;
}
if (_loopStub != null)
{
VirtualFree(_loopStub, 0u, 32768u);
_loopStub = null;
}
if (_controlBlock != null)
{
VirtualFree(_controlBlock, 0u, 32768u);
_controlBlock = null;
}
if (_selfHandle.IsAllocated)
{
_selfHandle.Free();
}
_workAvailable.Dispose();
_workCompleted.Dispose();
}
}
}
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
@@ -48,6 +49,13 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
ulong Arg1,
ulong Arg2);
private readonly record struct DeferredBootstrapTraceEntry(
long DispatchIndex,
ulong Op,
ulong SymbolPointer,
ulong OutputPointer,
ulong ReturnRip);
#pragma warning disable CS0649
private struct EXCEPTION_POINTERS
{
@@ -232,7 +240,31 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private KeyValuePair<string, ulong>[] _runtimeSymbolsByAddress = Array.Empty<KeyValuePair<string, ulong>>();
private readonly Dictionary<string, ulong> _runtimeSymbolsByName = new Dictionary<string, ulong>(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, ulong> _runtimeSymbolsByName =
new(StringComparer.Ordinal);
// Keep in sync with SelfLoader import-stub mapping constants.
private const ulong ImportStubRegionCanonicalBase = 0x0000_7000_0000_0000UL;
private const ulong ImportStubRegionAddressStride = 0x0000_0000_0100_0000UL;
private const ulong LazyImportStubSlotSize = 0x10;
private const ulong ImportStubRegionPageSize = 0x1000UL;
private const string KernelDynlibDlsymAerolibNid = "LwG8g3niqwA";
private readonly object _lazyDlsymStubGate = new();
private readonly Dictionary<string, ulong> _lazyDlsymStubCache = new(StringComparer.Ordinal);
private ulong _lazyImportStubPoolBase;
private ulong _lazyImportStubNextSlot;
private ulong _lazyImportStubPoolLimit;
private bool _lazyImportStubPoolMapped;
private readonly RecentImportTraceEntry[] _recentImportTrace = new RecentImportTraceEntry[64];
@@ -240,6 +272,14 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private int _recentImportTraceWriteIndex;
private readonly DeferredBootstrapTraceEntry[] _deferredBootstrapTrace = new DeferredBootstrapTraceEntry[32];
private int _deferredBootstrapTraceCount;
private int _deferredBootstrapTraceWriteIndex;
private readonly object _deferredBootstrapTraceGate = new();
private readonly string[] _distinctImportNidHistory = new string[128];
private int _distinctImportNidHistoryCount;
@@ -424,6 +464,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private void ThreadMain()
{
var previousGuestThreadHandle = GuestThreadExecution.EnterGuestThread(_guestThreadHandle);
if (LogThreadMode)
{
TraceThreadMode($"runner_start guest=0x{_guestThreadHandle:X16}");
}
try
{
while (true)
@@ -434,6 +478,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
return;
}
if (LogThreadMode)
{
_threadModeCycleId = Interlocked.Increment(ref _threadModeCycleCounter);
TraceThreadMode($"runner_run guest=0x{_guestThreadHandle:X16}");
}
try
{
_work?.Invoke();
@@ -446,6 +495,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
}
finally
{
if (LogThreadMode)
{
TraceThreadMode($"runner_stop guest=0x{_guestThreadHandle:X16}");
}
GuestThreadExecution.RestoreGuestThread(previousGuestThreadHandle);
}
}
@@ -469,6 +522,14 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private readonly Queue<GuestThreadState> _readyGuestThreads = new Queue<GuestThreadState>();
// Once set, guest worker threads are unwound to the host at their next import
// dispatch and Pump refuses to start new ones. This must happen before the
// runtime frees trampolines or guest memory: workers that keep running native
// guest code during teardown execute freed pages (observed as an execute-AV in
// a MEM_FREE module region plus a CLR "UnmanagedCallersOnly" fatal from a Pump
// thread entering a freed stub).
private volatile bool _guestTeardownRequested;
private int _readyGuestThreadCount;
private readonly Dictionary<ulong, GuestThreadState> _guestThreads = new Dictionary<ulong, GuestThreadState>();
@@ -567,6 +628,33 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private static int _nestedVehTraceCount;
// SHARPEMU_LOG_THREAD_MODE=1 — GC thread-mode corruption investigation. Traces
// every managed<->guest transition per guest-thread run cycle so the last event
// before a ReversePInvokeBadTransition FailFast identifies the corrupting path.
private static readonly bool LogThreadMode =
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_THREAD_MODE"), "1", StringComparison.Ordinal);
private static long _threadModeCycleCounter;
[ThreadStatic]
private static long _threadModeCycleId;
[ThreadStatic]
private static int _threadModeGatewayDepth;
[ThreadStatic]
private static long _threadModeGatewayCalls;
[ThreadStatic]
private static bool _threadModeGatewayFirstLogged;
private static void TraceThreadMode(string message)
{
Console.Error.WriteLine(
$"[THREADMODE] {message} cycle={_threadModeCycleId} tid={GetCurrentThreadId()} managed={Environment.CurrentManagedThreadId}");
Console.Error.Flush();
}
private const uint MEM_COMMIT = 4096u;
private const uint MEM_RESERVE = 8192u;
@@ -625,7 +713,17 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private unsafe static int CallNativeEntry(void* entry)
{
var nativeEntry = (delegate* unmanaged[Cdecl]<int>)entry;
return nativeEntry();
if (!LogThreadMode)
{
return nativeEntry();
}
_threadModeGatewayFirstLogged = false;
var dispatchesBefore = _threadModeGatewayCalls;
TraceThreadMode($"native_enter entry=0x{(ulong)entry:X16}");
var result = nativeEntry();
TraceThreadMode($"native_exit result=0x{result:X8} dispatches={_threadModeGatewayCalls - dispatchesBefore}");
return result;
}
private unsafe static void WriteCtxU64(void* contextRecord, int offset, ulong value)
@@ -797,8 +895,14 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
result = OrbisGen2Result.ORBIS_GEN2_OK;
LastError = null;
InitializeRuntimeSymbolIndex(runtimeSymbols);
ResetLazyDlsymStubState();
_recentImportTraceCount = 0;
_recentImportTraceWriteIndex = 0;
lock (_deferredBootstrapTraceGate)
{
_deferredBootstrapTraceCount = 0;
_deferredBootstrapTraceWriteIndex = 0;
}
_distinctImportNidHistoryCount = 0;
_distinctImportNidHistoryWriteIndex = 0;
_lastDistinctImportNid = string.Empty;
@@ -824,6 +928,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
_importLoopGuardSeconds = GetImportLoopGuardSeconds();
_entryReturnSentinelRip = 0uL;
_forcedGuestExit = false;
_guestTeardownRequested = false;
_importLoopSignatureCount = 0;
_importLoopSignatureWriteIndex = 0;
_importLoopPatternHits = 0;
@@ -871,6 +976,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
}
finally
{
DrainDeferredBootstrapTraces();
GuestThreadExecution.Scheduler = previousGuestThreadScheduler;
Console.Error.WriteLine("[LOADER][INFO] === Execute END (LastError: " + (LastError ?? "null") + ") ===");
}
@@ -1937,6 +2043,36 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
byte* code = (byte*)ptr;
int offset = 0;
// Native pre-filter: these exception codes are raised while the thread can be in
// cooperative GC mode (a C# throw is RaiseException(0xE0434352) on the throwing
// thread; FailFast/stack-overflow arrive mid-runtime-failure). Entering the managed
// handler then trips the CLR's reverse-P/Invoke check and kills the process with
// "Invalid Program: attempted to call a UnmanagedCallersOnly method from managed
// code" — this is why no managed throw (even one with a catch handler) ever
// survived inside the emulator. Continue the handler search without touching
// managed code; the CLR's own VEH handles its exceptions. MSVC C++ exceptions
// (Vulkan drivers, host CRT) are excluded too: the managed handler only ever
// returned CONTINUE_SEARCH for them.
ReadOnlySpan<uint> nonManagedExceptionCodes = [0xE0434352u, MSVC_CPP_EXCEPTION, 0xC0000409u, 0xC00000FDu];
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x01); // mov rax, [rcx] (ExceptionRecord*)
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x00); // mov eax, [rax] (ExceptionCode)
var passJumpOffsets = stackalloc int[nonManagedExceptionCodes.Length];
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
{
EmitByte(code, ref offset, 0x3D); // cmp eax, imm32
EmitUInt32(code, ref offset, nonManagedExceptionCodes[i]);
EmitByte(code, ref offset, 0x74); // je pass
passJumpOffsets[i] = offset;
EmitByte(code, ref offset, 0x00);
}
EmitByte(code, ref offset, 0xEB); EmitByte(code, ref offset, 0x03); // jmp over pass block
int passOffset = offset;
EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0); // pass: xor eax, eax (EXCEPTION_CONTINUE_SEARCH)
EmitByte(code, ref offset, 0xC3); // ret
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
{
code[passJumpOffsets[i]] = checked((byte)(passOffset - (passJumpOffsets[i] + 1)));
}
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x54); // push r12
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x55); // push r13
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE4); // mov r12, rsp
@@ -2621,6 +2757,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
public void Pump(CpuContext callerContext, string reason)
{
_ = callerContext;
if (_guestTeardownRequested)
{
return;
}
var runSynchronously = string.Equals(reason, "entry_return", StringComparison.Ordinal);
WakeExpiredBlockedGuestThreads();
if (Volatile.Read(ref _readyGuestThreadCount) == 0)
@@ -3145,6 +3285,45 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
continuationThread.Join();
}
/// <summary>
/// Parks every guest worker thread before the caller starts freeing executable
/// memory. Workers unwind to the host at their next import dispatch (see the
/// teardown check in DispatchImport); this waits for their host threads to
/// finish within <paramref name="timeoutMs"/>. Returns false when at least one
/// worker is still running — the caller must then leak its executable stubs
/// rather than free memory a live thread may still execute.
/// </summary>
private bool RequestGuestThreadTeardown(int timeoutMs)
{
_guestTeardownRequested = true;
Thread[] hostThreads;
lock (_guestThreadGate)
{
_readyGuestThreads.Clear();
Interlocked.Exchange(ref _readyGuestThreadCount, 0);
hostThreads = _guestThreads.Values
.Select(static thread => thread.HostThread)
.Where(static host => host is not null && host != Thread.CurrentThread && host.IsAlive)
.Cast<Thread>()
.ToArray();
}
var deadline = Environment.TickCount64 + timeoutMs;
var allStopped = true;
foreach (var host in hostThreads)
{
var remaining = (int)Math.Max(1L, deadline - Environment.TickCount64);
if (!host.Join(remaining) && host.IsAlive)
{
allStopped = false;
Console.Error.WriteLine(
$"[LOADER][WARN] Guest worker host thread '{host.Name}' still running after teardown wait.");
}
}
return allStopped;
}
private void ClearGuestThreads()
{
GuestContinuationRunner[] runners;
@@ -3424,6 +3603,13 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
ApplyGuestThreadAffinity(thread.AffinityMask);
Volatile.Write(ref thread.HostThreadId, unchecked((int)GetCurrentThreadId()));
_activeGuestThreadState = thread;
if (LogThreadMode)
{
_threadModeCycleId = Interlocked.Increment(ref _threadModeCycleCounter);
TraceThreadMode(
$"cycle_start name='{thread.Name}' guest=0x{thread.ThreadHandle:X16} reason={reason} " +
$"rsp_slot=0x{(ulong)TlsGetValue(_hostRspSlotTlsIndex):X}");
}
try
{
LastError = null;
@@ -3498,6 +3684,13 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
}
finally
{
if (LogThreadMode)
{
TraceThreadMode(
$"cycle_end name='{thread.Name}' state={thread.State} " +
$"imports={Interlocked.Read(ref thread.ImportCount)} " +
$"rsp_slot=0x{(ulong)TlsGetValue(_hostRspSlotTlsIndex):X}");
}
_activeGuestThreadState = previousGuestThreadState;
Volatile.Write(ref thread.HostThreadId, 0);
GuestThreadExecution.RestoreGuestThread(previousGuestThreadHandle);
@@ -3572,173 +3765,175 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
var previousYieldRequested = _activeGuestThreadYieldRequested;
var previousYieldReason = _activeGuestThreadYieldReason;
nint previousHostRspSlotValue = TlsGetValue(_hostRspSlotTlsIndex);
if (LogThreadMode)
{
TraceThreadMode(
$"entry_setup name='{name}' entry=0x{entryPoint:X16} stub=0x{(ulong)ptr:X16} " +
$"guest_rsp=0x{context[CpuRegister.Rsp]:X16} rsp_slot_prev=0x{(ulong)previousHostRspSlotValue:X}");
}
try
{
_activeExecutionBackend = this;
_activeCpuContext = context;
_activeEntryReturnSentinelRip = 0;
_activeGuestReturnSlotAddress = 0;
_activeForcedGuestExit = false;
_activeGuestThreadYieldRequested = false;
_activeGuestThreadYieldReason = null;
BindTlsBase(context);
byte* ptr2 = (byte*)ptr;
ulong hostRspSlot = (ulong)ptr + stubSize - 16uL;
int offset = 0;
ptr2[offset++] = 83;
ptr2[offset++] = 85;
ptr2[offset++] = 87;
ptr2[offset++] = 86;
ptr2[offset++] = 65;
ptr2[offset++] = 84;
ptr2[offset++] = 65;
ptr2[offset++] = 85;
ptr2[offset++] = 65;
ptr2[offset++] = 86;
ptr2[offset++] = 65;
ptr2[offset++] = 87;
EmitHostNonvolatileXmmSave(ptr2, ref offset);
ptr2[offset++] = 73;
ptr2[offset++] = 186;
*(ulong*)(ptr2 + offset) = hostRspSlot;
offset += 8;
ptr2[offset++] = 73;
ptr2[offset++] = 137;
ptr2[offset++] = 34;
ptr2[offset++] = 72;
ptr2[offset++] = 184;
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rsp];
offset += 8;
ptr2[offset++] = 72;
ptr2[offset++] = 137;
ptr2[offset++] = 196;
ptr2[offset++] = 72;
ptr2[offset++] = 131;
ptr2[offset++] = 236;
ptr2[offset++] = 8;
ptr2[offset++] = 72;
ptr2[offset++] = 189;
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rbp];
offset += 8;
ptr2[offset++] = 72;
ptr2[offset++] = 184;
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rdi];
offset += 8;
ptr2[offset++] = 72;
ptr2[offset++] = 137;
ptr2[offset++] = 199;
ptr2[offset++] = 72;
ptr2[offset++] = 184;
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rsi];
offset += 8;
ptr2[offset++] = 72;
ptr2[offset++] = 137;
ptr2[offset++] = 198;
ptr2[offset++] = 72;
ptr2[offset++] = 184;
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rdx];
offset += 8;
ptr2[offset++] = 72;
ptr2[offset++] = 137;
ptr2[offset++] = 194;
ptr2[offset++] = 72;
ptr2[offset++] = 184;
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rcx];
offset += 8;
ptr2[offset++] = 72;
ptr2[offset++] = 137;
ptr2[offset++] = 193;
ptr2[offset++] = 72;
ptr2[offset++] = 184;
*(ulong*)(ptr2 + offset) = entryPoint;
offset += 8;
ptr2[offset++] = byte.MaxValue;
ptr2[offset++] = 208;
int sentinelOffset = offset + 4;
ptr2[offset++] = 72;
ptr2[offset++] = 131;
ptr2[offset++] = 196;
ptr2[offset++] = 8;
ptr2[offset++] = 73;
ptr2[offset++] = 186;
*(ulong*)(ptr2 + offset) = hostRspSlot;
offset += 8;
ptr2[offset++] = 73;
ptr2[offset++] = 139;
ptr2[offset++] = 34;
EmitHostNonvolatileXmmRestore(ptr2, ref offset);
ptr2[offset++] = 65;
ptr2[offset++] = 95;
ptr2[offset++] = 65;
ptr2[offset++] = 94;
ptr2[offset++] = 65;
ptr2[offset++] = 93;
ptr2[offset++] = 65;
ptr2[offset++] = 92;
ptr2[offset++] = 94;
ptr2[offset++] = 95;
ptr2[offset++] = 93;
ptr2[offset++] = 91;
ptr2[offset++] = 195;
ulong sentinel = (ulong)ptr + (ulong)sentinelOffset;
ActiveEntryReturnSentinelRip = (ulong)_guestReturnStub;
_activeGuestReturnSlotAddress = context[CpuRegister.Rsp] - 16uL;
if (!context.TryWriteUInt64(context[CpuRegister.Rsp], sentinel))
{
reason = $"failed to patch guest thread return sentinel at 0x{context[CpuRegister.Rsp]:X16}";
return GuestNativeCallExitReason.Exception;
}
uint oldProtect = default(uint);
if (!VirtualProtect(ptr, stubSize, 64u, &oldProtect))
{
reason = $"VirtualProtect failed for guest thread entry stub at 0x{(nint)ptr:X16}";
return GuestNativeCallExitReason.Exception;
}
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
ActiveGuestThreadYieldRequested = false;
ActiveGuestThreadYieldReason = null;
try
{
var nativeReturn = CallNativeEntry(ptr);
if (ActiveGuestThreadYieldRequested)
{
reason = ActiveGuestThreadYieldReason ?? "guest thread blocked";
return GuestNativeCallExitReason.Blocked;
}
if (ActiveForcedGuestExit)
{
reason = LastError ?? "guest thread forced exit";
return GuestNativeCallExitReason.ForcedExit;
}
reason = $"returned 0x{nativeReturn:X8}";
return GuestNativeCallExitReason.Returned;
}
catch (AccessViolationException ex)
{
reason = "access violation: " + ex.Message;
return GuestNativeCallExitReason.Exception;
}
catch (Exception ex)
{
reason = ex.GetType().Name + ": " + ex.Message;
return GuestNativeCallExitReason.Exception;
}
}
finally
{
TlsSetValue(_hostRspSlotTlsIndex, previousHostRspSlotValue);
RestoreActiveExecutionThread(
previousActiveBackend,
previousActiveContext,
previousSentinel,
previousReturnSlotAddress,
previousForcedExit,
previousYieldRequested,
previousYieldReason);
VirtualFree(ptr, 0u, 32768u);
}
}
{
_activeExecutionBackend = this;
_activeCpuContext = context;
_activeEntryReturnSentinelRip = 0;
_activeGuestReturnSlotAddress = 0;
_activeForcedGuestExit = false;
_activeGuestThreadYieldRequested = false;
_activeGuestThreadYieldReason = null;
BindTlsBase(context);
byte* ptr2 = (byte*)ptr;
ulong hostRspSlot = (ulong)ptr + stubSize - 16uL;
int offset = 0;
ptr2[offset++] = 83;
ptr2[offset++] = 85;
ptr2[offset++] = 87;
ptr2[offset++] = 86;
ptr2[offset++] = 65;
ptr2[offset++] = 84;
ptr2[offset++] = 65;
ptr2[offset++] = 85;
ptr2[offset++] = 65;
ptr2[offset++] = 86;
ptr2[offset++] = 65;
ptr2[offset++] = 87;
EmitHostNonvolatileXmmSave(ptr2, ref offset);
ptr2[offset++] = 73;
ptr2[offset++] = 186;
*(ulong*)(ptr2 + offset) = hostRspSlot;
offset += 8;
ptr2[offset++] = 73;
ptr2[offset++] = 137;
ptr2[offset++] = 34;
ptr2[offset++] = 72;
ptr2[offset++] = 184;
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rsp];
offset += 8;
ptr2[offset++] = 72;
ptr2[offset++] = 137;
ptr2[offset++] = 196;
ptr2[offset++] = 72;
ptr2[offset++] = 131;
ptr2[offset++] = 236;
ptr2[offset++] = 8;
ptr2[offset++] = 72;
ptr2[offset++] = 189;
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rbp];
offset += 8;
ptr2[offset++] = 72;
ptr2[offset++] = 184;
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rdi];
offset += 8;
ptr2[offset++] = 72;
ptr2[offset++] = 137;
ptr2[offset++] = 199;
ptr2[offset++] = 72;
ptr2[offset++] = 184;
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rsi];
offset += 8;
ptr2[offset++] = 72;
ptr2[offset++] = 137;
ptr2[offset++] = 198;
ptr2[offset++] = 72;
ptr2[offset++] = 184;
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rdx];
offset += 8;
ptr2[offset++] = 72;
ptr2[offset++] = 137;
ptr2[offset++] = 194;
ptr2[offset++] = 72;
ptr2[offset++] = 184;
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rcx];
offset += 8;
ptr2[offset++] = 72;
ptr2[offset++] = 137;
ptr2[offset++] = 193;
ptr2[offset++] = 72;
ptr2[offset++] = 184;
*(ulong*)(ptr2 + offset) = entryPoint;
offset += 8;
ptr2[offset++] = byte.MaxValue;
ptr2[offset++] = 208;
int sentinelOffset = offset + 4;
ptr2[offset++] = 72;
ptr2[offset++] = 131;
ptr2[offset++] = 196;
ptr2[offset++] = 8;
ptr2[offset++] = 73;
ptr2[offset++] = 186;
*(ulong*)(ptr2 + offset) = hostRspSlot;
offset += 8;
ptr2[offset++] = 73;
ptr2[offset++] = 139;
ptr2[offset++] = 34;
EmitHostNonvolatileXmmRestore(ptr2, ref offset);
ptr2[offset++] = 65;
ptr2[offset++] = 95;
ptr2[offset++] = 65;
ptr2[offset++] = 94;
ptr2[offset++] = 65;
ptr2[offset++] = 93;
ptr2[offset++] = 65;
ptr2[offset++] = 92;
ptr2[offset++] = 94;
ptr2[offset++] = 95;
ptr2[offset++] = 93;
ptr2[offset++] = 91;
ptr2[offset++] = 195;
ulong sentinel = (ulong)ptr + (ulong)sentinelOffset;
ActiveEntryReturnSentinelRip = (ulong)_guestReturnStub;
_activeGuestReturnSlotAddress = context[CpuRegister.Rsp] - 16uL;
if (!context.TryWriteUInt64(context[CpuRegister.Rsp], sentinel))
{
reason = $"failed to patch guest thread return sentinel at 0x{context[CpuRegister.Rsp]:X16}";
return GuestNativeCallExitReason.Exception;
}
uint oldProtect = default(uint);
if (!VirtualProtect(ptr, stubSize, 64u, &oldProtect))
{
reason = $"VirtualProtect failed for guest thread entry stub at 0x{(nint)ptr:X16}";
return GuestNativeCallExitReason.Exception;
}
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
ActiveGuestThreadYieldRequested = false;
ActiveGuestThreadYieldReason = null;
var nativeReturn = RunGuestEntryStub(ptr, hostRspSlot);
if (ActiveGuestThreadYieldRequested)
{
reason = ActiveGuestThreadYieldReason ?? "guest thread blocked";
return GuestNativeCallExitReason.Blocked;
}
if (ActiveForcedGuestExit)
{
reason = LastError ?? "guest thread forced exit";
return GuestNativeCallExitReason.ForcedExit;
}
reason = $"returned 0x{nativeReturn:X8}";
return GuestNativeCallExitReason.Returned;
}
catch (AccessViolationException ex)
{
reason = "access violation: " + ex.Message;
return GuestNativeCallExitReason.Exception;
}
catch (Exception ex)
{
reason = ex.GetType().Name + ": " + ex.Message;
return GuestNativeCallExitReason.Exception;
}
finally
{
TlsSetValue(_hostRspSlotTlsIndex, previousHostRspSlotValue);
RestoreActiveExecutionThread(
previousActiveBackend,
previousActiveContext,
previousSentinel,
previousReturnSlotAddress,
previousForcedExit,
previousYieldRequested,
previousYieldReason);
VirtualFree(ptr, 0u, 32768u);
}
}
private unsafe GuestNativeCallExitReason ExecuteGuestContinuationEntry(
CpuContext context,
@@ -3768,6 +3963,12 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
var previousYieldRequested = _activeGuestThreadYieldRequested;
var previousYieldReason = _activeGuestThreadYieldReason;
nint previousHostRspSlotValue = TlsGetValue(_hostRspSlotTlsIndex);
if (LogThreadMode)
{
TraceThreadMode(
$"continuation_setup name='{name}' resume=0x{entryPoint:X16} stub=0x{(ulong)ptr:X16} " +
$"guest_rsp=0x{context[CpuRegister.Rsp]:X16} rsp_slot_prev=0x{(ulong)previousHostRspSlotValue:X}");
}
try
{
_activeExecutionBackend = this;
@@ -3836,12 +4037,12 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
return GuestNativeCallExitReason.Exception;
}
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
ActiveGuestThreadYieldRequested = false;
ActiveGuestThreadYieldReason = null;
try
{
var nativeReturn = CallNativeEntry(ptr);
var nativeReturn = RunGuestEntryStub(ptr, hostRspSlot);
if (ActiveGuestThreadYieldRequested)
{
reason = ActiveGuestThreadYieldReason ?? "guest thread blocked";
@@ -4117,7 +4318,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
int num6 = -1;
try
{
num6 = CallNativeEntry(ptr);
num6 = RunGuestEntryStub(ptr, num2);
Console.Error.WriteLine($"[LOADER][INFO] Guest returned: {num6}");
PumpUntilGuestThreadsIdle(context, "entry_return");
}
@@ -4144,6 +4345,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
LastError = "Detected repeating import loop and forced guest unwind to host.";
}
Console.Error.WriteLine("[LOADER][ERROR] " + LastError);
RequestGuestThreadTeardown(3000);
return false;
}
if (num6 == 0)
@@ -4158,6 +4360,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
LastError = $"Guest entry point returned non-zero: {num6}";
}
Console.Error.WriteLine("[LOADER][ERROR] " + LastError);
RequestGuestThreadTeardown(3000);
return false;
}
finally
@@ -4300,13 +4503,14 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
ulong rsp = cpuContext[CpuRegister.Rsp];
Console.Error.WriteLine($"[LOADER][ERROR] Stall snapshot: rip=0x{cpuContext.Rip:X16} rsp=0x{rsp:X16} rbp=0x{cpuContext[CpuRegister.Rbp]:X16} rax=0x{cpuContext[CpuRegister.Rax]:X16} rbx=0x{cpuContext[CpuRegister.Rbx]:X16} rcx=0x{cpuContext[CpuRegister.Rcx]:X16} rdx=0x{cpuContext[CpuRegister.Rdx]:X16} rsi=0x{cpuContext[CpuRegister.Rsi]:X16} rdi=0x{cpuContext[CpuRegister.Rdi]:X16}");
ulong num = cpuContext.Rip & 0xFFFFFFFFFFFFFFF0uL;
for (int i = 0; i < _importEntries.Length; i++)
var importEntries = _importEntries;
for (int i = 0; i < importEntries.Length; i++)
{
if (_importEntries[i].Address != num)
if (importEntries[i].Address != num)
{
continue;
}
string text = _importEntries[i].Nid;
string text = importEntries[i].Nid;
if (_moduleManager.TryGetExport(text, out ExportedFunction export))
{
Console.Error.WriteLine($"[LOADER][ERROR] Stall import-stub: rip=0x{num:X16} nid={text} -> {export.LibraryName}:{export.Name}");
@@ -4482,9 +4686,22 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
public unsafe void Dispose()
{
if (!RequestGuestThreadTeardown(2000))
{
// A guest worker is still executing native code; freeing the trampolines,
// exception-handler stubs, or GC handles under it turns process exit into
// an execute-AV / CLR fatal. Leak them — the process is going away anyway.
Console.Error.WriteLine(
"[LOADER][WARN] Skipping executable stub teardown: guest worker threads are still running.");
return;
}
// Native guest workers park idle once every guest thread has unwound; stop
// them before any executable stub or TLS index they reference is freed.
DisposeNativeGuestExecutors();
ClearImportHandlerTrampolines();
_importEntries = Array.Empty<ImportStubEntry>();
_runtimeSymbolsByName.Clear();
ResetLazyDlsymStubState();
_importNidHashCache.Clear();
StopStallWatchdog();
if (_exceptionHandler != 0)
+1 -1
View File
@@ -140,7 +140,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
LastMilestoneLog = null;
KernelModuleRegistry.Reset();
var image = LoadImage(normalizedEbootPath);
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version, BuildInfo.CommitSha);
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
LogAppBundleInfo(normalizedEbootPath, image);
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
+4 -1
View File
@@ -65,7 +65,10 @@
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
},
"sharpemu.hle": {
"type": "Project"
"type": "Project",
"dependencies": {
"SharpEmu.Logging": "[1.0.0, )"
}
},
"sharpemu.libs": {
"type": "Project",
+150
View File
@@ -0,0 +1,150 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Threading;
using System.Collections.Specialized;
namespace SharpEmu.GUI;
public sealed class ConsoleWindow : Window
{
private readonly AvaloniaList<LogLine> _sourceLines;
private readonly AvaloniaList<LogLine> _visibleLines = new();
private readonly ListBox _list;
private readonly TextBox _searchBox;
private readonly CheckBox _autoScrollCheck;
public ConsoleWindow(
AvaloniaList<LogLine> lines,
Action clear,
bool autoScroll)
{
_sourceLines = lines;
Title = "SharpEmu Console";
Width = 980;
Height = 620;
MinWidth = 520;
MinHeight = 320;
Background = new SolidColorBrush(Color.Parse("#0D1017"));
Icon = new WindowIcon(AssetLoader.Open(new Uri("avares://SharpEmu.GUI/Assets/SharpEmu.ico")));
_searchBox = new TextBox { Watermark = "Search...", Width = 320, Margin = new Thickness(0, 0, 12, 0) };
_autoScrollCheck = new CheckBox
{
Content = "Auto-scroll",
IsChecked = autoScroll,
FontSize = 12,
Margin = new Thickness(0, 0, 12, 0),
VerticalAlignment = VerticalAlignment.Center,
};
var copyButton = new Button
{
Classes = { "ghost" },
Content = "Copy",
Padding = new Thickness(10, 4),
Margin = new Thickness(0, 0, 8, 0),
};
var clearButton = new Button { Classes = { "ghost" }, Content = "Clear", Padding = new Thickness(10, 4) };
copyButton.Click += async (_, _) => await CopyAsync();
clearButton.Click += (_, _) => clear();
_searchBox.TextChanged += (_, _) => RefreshVisibleLines();
_list = new ListBox
{
Classes = { "console" },
ItemsSource = _visibleLines,
BorderThickness = new Thickness(1),
BorderBrush = new SolidColorBrush(Color.Parse("#232B3A")),
ItemTemplate = new FuncDataTemplate<LogLine>((_, _) =>
{
var text = new TextBlock { TextWrapping = TextWrapping.NoWrap };
text.Bind(TextBlock.TextProperty, new Binding(nameof(LogLine.Text)));
text.Bind(TextBlock.ForegroundProperty, new Binding(nameof(LogLine.Brush)));
return text;
}),
};
Content = new Grid
{
Margin = new Thickness(12),
RowDefinitions = new RowDefinitions("Auto,*"),
Children =
{
new Grid
{
Margin = new Thickness(0, 0, 0, 8),
ColumnDefinitions = new ColumnDefinitions("*,Auto,Auto,Auto,Auto"),
Children =
{
new TextBlock
{
Classes = { "sectionTitle" },
Text = "CONSOLE",
VerticalAlignment = VerticalAlignment.Center,
},
_searchBox.WithGridColumn(1),
_autoScrollCheck.WithGridColumn(2),
copyButton.WithGridColumn(3),
clearButton.WithGridColumn(4),
},
},
_list.WithGridRow(1),
},
};
lines.CollectionChanged += OnLinesChanged;
Closed += (_, _) => lines.CollectionChanged -= OnLinesChanged;
RefreshVisibleLines();
}
private void OnLinesChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
RefreshVisibleLines();
if (_autoScrollCheck.IsChecked == true)
{
Dispatcher.UIThread.Post(() => (_list.Scroll as ScrollViewer)?.ScrollToEnd());
}
}
private void RefreshVisibleLines()
{
var query = _searchBox.Text ?? string.Empty;
_visibleLines.Clear();
_visibleLines.AddRange(string.IsNullOrWhiteSpace(query)
? _sourceLines
: _sourceLines.Where(line => line.Text.Contains(query, StringComparison.OrdinalIgnoreCase)));
}
private async Task CopyAsync()
{
if (_visibleLines.Count == 0 || Clipboard is null)
{
return;
}
await Clipboard.SetTextAsync(string.Join(Environment.NewLine, _visibleLines.Select(line => line.Text)));
}
}
file static class GridExtensions
{
public static T WithGridColumn<T>(this T control, int column) where T : Control
{
Grid.SetColumn(control, column);
return control;
}
public static T WithGridRow<T>(this T control, int row) where T : Control
{
Grid.SetRow(control, row);
return control;
}
}
+2 -22
View File
@@ -168,8 +168,7 @@ internal sealed class EmulatorProcess : IDisposable
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;
PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF;
mitigationPolicies = Marshal.AllocHGlobal(sizeof(ulong) * 2);
Marshal.WriteInt64(mitigationPolicies, unchecked((long)policy1));
@@ -200,29 +199,10 @@ internal sealed class EmulatorProcess : IDisposable
ref startupInfoEx,
out var processInfo);
if (!created)
{
// Some mitigation policy bits (e.g. XFG) are not supported on
// older Windows builds. Mirror the CLI's behavior and fall back
// to launching without the mitigation attribute list.
startupInfoEx.lpAttributeList = 0;
created = CreateProcessW(
exePath,
new StringBuilder(BuildCommandLine(exePath, arguments)),
0,
0,
true,
CREATE_NO_WINDOW,
0,
currentDirectory,
ref startupInfoEx,
out processInfo);
}
if (!created)
{
var error = Marshal.GetLastWin32Error();
throw new Win32Exception(error, $"Failed to start '{exePath}' (Win32 error {error}: {new Win32Exception(error).Message}).");
throw new Win32Exception(error, $"Failed to start '{exePath}' with CET/CFG mitigation disabled (Win32 error {error}: {new Win32Exception(error).Message}).");
}
CloseHandle(processInfo.hThread);
+9 -6
View File
@@ -14,7 +14,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaChromeHints="PreferSystemChrome"
ExtendClientAreaTitleBarHeightHint="44"
Icon="avares://SharpEmu.GUI/Assets/SharpEmu.ico">
Icon="avares://SharpEmu.GUI/Assets/SharpEmu.ico"
KeyDown="OnKeyDown">
<Grid RowDefinitions="44,*,32">
@@ -159,15 +160,17 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Border Grid.Row="2" x:Name="ConsolePanel" Classes="card" Padding="0" Height="240"
Margin="0,12,0,0" IsVisible="False">
<Grid RowDefinitions="Auto,*">
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto, Auto" Margin="16,12,16,8">
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto,Auto,Auto" Margin="16,12,16,8">
<TextBlock Classes="sectionTitle" Text="CONSOLE" VerticalAlignment="Center" />
<TextBox Grid.Column="1" FontSize="12" Margin="0,0,12,0" x:Name="ConsoleSearchBox"
Watermark="Search..." MaxWidth="5500" />
<TextBox Grid.Column="1" FontSize="12" Margin="0,0,12,0" x:Name="ConsoleSearchBox"
Watermark="Search..." Width="320" />
<CheckBox Grid.Column="2" x:Name="AutoScrollCheck" Content="Auto-scroll" IsChecked="True"
FontSize="12" Margin="0,0,12,0" />
<Button Grid.Column="3" x:Name="CopyLogButton" Classes="ghost" Content="Copy" FontSize="12"
<Button Grid.Column="3" x:Name="DetachConsoleButton" Classes="ghost" Content="Split" FontSize="12"
Padding="10,4" Margin="0,0,8,0" />
<Button Grid.Column="4" x:Name="ClearLogButton" Classes="ghost" Content="Clear" FontSize="12"
<Button Grid.Column="4" x:Name="CopyLogButton" Classes="ghost" Content="Copy" FontSize="12"
Padding="10,4" Margin="0,0,8,0" />
<Button Grid.Column="5" x:Name="ClearLogButton" Classes="ghost" Content="Clear" FontSize="12"
Padding="10,4" />
</Grid>
<ListBox Grid.Row="1" x:Name="ConsoleList" Classes="console" BorderThickness="0,1,0,0"
+64 -14
View File
@@ -8,6 +8,7 @@ using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using Avalonia.VisualTree;
@@ -41,6 +42,7 @@ public partial class MainWindow : Window
private GuiSettings _settings = new();
private EmulatorProcess? _emulator;
private ConsoleWindow? _consoleWindow;
private StreamWriter? _fileLog;
private readonly SndPreviewPlayer _sndPreview = new();
private string? _emulatorExePath;
@@ -97,8 +99,9 @@ public partial class MainWindow : Window
StopButton.Click += (_, _) => StopEmulator();
ClearLogButton.Click += (_, _) => _consoleLines.Clear();
CopyLogButton.Click += async (_, _) => await CopyConsoleAsync();
DetachConsoleButton.Click += (_, _) => ShowConsoleWindow();
OptionsToggle.IsCheckedChanged += (_, _) => OptionsPanel.IsVisible = OptionsToggle.IsChecked == true;
ConsoleToggle.IsCheckedChanged += (_, _) => ConsolePanel.IsVisible = ConsoleToggle.IsChecked == true;
ConsoleToggle.IsCheckedChanged += (_, _) => ConsolePanel.IsVisible = ConsoleToggle.IsChecked == true && _consoleWindow is null;
SelectLogFilePathButton.Click += async (_, _) => await SelectFilePathAsync();
TitleMusicToggle.IsCheckedChanged += (_, _) => OnTitleMusicToggled();
DiscordToggle.IsCheckedChanged += (_, _) =>
@@ -292,6 +295,34 @@ public partial class MainWindow : Window
}
}
private void OnKeyDown(object sender, KeyEventArgs args)
{
args.Handled = true;
switch (args.Key)
{
case Key.F11:
OnWindowFullScreen(this, new RoutedEventArgs());
break;
default:
args.Handled = false;
break;
}
}
private void OnWindowFullScreen(object sender, RoutedEventArgs args)
{
if (WindowState == WindowState.FullScreen)
{
WindowState = WindowState.Normal;
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.PreferSystemChrome;
}
else
{
WindowState = WindowState.FullScreen;
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.NoChrome;
}
}
private void OnWindowClosing()
{
ReadControlsIntoSettings();
@@ -300,6 +331,7 @@ public partial class MainWindow : Window
_gamepadTimer.Stop();
_sndPreview.Stop();
_discord?.Dispose();
_consoleWindow?.Close();
_emulator?.Dispose();
DropFileLog();
}
@@ -1029,13 +1061,11 @@ public partial class MainWindow : Window
arguments.Add($"--trace-imports={_settings.ImportTraceLimit}");
}
arguments.Add(ebootPath);
_consoleLines.Clear();
ConsoleToggle.IsChecked = true;
// Mirror everything the console pane shows into a log file for the
// duration of the run, regardless of the emulator's log level.
// Let the CLI mirror stdout/stderr itself; it sees loader/native
// diagnostics before the GUI pipe reader can filter or batch them.
DropFileLog();
if (_settings.LogToFile)
{
@@ -1073,18 +1103,14 @@ public partial class MainWindow : Window
if (!string.IsNullOrEmpty(filePath))
{
try
{
_fileLog = new StreamWriter(filePath, append: false);
AppendConsoleLine($"Log file: {filePath}", DimLineBrush);
}
catch (Exception ex)
{
AppendConsoleLine($"Could not open the log file: {ex.Message}", WarningLineBrush);
}
arguments.Add("--log-file");
arguments.Add(filePath);
AppendConsoleLine($"Log file: {filePath}", DimLineBrush);
}
}
arguments.Add(ebootPath);
AppendConsoleLine($"$ SharpEmu {string.Join(' ', arguments)}", DimLineBrush);
var emulator = new EmulatorProcess();
@@ -1441,4 +1467,28 @@ public partial class MainWindow : Window
var text = string.Join(Environment.NewLine, _consoleLines.Select(line => line.Text));
await Clipboard.SetTextAsync(text);
}
private void ShowConsoleWindow()
{
if (_consoleWindow is { } window)
{
window.Activate();
return;
}
ConsoleSearchBox.Text = string.Empty;
ConsoleToggle.IsChecked = false;
ConsolePanel.IsVisible = false;
_consoleWindow = new ConsoleWindow(
_consoleLines,
() => { _consoleLines.Clear(); _allConsoleLines.Clear(); },
AutoScrollCheck.IsChecked == true);
_consoleWindow.Closed += (_, _) =>
{
_consoleWindow = null;
ConsoleToggle.IsChecked = true;
ConsolePanel.IsVisible = true;
};
_consoleWindow.Show(this);
}
}
+6 -4
View File
@@ -3,11 +3,13 @@
using System.Buffers.Binary;
using System.Linq;
using SharpEmu.Logging;
namespace SharpEmu.HLE;
public sealed class Aerolib : ISymbolCatalog
{
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Aerolib");
private static readonly Lazy<Aerolib> _instance = new(() => new Aerolib());
private static readonly Aerolib EmptyCatalog = new Aerolib(empty: true);
@@ -108,14 +110,14 @@ public sealed class Aerolib : ISymbolCatalog
if (resourceName == null)
{
Console.Error.WriteLine("[AEROLIB] Embedded resource 'aerolib.bin' not found");
Log.Error("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");
Log.Error("Failed to open embedded resource stream");
return;
}
@@ -145,11 +147,11 @@ public sealed class Aerolib : ISymbolCatalog
_byExportName[name] = symbol;
}
Console.Error.WriteLine($"[AEROLIB] Loaded {_byNid.Count} NID entries from binary resource");
Log.Info($"Loaded {_byNid.Count} NID entries from binary resource");
}
catch (Exception ex)
{
Console.Error.WriteLine($"[AEROLIB] Failed to load embedded aerolib.bin: {ex.Message}");
Log.Error($"Failed to load embedded aerolib.bin: {ex.Message}", ex);
}
}
+6 -3
View File
@@ -3,11 +3,14 @@
using System.Collections.Concurrent;
using System.Reflection;
using SharpEmu.Logging;
namespace SharpEmu.HLE;
public sealed class ModuleManager : IModuleManager
{
private static readonly SharpEmuLogger Log = SharpEmuLog.For("HLE");
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);
@@ -47,7 +50,7 @@ public sealed class ModuleManager : IModuleManager
var handler = CreateHandler(type, method, instances);
if (!_dispatchTable.TryAdd(exportInfo.Value.Nid, handler))
{
Console.Error.WriteLine($"[HLE] Duplicate NID '{exportInfo.Value.Nid}' ({exportInfo.Value.ExportName}) — already registered, skipping.");
Log.Warning($"Duplicate NID '{exportInfo.Value.Nid}' ({exportInfo.Value.ExportName}) — already registered, skipping.");
continue;
}
@@ -106,7 +109,7 @@ public sealed class ModuleManager : IModuleManager
if (!_dispatchTable.TryGetValue(nid, out var function) || !_exportTable.TryGetValue(nid, out var export))
{
Console.Error.WriteLine($"[HLE] NID '{nid}' not found in dispatch table.");
Log.Warning($"NID '{nid}' not found in dispatch table.");
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
return false;
@@ -114,7 +117,7 @@ public sealed class ModuleManager : IModuleManager
if ((export.Target & context.TargetGeneration) == 0)
{
Console.Error.WriteLine($"[HLE] NID '{nid}' ({export.Name}) found but not implemented for generation {context.TargetGeneration} (targets: {export.Target}).");
Log.Warning($"NID '{nid}' ({export.Name}) found but not implemented for generation {context.TargetGeneration} (targets: {export.Target}).");
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED;
return false;
+4
View File
@@ -12,6 +12,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Aerolib\aerolib.bin" />
</ItemGroup>
+5 -1
View File
@@ -1,6 +1,10 @@
{
"version": 2,
"dependencies": {
"net10.0": {}
"net10.0": {
"sharpemu.logging": {
"type": "Project"
}
}
}
}
+51 -1
View File
@@ -3296,10 +3296,60 @@ public static class AgcExports
globalMemoryBindings,
vertexInputs,
renderTargets,
CreateRenderState(state.CxRegisters, renderTargets.FirstOrDefault()));
ApplyTransparentPremultipliedFillClear(
CreateRenderState(state.CxRegisters, renderTargets.FirstOrDefault()),
textures,
vertexInputs,
pixelEvaluation.InitialScalarRegisters));
return true;
}
private static readonly bool _transparentFillClearEnabled = !string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_TRANSPARENT_FILL_CLEAR"),
"1",
StringComparison.Ordinal);
/// <summary>
/// Chowdren resets its effect layers with an untextured transparent-black
/// fill using premultiplied blending. With One/OneMinusSrcAlpha that draw
/// is otherwise a no-op, causing fog and vignette layers to accumulate.
/// Treat precisely that draw shape as an overwrite.
/// </summary>
private static VulkanGuestRenderState ApplyTransparentPremultipliedFillClear(
VulkanGuestRenderState renderState,
IReadOnlyList<TranslatedImageBinding> textures,
IReadOnlyList<Gen5VertexInputBinding> vertexInputs,
IReadOnlyList<uint> pixelUserData)
{
if (!_transparentFillClearEnabled ||
textures.Count != 0 ||
vertexInputs.Count != 0 ||
pixelUserData.Count < 4 ||
renderState.Blend is not
{
Enable: true,
ColorSrcFactor: 1,
ColorDstFactor: 5,
ColorFunc: 0,
})
{
return renderState;
}
for (var index = 0; index < 4; index++)
{
if ((pixelUserData[index] & 0x7FFF_FFFFu) != 0)
{
return renderState;
}
}
return renderState with
{
Blend = renderState.Blend with { Enable = false },
};
}
private static VulkanGuestIndexBuffer? CreateVulkanIndexBuffer(
CpuContext ctx,
SubmittedDcbState state,
+1 -1
View File
@@ -21,7 +21,7 @@ internal static class AmprFileRegistry
return _hostPathsById.TryGetValue(id, out hostPath!);
}
private static uint ComputeFileId(string guestPath)
internal static uint ComputeFileId(string guestPath)
{
var bytes = System.Text.Encoding.UTF8.GetBytes(guestPath);
+11 -13
View File
@@ -8,9 +8,14 @@ namespace SharpEmu.Libs.Audio;
public static class AudioOut2Exports
{
private const int AudioOut2ContextParamSize = 0x80;
// Sized from guest evidence, not SDK headers: Quake keeps its
// SceAudioOut2ContextParam on the stack with the frame canary at param+0x60,
// and an earlier 0x80-byte ResetParam write zeroed that canary
// (__stack_chk_fail right after sceAudioOut2UserCreate, which silently killed
// the whole audio init). Stay well below 0x60 and only write the prefix we
// populate.
private const int AudioOut2ContextParamSize = 0x30;
private const int AudioOut2ContextMemorySize = 0x10000;
private const int AudioOut2ContextMemoryAlignment = 0x10000;
private static long _nextContextHandle = 1;
private static long _nextUserHandle = 1;
private static int _nextPortId;
@@ -59,20 +64,13 @@ public static class AudioOut2Exports
public static int AudioOut2ContextQueryMemory(CpuContext ctx)
{
var paramAddress = ctx[CpuRegister.Rdi];
var memoryInfoAddress = ctx[CpuRegister.Rsi];
if (paramAddress == 0 || memoryInfoAddress == 0)
var outMemorySizeAddress = ctx[CpuRegister.Rsi];
if (paramAddress == 0 || outMemorySizeAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> memoryInfo = stackalloc byte[0x20];
memoryInfo.Clear();
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x00..], AudioOut2ContextMemorySize);
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x08..], AudioOut2ContextMemoryAlignment);
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x10..], AudioOut2ContextMemorySize);
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x18..], AudioOut2ContextMemoryAlignment);
return ctx.Memory.TryWrite(memoryInfoAddress, memoryInfo)
return ctx.TryWriteUInt64(outMemorySizeAddress, AudioOut2ContextMemorySize)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
+104
View File
@@ -0,0 +1,104 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.DiscMap;
/// <summary>
/// HLE implementation of libSceDiscMap, ported from Kyty's LibDiscMap.cpp
/// (https://github.com/InoriRus/Kyty, MIT). Titles installed to internal
/// storage query these entry points on nearly every file access to decide
/// whether a read must be redirected to the Blu-ray drive. Reporting every
/// request as already resident on HDD lets the regular file system path
/// service the read.
/// </summary>
public static class DiscMapExports
{
private const int DiscMapErrorInvalidArgument = unchecked((int)0x81100001);
private const int DiscMapErrorLocationNotMapped = unchecked((int)0x81100002);
private const int DiscMapErrorFileNotFound = unchecked((int)0x81100003);
private const int DiscMapErrorNoBitmapInfo = unchecked((int)0x81100004);
[SysAbiExport(
Nid = "lbQKqsERhtE",
ExportName = "sceDiscMapIsRequestOnHDD",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceDiscMap")]
public static int DiscMapIsRequestOnHDD(CpuContext ctx)
{
var pathAddress = ctx[CpuRegister.Rdi];
var offset = ctx[CpuRegister.Rsi];
var size = ctx[CpuRegister.Rdx];
var resultAddress = ctx[CpuRegister.Rcx];
if (pathAddress == 0 || resultAddress == 0)
{
return DiscMapErrorInvalidArgument;
}
if (!ctx.TryWriteInt32(resultAddress, 1))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceDiscMap(ctx, "sceDiscMapIsRequestOnHDD", pathAddress, offset, size);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "fJgP+wqifno",
ExportName = "sceDiscMapUnknownFJgP",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceDiscMap")]
public static int DiscMapUnknownFJgP(CpuContext ctx) => WriteMappingTriple(ctx, "sceDiscMapUnknownFJgP");
[SysAbiExport(
Nid = "ioKMruft1ek",
ExportName = "sceDiscMapUnknownIoKM",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceDiscMap")]
public static int DiscMapUnknownIoKM(CpuContext ctx) => WriteMappingTriple(ctx, "sceDiscMapUnknownIoKM");
private static int WriteMappingTriple(CpuContext ctx, string exportName)
{
var pathAddress = ctx[CpuRegister.Rdi];
var offset = ctx[CpuRegister.Rsi];
var size = ctx[CpuRegister.Rdx];
var flagsAddress = ctx[CpuRegister.Rcx];
var outAddress1 = ctx[CpuRegister.R8];
var outAddress2 = ctx[CpuRegister.R9];
if (pathAddress == 0 || flagsAddress == 0 || outAddress1 == 0 || outAddress2 == 0)
{
return DiscMapErrorInvalidArgument;
}
if (!ctx.TryWriteUInt64(flagsAddress, 0) ||
!ctx.TryWriteUInt64(outAddress1, 0) ||
!ctx.TryWriteUInt64(outAddress2, 0))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceDiscMap(ctx, exportName, pathAddress, offset, size);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static void TraceDiscMap(CpuContext ctx, string exportName, ulong pathAddress, ulong offset, ulong size)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_DISCMAP"), "1", StringComparison.Ordinal))
{
return;
}
var path = ctx.TryReadNullTerminatedUtf8(pathAddress, 1024, out var value)
? value
: $"<unreadable 0x{pathAddress:X16}>";
Console.Error.WriteLine($"[HLE][DISCMAP] {exportName} path={path} offset=0x{offset:X} size=0x{size:X}");
}
}
+46
View File
@@ -0,0 +1,46 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Ime;
public static class ImeExports
{
// Quake (KEX) calls this from its main loop and from the audio bring-up path with
// an event-handler pointer. No IME session ever exists here, so report success
// without invoking the handler ("no pending IME events"). This NID was previously
// misbound as an sceNgs2VoiceControl alias, which fed the game NGS2 errors.
[SysAbiExport(
Nid = "-4GCfYdNF1s",
ExportName = "sceImeUpdate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceIme")]
public static int ImeUpdate(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "eaFXjfJv3xs",
ExportName = "sceImeKeyboardOpen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceIme")]
public static int ImeKeyboardOpen(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "dKadqZFgKKQ",
ExportName = "sceImeKeyboardGetResourceId",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceIme")]
public static int ImeKeyboardGetResourceId(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}
@@ -149,6 +149,19 @@ public static class KernelAprCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// Success stub: the argument layout is unknown and callers tolerate the
// empty answer (Quake streams fine), so no output payload is written until
// the real signature is reversed.
[SysAbiExport(
Nid = "WvEu7yl3Ivg",
ExportName = "sceKernelAprGetFileSize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelAprGetFileSize(CpuContext ctx)
{
return ctx.SetReturn(0);
}
private static bool TryWriteAprResult(CpuContext ctx, ulong resultAddress)
{
Span<byte> result = stackalloc byte[sizeof(ulong)];
@@ -1689,15 +1689,15 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var entryCount = (int)count;
Span<uint> localIds = count <= 256 ? stackalloc uint[entryCount] : new uint[entryCount];
Span<ulong> localSizes = count <= 128 ? stackalloc ulong[entryCount] : new ulong[entryCount];
var resolvedGuestPaths = new string[entryCount];
var resolvedHostPaths = new string[entryCount];
for (ulong i = 0; i < count; i++)
{
if (idsAddress != 0 &&
!TryWriteUInt32Compat(ctx, idsAddress + (i * sizeof(uint)), uint.MaxValue))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var index = (int)i;
if (!TryResolveAprFilepath(ctx, pathListAddress, i, out var guestPath))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
@@ -1712,23 +1712,47 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
var fileId = AmprFileRegistry.Register(guestPath, hostPath);
var fileId = AmprFileRegistry.ComputeFileId(guestPath);
LogIoTrace("apr_resolve", guestPath, $"host='{hostPath}' index={i} count={count} id=0x{fileId:X8} size={fileSize}");
if (idsAddress != 0 &&
!TryWriteUInt32Compat(ctx, idsAddress + (i * sizeof(uint)), fileId))
localIds[index] = fileId;
localSizes[index] = fileSize;
resolvedGuestPaths[index] = guestPath;
resolvedHostPaths[index] = hostPath;
}
Span<byte> sizePayload = count <= 64 ? stackalloc byte[entryCount * sizeof(ulong)] : new byte[entryCount * sizeof(ulong)];
for (ulong i = 0; i < count; i++)
{
BinaryPrimitives.WriteUInt64LittleEndian(sizePayload[(int)(i * sizeof(ulong))..], localSizes[(int)i]);
}
if (!TryWriteCompat(ctx, sizesAddress, sizePayload))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (idsAddress != 0)
{
Span<byte> idPayload = count <= 128 ? stackalloc byte[entryCount * sizeof(uint)] : new byte[entryCount * sizeof(uint)];
for (ulong i = 0; i < count; i++)
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
BinaryPrimitives.WriteUInt32LittleEndian(idPayload[(int)(i * sizeof(uint))..], localIds[(int)i]);
}
if (!TryWriteUInt64Compat(ctx, sizesAddress + (i * sizeof(ulong)), fileSize))
if (!TryWriteCompat(ctx, idsAddress, idPayload))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
}
for (var i = 0; i < entryCount; i++)
{
AmprFileRegistry.Register(resolvedGuestPaths[i], resolvedHostPaths[i]);
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -6350,19 +6374,19 @@ public static class KernelMemoryCompatExports
}
var currentIndex = directory.NextIndex;
if (basePointerAddress != 0 && !TryWriteUInt64Compat(ctx, basePointerAddress, (ulong)currentIndex))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (currentIndex >= directory.Entries.Length)
{
if (basePointerAddress != 0 &&
!TryWriteUInt64Compat(ctx, basePointerAddress, (ulong)currentIndex))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
var entryName = directory.Entries[currentIndex];
directory.NextIndex = currentIndex + 1;
var entryBytes = Encoding.UTF8.GetBytes(entryName);
var nameLength = Math.Min(entryBytes.Length, 255);
@@ -6381,6 +6405,13 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (basePointerAddress != 0 &&
!TryWriteUInt64Compat(ctx, basePointerAddress, (ulong)currentIndex))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
directory.NextIndex = currentIndex + 1;
ctx[CpuRegister.Rax] = 512;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -19,10 +19,15 @@ public static class KernelRuntimeCompatExports
internal const int ClockProf = 2;
internal const int ClockMonotonic = 4;
internal const int ClockUptime = 5;
internal const int ClockUptimePrecise = 7;
internal const int ClockUptimeFast = 8;
internal const int ClockRealtimePrecise = 9;
internal const int ClockMonotonicPrecise = 11;
internal const int ClockRealtimeFast = 10;
internal const int ClockMonotonicPrecise = 11;
internal const int ClockMonotonicFast = 12;
internal const int ClockSecond = 13;
internal const int ClockThreadCputimeId = 14;
internal const int ClockProcTime = 15;
private const int Efault = 14;
private const int Einval = 22;
private const ulong TlsErrnoOffset = 0x40;
@@ -723,10 +728,24 @@ public static class KernelRuntimeCompatExports
return true;
}
// CLOCK_SECOND is FreeBSD's cached whole-second realtime clock (Quake's
// audio_output_thread polls it and treated the previous EINVAL as a fatal
// init failure, exiting and getting respawned in a loop).
case ClockSecond:
seconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
nanoseconds = 0;
return true;
case ClockMonotonic:
case ClockMonotonicPrecise:
case ClockMonotonicFast:
case ClockUptime:
case ClockUptimePrecise:
case ClockUptimeFast:
// Per-thread/process CPU time approximated with the monotonic clock; games
// use these for profiling deltas where monotonicity matters, not absolutes.
case ClockThreadCputimeId:
case ClockProcTime:
GetProcessMonotonicTime(out seconds, out nanoseconds);
return true;
+33
View File
@@ -0,0 +1,33 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Mouse;
public static class MouseExports
{
// Returns 0 read entries: no mouse is connected. This NID was previously misbound
// as an sceNgs2VoiceGetState alias.
[SysAbiExport(
Nid = "x8qnXqh-tiM",
ExportName = "sceMouseRead",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceMouse")]
public static int MouseRead(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "RaqxZIf6DvE",
ExportName = "sceMouseOpen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceMouse")]
public static int MouseOpen(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}
+14 -15
View File
@@ -203,13 +203,12 @@ public static class Ngs2Exports
}
}
[SysAbiExport(
Nid = "-4GCfYdNF1s",
ExportName = "sceNgs2VoiceControl",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNgs2")]
public static int Ngs2VoiceControlAlt(CpuContext ctx) => Ngs2VoiceControl(ctx);
// The earlier "alt NID" guesses here were wrong: an NID is the aerolib hash of one
// symbol name, and hashing scripts/ps5_names.txt shows the previous alt bindings
// actually belonged to sceImeUpdate (-4GCfYdNF1s), sceMouseRead (x8qnXqh-tiM) and
// sceSystemGestureUpdateAllTouchRecognizer (wPJGwI2RM2I) — Quake's audio thread was
// receiving an NGS2 error from what it thought was sceImeUpdate. Those now live in
// their real libraries; the NIDs below are verified against the hash.
[SysAbiExport(
Nid = "AbYvTOZ8Pts",
ExportName = "sceNgs2VoiceRunCommands",
@@ -219,25 +218,25 @@ public static class Ngs2Exports
[SysAbiExport(
Nid = "-TOuuAQ-buE",
ExportName = "sceNgs2VoiceRunCommands",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNgs2")]
public static int Ngs2VoiceRunCommandsAlt(CpuContext ctx) => Ngs2VoiceControl(ctx);
[SysAbiExport(
Nid = "x8qnXqh-tiM",
ExportName = "sceNgs2VoiceGetState",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNgs2")]
public static int Ngs2VoiceGetState(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport(
Nid = "wPJGwI2RM2I",
Nid = "rEh728kXk3w",
ExportName = "sceNgs2VoiceGetStateFlags",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNgs2")]
public static int Ngs2VoiceGetStateFlags(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport(
Nid = "xa8oL9dmXkM",
ExportName = "sceNgs2PanInit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNgs2")]
public static int Ngs2PanInit(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport(
Nid = "i0VnXM-C9fc",
ExportName = "sceNgs2SystemRender",
+24
View File
@@ -33,6 +33,30 @@ public static class NpManagerExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// Offline profile: the online id payload is left untouched and the call
// reports success, matching the other offline NpManager stubs here.
[SysAbiExport(
Nid = "XDncXQIJUSk",
ExportName = "sceNpGetOnlineId",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpManager")]
public static int NpGetOnlineId(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "e-ZuhGEoeC4",
ExportName = "sceNpGetNpReachabilityState",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpManager")]
public static int NpGetNpReachabilityState(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "VfRSmPmj8Q8",
ExportName = "sceNpRegisterStateCallback",
@@ -76,4 +76,14 @@ public static class NpUniversalDataSystemExports
{
return ctx.SetReturn(0, typeof(long));
}
[SysAbiExport(
Nid = "AUIHb7jUX3I",
ExportName = "sceNpUniversalDataSystemDestroyHandle",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpUniversalDataSystem")]
public static int NpUniversalDataSystemDestroyHandle(CpuContext ctx)
{
return ctx.SetReturn(0, typeof(long));
}
}
@@ -83,4 +83,15 @@ public static class SystemGestureExports
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "wPJGwI2RM2I",
ExportName = "sceSystemGestureUpdateAllTouchRecognizer",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSystemGesture")]
public static int SystemGestureUpdateAllTouchRecognizer(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}
+23
View File
@@ -0,0 +1,23 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Ult;
public static class UltExports
{
// Games initialize the ULT (user-level thread) runtime before spinning up
// their job systems; an unresolved import here (0x80020002) makes engines
// treat the whole task system as unavailable. The emulator schedules guest
// pthreads natively, so accepting the initialization is sufficient.
[SysAbiExport(
Nid = "hZIg1EWGsHM",
ExportName = "sceUltInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceUlt")]
public static int UltInitialize(CpuContext ctx)
{
return ctx.SetReturn(0, typeof(long));
}
}
@@ -64,7 +64,7 @@ public static class VideoOutExports
private static long _vblankSignalCount;
private static long _flipSubmitCount;
public static void ConfigureApplicationInfo(string? title, string? titleId, string? version)
public static void ConfigureApplicationInfo(string? title, string? titleId, string? version, string? emulatorCommitSha)
{
var parts = new List<string>();
if (!string.IsNullOrWhiteSpace(title))
@@ -79,9 +79,10 @@ public static class VideoOutExports
var application = parts.Count == 0 ? "VideoOut" : string.Join(' ', parts);
var versionSuffix = string.IsNullOrWhiteSpace(version) ? string.Empty : $" v{version.Trim()}";
var commitSuffix = string.IsNullOrWhiteSpace(emulatorCommitSha) ? string.Empty : $" \u00b7 {emulatorCommitSha.Trim()}";
lock (_stateGate)
{
_windowTitle = $"SharpEmu - {application}{versionSuffix}";
_windowTitle = $"SharpEmu{commitSuffix} - {application}{versionSuffix}";
}
}
+6
View File
@@ -98,6 +98,12 @@
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
},
"sharpemu.hle": {
"type": "Project",
"dependencies": {
"SharpEmu.Logging": "[1.0.0, )"
}
},
"sharpemu.logging": {
"type": "Project"
}
}