mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c8c98ed7d | |||
| aaebfe017b | |||
| 9cdc8550ec | |||
| d6fccedab8 | |||
| fed7a6d062 | |||
| 50f78f3713 | |||
| 63b440efcd | |||
| 0565d01744 | |||
| 56bf00e9a5 | |||
| 46b729c5b4 | |||
| 4b7df8623a | |||
| 3c2134474d | |||
| 298ef01809 | |||
| 2060dacaf1 | |||
| f73c9e8c3f | |||
| 3d2c30b151 | |||
| 61d28e9e08 | |||
| 4bd42795c7 | |||
| c03ca32a02 | |||
| 6e2878f2ff | |||
| 2e09b015bc | |||
| 7b9efd1539 | |||
| fbf2c2d00a | |||
| 8c1507777c | |||
| 8ebc43e758 | |||
| 5aadb7495a | |||
| cdee77521e | |||
| d2fca37716 | |||
| 5bee81df70 | |||
| d4bb282c49 |
@@ -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 |
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -8,6 +8,7 @@ path = [
|
||||
"**/packages.lock.json",
|
||||
"scripts/ps5_names.txt",
|
||||
"src/SharpEmu.HLE/Aerolib/aerolib.bin",
|
||||
"src/SharpEmu.GUI/Languages/**",
|
||||
"_logs/**",
|
||||
".github/images/**",
|
||||
"assets/images/**"
|
||||
|
||||
+372
-13
@@ -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,
|
||||
|
||||
@@ -48,6 +48,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<TargetPath>LICENSE.txt</TargetPath>
|
||||
<Visible>False</Visible>
|
||||
</Content>
|
||||
<Content Include="..\SharpEmu.GUI\Languages\*.json" Condition="'$(Configuration)' != 'Release'">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<TargetPath>Languages\%(Filename)%(Extension)</TargetPath>
|
||||
<Visible>False</Visible>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Keep glfw as a loose file next to the executable; every other native
|
||||
|
||||
@@ -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
|
||||
@@ -768,11 +810,14 @@ public sealed partial class DirectExecutionBackend
|
||||
return !_logUsleep;
|
||||
}
|
||||
|
||||
// Only mutex/rwlock *lock* is excluded: it may block a contended acquire, which the
|
||||
// leaf path can't. unlock never blocks and stays here — routing it off the fast path
|
||||
// slows guest spinlocks enough to livelock (Demon's Souls).
|
||||
return nid is
|
||||
"9UK1vLZQft4" or // scePthreadMutexLock
|
||||
"tn3VlD0hG60" or // scePthreadMutexUnlock
|
||||
"7H0iTOciTLo" or // pthread_mutex_lock
|
||||
"2Z+PpY6CaJg" or // pthread_mutex_unlock
|
||||
"EgmLo6EWgso" or // pthread_rwlock_unlock
|
||||
"+L98PIbGttk" or // scePthreadRwlockUnlock
|
||||
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
|
||||
"zgXifHT9ErY" or // sceVideoOutIsFlipPending
|
||||
"V++UgBtQhn0" or // sceAgcGetDataPacketPayloadAddress
|
||||
@@ -835,6 +880,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
|
||||
@@ -845,12 +891,7 @@ public sealed partial class DirectExecutionBackend
|
||||
"6ULAa0fq4jA" or // scePthreadRwlockInit
|
||||
"1471ajPzxh0" or // pthread_rwlock_destroy
|
||||
"BB+kb08Tl9A" or // scePthreadRwlockDestroy
|
||||
"iGjsr1WAtI0" or // pthread_rwlock_rdlock
|
||||
"Ox9i0c7L5w0" or // scePthreadRwlockRdlock
|
||||
"sIlRvQqsN2Y" or // pthread_rwlock_wrlock
|
||||
"mqdNorrB+gI" or // scePthreadRwlockWrlock
|
||||
"EgmLo6EWgso" or // pthread_rwlock_unlock
|
||||
"+L98PIbGttk" or // scePthreadRwlockUnlock
|
||||
// rwlock rd/wr lock removed (can block); init/destroy/unlock stay.
|
||||
"aI+OeCz8xrQ" or // scePthreadSelf
|
||||
"EotR8a3ASf4" or // pthread_self
|
||||
"eoht7mQOCmo" or // scePthreadGetspecific
|
||||
@@ -1275,30 +1316,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 +1859,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 +1913,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") + ") ===");
|
||||
}
|
||||
@@ -1886,9 +1992,16 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
byte* code = (byte*)ptr;
|
||||
int offset = 0;
|
||||
EmitByte(code, ref offset, 0x48); // sub rsp, 0x20
|
||||
// TlsGetValue returns its TLS pointer in RAX. Preserve the guest return value
|
||||
// above the 32-byte Windows shadow space while keeping the call site aligned.
|
||||
EmitByte(code, ref offset, 0x48); // sub rsp, 0x30
|
||||
EmitByte(code, ref offset, 0x83);
|
||||
EmitByte(code, ref offset, 0xEC);
|
||||
EmitByte(code, ref offset, 0x30);
|
||||
EmitByte(code, ref offset, 0x48); // mov [rsp+0x20], rax
|
||||
EmitByte(code, ref offset, 0x89);
|
||||
EmitByte(code, ref offset, 0x44);
|
||||
EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x20);
|
||||
EmitByte(code, ref offset, 0xB9); // mov ecx, tlsIndex
|
||||
EmitUInt32(code, ref offset, _hostRspSlotTlsIndex);
|
||||
@@ -1898,13 +2011,21 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
offset += sizeof(ulong);
|
||||
EmitByte(code, ref offset, 0xFF); // call rax
|
||||
EmitByte(code, ref offset, 0xD0);
|
||||
EmitByte(code, ref offset, 0x48); // add rsp, 0x20
|
||||
EmitByte(code, ref offset, 0x49); // mov r11, rax
|
||||
EmitByte(code, ref offset, 0x89);
|
||||
EmitByte(code, ref offset, 0xC3);
|
||||
EmitByte(code, ref offset, 0x48); // mov rax, [rsp+0x20]
|
||||
EmitByte(code, ref offset, 0x8B);
|
||||
EmitByte(code, ref offset, 0x44);
|
||||
EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x20);
|
||||
EmitByte(code, ref offset, 0x48); // add rsp, 0x30
|
||||
EmitByte(code, ref offset, 0x83);
|
||||
EmitByte(code, ref offset, 0xC4);
|
||||
EmitByte(code, ref offset, 0x20);
|
||||
EmitByte(code, ref offset, 0x48); // mov rsp, [rax]
|
||||
EmitByte(code, ref offset, 0x30);
|
||||
EmitByte(code, ref offset, 0x49); // mov rsp, [r11]
|
||||
EmitByte(code, ref offset, 0x8B);
|
||||
EmitByte(code, ref offset, 0x20);
|
||||
EmitByte(code, ref offset, 0x23);
|
||||
EmitHostNonvolatileXmmRestore(code, ref offset);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5F);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5E);
|
||||
@@ -1937,6 +2058,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,17 +2772,20 @@ 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)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (_guestThreadPumpDepth != 0)
|
||||
if (Interlocked.CompareExchange(ref _guestThreadPumpDepth, 1, 0) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_guestThreadPumpDepth++;
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < 8; i++)
|
||||
@@ -2677,7 +2831,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
}
|
||||
finally
|
||||
{
|
||||
_guestThreadPumpDepth--;
|
||||
Volatile.Write(ref _guestThreadPumpDepth, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2720,9 +2874,18 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
}
|
||||
}
|
||||
|
||||
if (wakeCount != 0 && _logGuestThreads)
|
||||
if (wakeCount != 0)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][INFO] guest_threads.wake key={wakeKey} count={wakeCount}");
|
||||
if (_logGuestThreads)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][INFO] guest_threads.wake key={wakeKey} count={wakeCount}");
|
||||
}
|
||||
|
||||
// Pump or the readied thread waits for an import dispatch that never comes.
|
||||
if (_cpuContext is { } wakeContext)
|
||||
{
|
||||
Pump(wakeContext, "wake");
|
||||
}
|
||||
}
|
||||
|
||||
return wakeCount;
|
||||
@@ -3145,6 +3308,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 +3626,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 +3707,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 +3788,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 +3986,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 +4060,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 +4341,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 +4368,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 +4383,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
|
||||
@@ -4204,6 +4430,26 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
return;
|
||||
}
|
||||
_stallWatchdogStop = false;
|
||||
|
||||
// Drives woken threads when every guest thread is parked (nothing dispatches then).
|
||||
var dispatcherThread = new Thread(new ThreadStart(delegate
|
||||
{
|
||||
while (!_stallWatchdogStop)
|
||||
{
|
||||
Thread.Sleep(1);
|
||||
WakeExpiredBlockedGuestThreads();
|
||||
if (Volatile.Read(ref _readyGuestThreadCount) > 0 && _cpuContext is { } dispatchContext)
|
||||
{
|
||||
Pump(dispatchContext, "dispatcher");
|
||||
}
|
||||
}
|
||||
}))
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "SharpEmu-GuestThreadDispatcher"
|
||||
};
|
||||
dispatcherThread.Start();
|
||||
|
||||
long num = (long)((double)stallWatchdogSeconds * Stopwatch.Frequency);
|
||||
_stallWatchdogThread = new Thread(new ThreadStart(delegate
|
||||
{
|
||||
@@ -4232,6 +4478,15 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
MarkExecutionProgress();
|
||||
continue;
|
||||
}
|
||||
if (IsExpectedBlockingImportStall(out var blockingNid, out var blockingName))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] No import progress for {stallWatchdogSeconds}s while waiting in {blockingName} ({blockingNid}); continuing.");
|
||||
LogStallWatchdogSnapshot();
|
||||
Console.Error.Flush();
|
||||
MarkExecutionProgress();
|
||||
continue;
|
||||
}
|
||||
if (Interlocked.Exchange(ref _stallWatchdogTriggered, 1) != 0)
|
||||
{
|
||||
continue;
|
||||
@@ -4267,6 +4522,38 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
return false;
|
||||
}
|
||||
|
||||
// A thread parked in a blocking wait is idle by design, not stalled.
|
||||
private bool IsExpectedBlockingImportStall(out string nid, out string name)
|
||||
{
|
||||
nid = string.Empty;
|
||||
name = string.Empty;
|
||||
var cpuContext = _cpuContext;
|
||||
if (cpuContext is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var importAddress = cpuContext.Rip & 0xFFFFFFFFFFFFFFF0uL;
|
||||
foreach (var entry in _importEntries)
|
||||
{
|
||||
if (entry.Address != importAddress)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
nid = entry.Nid;
|
||||
name = _moduleManager.TryGetExport(nid, out var export)
|
||||
? $"{export.LibraryName}:{export.Name}"
|
||||
: nid;
|
||||
return nid is
|
||||
"Op8TBGY5KHg" or // pthread_cond_wait
|
||||
"27bAgiJmOh0" or // pthread_cond_timedwait
|
||||
"fzyMKs9kim0"; // sceKernelWaitEqueue
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void StopStallWatchdog()
|
||||
{
|
||||
_stallWatchdogStop = true;
|
||||
@@ -4300,13 +4587,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 +4770,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)
|
||||
|
||||
@@ -83,7 +83,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
ImportTraceLimit = Math.Max(0, options.ImportTraceLimit),
|
||||
};
|
||||
var moduleManager = new ModuleManager();
|
||||
moduleManager.RegisterFromAssembly(typeof(VideoOutExports).Assembly, Generation.Gen4 | Generation.Gen5, Aerolib.Instance);
|
||||
moduleManager.RegisterFromAssembly(typeof(KernelExports).Assembly, Generation.Gen4 | Generation.Gen5, Aerolib.Instance);
|
||||
moduleManager.Freeze();
|
||||
|
||||
@@ -140,7 +139,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);
|
||||
|
||||
@@ -65,7 +65,10 @@
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
},
|
||||
"sharpemu.hle": {
|
||||
"type": "Project"
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"SharpEmu.Logging": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.libs": {
|
||||
"type": "Project",
|
||||
|
||||
@@ -127,6 +127,34 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
</Style>
|
||||
|
||||
<!-- Top-level page switcher (Library / Options): plain transparent
|
||||
buttons, not TabItem, so there is no Fluent selected-tab underline.
|
||||
The active page is conveyed by brightness alone; LB/RB gamepad
|
||||
hints flank the pair. -->
|
||||
<Style Selector="Button.segment">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Foreground" Value="{StaticResource MutedBrush}" />
|
||||
<Setter Property="FontSize" Value="22" />
|
||||
<Setter Property="FontWeight" Value="Bold" />
|
||||
<Setter Property="Padding" Value="6,4" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
</Style>
|
||||
<Style Selector="Button.segment:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
</Style>
|
||||
<Style Selector="Button.segment.active">
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
</Style>
|
||||
|
||||
<!-- Gamepad shoulder-button hint chip (LB/RB, L1/R1). -->
|
||||
<Style Selector="Border.padHint">
|
||||
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
|
||||
<Setter Property="CornerRadius" Value="6" />
|
||||
<Setter Property="Padding" Value="8,3" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ContextMenu">
|
||||
<Setter Property="Background" Value="{StaticResource CardBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
// 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)
|
||||
{
|
||||
var loc = Localization.Instance;
|
||||
|
||||
_sourceLines = lines;
|
||||
Title = loc.Get("Console.WindowTitle");
|
||||
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 = loc.Get("Console.SearchWatermark"),
|
||||
Width = 320,
|
||||
Margin = new Thickness(0, 0, 12, 0),
|
||||
};
|
||||
_autoScrollCheck = new CheckBox
|
||||
{
|
||||
Content = loc.Get("Console.AutoScroll"),
|
||||
IsChecked = autoScroll,
|
||||
FontSize = 12,
|
||||
Margin = new Thickness(0, 0, 12, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
var copyButton = new Button
|
||||
{
|
||||
Classes = { "ghost" },
|
||||
Content = loc.Get("Console.Copy"),
|
||||
Padding = new Thickness(10, 4),
|
||||
Margin = new Thickness(0, 0, 8, 0),
|
||||
};
|
||||
var clearButton = new Button
|
||||
{
|
||||
Classes = { "ghost" },
|
||||
Content = loc.Get("Console.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 = loc.Get("Console.Title"),
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.IO.Pipes;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal Discord Rich Presence client. Speaks Discord's local IPC
|
||||
/// protocol (framed JSON over the discord-ipc-N named pipe) directly, so no
|
||||
/// external dependency is needed. All failures are silent: no Discord, no
|
||||
/// presence, no impact on the launcher.
|
||||
/// </summary>
|
||||
public sealed class DiscordRichPresence : IDisposable
|
||||
{
|
||||
private const int OpHandshake = 0;
|
||||
private const int OpFrame = 1;
|
||||
private const int MaxFrameBytes = 64 * 1024;
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
|
||||
};
|
||||
|
||||
private readonly object _gate = new();
|
||||
private readonly string _clientId;
|
||||
private readonly AutoResetEvent _signal = new(false);
|
||||
private readonly Thread _worker;
|
||||
|
||||
private NamedPipeClientStream? _pipe;
|
||||
private PresenceState? _desired;
|
||||
private PresenceState? _sent;
|
||||
private volatile bool _disposed;
|
||||
|
||||
private sealed record PresenceState(string Details, string? State, long? StartUnixSeconds);
|
||||
|
||||
public DiscordRichPresence(string clientId)
|
||||
{
|
||||
_clientId = clientId;
|
||||
_worker = new Thread(WorkerLoop)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "DiscordRichPresence",
|
||||
};
|
||||
_worker.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests the given presence; the worker applies it asynchronously and
|
||||
/// retries (with reconnection) until it is delivered or replaced.
|
||||
/// </summary>
|
||||
public void SetPresence(string details, string? state = null, long? startUnixSeconds = null)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_desired = new PresenceState(details, state, startUnixSeconds);
|
||||
}
|
||||
|
||||
Trace($"queued details=\"{details}\" state=\"{state}\"");
|
||||
_signal.Set();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_disposed = true;
|
||||
_signal.Set();
|
||||
ClosePipe();
|
||||
}
|
||||
|
||||
private void WorkerLoop()
|
||||
{
|
||||
while (!_disposed)
|
||||
{
|
||||
// Woken immediately on presence changes; the periodic tick
|
||||
// retries after connection failures (e.g. Discord starts later).
|
||||
_signal.WaitOne(TimeSpan.FromSeconds(15));
|
||||
if (_disposed || _clientId.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
PresenceState? desired;
|
||||
lock (_gate)
|
||||
{
|
||||
desired = _desired;
|
||||
}
|
||||
|
||||
if (desired is null || desired == _sent)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
EnsureConnected();
|
||||
SendActivity(desired);
|
||||
_sent = desired;
|
||||
Trace($"sent details=\"{desired.Details}\" state=\"{desired.State}\"");
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// Connection died (or never existed); reconnect on a later tick.
|
||||
Trace($"send failed: {exception.Message}");
|
||||
ClosePipe();
|
||||
_sent = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureConnected()
|
||||
{
|
||||
if (_pipe is { IsConnected: true })
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ClosePipe();
|
||||
Exception? lastError = null;
|
||||
for (var index = 0; index < 10; index++)
|
||||
{
|
||||
var pipe = new NamedPipeClientStream(
|
||||
".",
|
||||
$"discord-ipc-{index}",
|
||||
PipeDirection.InOut,
|
||||
PipeOptions.Asynchronous);
|
||||
try
|
||||
{
|
||||
pipe.Connect(500);
|
||||
_pipe = pipe;
|
||||
WriteFrame(OpHandshake, JsonSerializer.Serialize(new
|
||||
{
|
||||
v = 1,
|
||||
client_id = _clientId,
|
||||
}));
|
||||
|
||||
// Discord replies with a READY (or error) frame; consume it
|
||||
// so the pipe buffer stays drained.
|
||||
_ = ReadFrame();
|
||||
return;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
lastError = exception;
|
||||
pipe.Dispose();
|
||||
_pipe = null;
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError ?? new IOException("Discord IPC pipe not found.");
|
||||
}
|
||||
|
||||
private void SendActivity(PresenceState presence)
|
||||
{
|
||||
var payload = new Dictionary<string, object?>
|
||||
{
|
||||
["cmd"] = "SET_ACTIVITY",
|
||||
["nonce"] = Guid.NewGuid().ToString(),
|
||||
["args"] = new Dictionary<string, object?>
|
||||
{
|
||||
["pid"] = Environment.ProcessId,
|
||||
["activity"] = new Dictionary<string, object?>
|
||||
{
|
||||
["details"] = presence.Details,
|
||||
["state"] = presence.State,
|
||||
["timestamps"] = presence.StartUnixSeconds is { } start
|
||||
? new Dictionary<string, object?> { ["start"] = start }
|
||||
: null,
|
||||
["assets"] = new Dictionary<string, object?>
|
||||
{
|
||||
["large_image"] = "logo",
|
||||
["large_text"] = "SharpEmu",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
WriteFrame(OpFrame, JsonSerializer.Serialize(payload, JsonOptions));
|
||||
|
||||
// Consume Discord's acknowledgement to keep the pipe drained.
|
||||
_ = ReadFrame();
|
||||
}
|
||||
|
||||
private void WriteFrame(int opcode, string json)
|
||||
{
|
||||
var pipe = _pipe ?? throw new IOException("Discord IPC pipe is not connected.");
|
||||
var payload = Encoding.UTF8.GetBytes(json);
|
||||
var frame = new byte[8 + payload.Length];
|
||||
BitConverter.TryWriteBytes(frame.AsSpan(0, 4), opcode);
|
||||
BitConverter.TryWriteBytes(frame.AsSpan(4, 4), payload.Length);
|
||||
payload.CopyTo(frame, 8);
|
||||
pipe.Write(frame, 0, frame.Length);
|
||||
pipe.Flush();
|
||||
}
|
||||
|
||||
private string ReadFrame()
|
||||
{
|
||||
var pipe = _pipe ?? throw new IOException("Discord IPC pipe is not connected.");
|
||||
var header = ReadExactly(pipe, 8);
|
||||
var length = BitConverter.ToInt32(header, 4);
|
||||
if (length is < 0 or > MaxFrameBytes)
|
||||
{
|
||||
throw new IOException($"Discord IPC frame length {length} is out of range.");
|
||||
}
|
||||
|
||||
return Encoding.UTF8.GetString(ReadExactly(pipe, length));
|
||||
}
|
||||
|
||||
private static byte[] ReadExactly(NamedPipeClientStream pipe, int count)
|
||||
{
|
||||
var buffer = new byte[count];
|
||||
var read = 0;
|
||||
while (read < count)
|
||||
{
|
||||
var chunk = pipe.Read(buffer, read, count - read);
|
||||
if (chunk <= 0)
|
||||
{
|
||||
throw new IOException("Discord IPC pipe closed.");
|
||||
}
|
||||
|
||||
read += chunk;
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private static void Trace(string message)
|
||||
{
|
||||
if (string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_DISCORD"),
|
||||
"1",
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine($"[DISCORD] {message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void ClosePipe()
|
||||
{
|
||||
var pipe = _pipe;
|
||||
_pipe = null;
|
||||
try
|
||||
{
|
||||
pipe?.Dispose();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Best effort; the pipe may already be broken.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,14 @@ internal sealed class EmulatorProcess : IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefer terminating the job: it kills the whole tree, including
|
||||
// any children the emulator spawned, even when the main process
|
||||
// is wedged in a GPU driver call.
|
||||
if (_jobHandle != 0)
|
||||
{
|
||||
_ = TerminateJobObject(_jobHandle, 1);
|
||||
}
|
||||
|
||||
if (_processHandle != 0)
|
||||
{
|
||||
_ = TerminateProcess(_processHandle, 1);
|
||||
@@ -160,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));
|
||||
@@ -192,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);
|
||||
@@ -639,6 +627,10 @@ internal sealed class EmulatorProcess : IDisposable
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool TerminateProcess(nint process, uint exitCode);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool TerminateJobObject(nint job, uint exitCode);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool CloseHandle(nint handle);
|
||||
|
||||
@@ -29,10 +29,12 @@ public sealed class GameEntry : INotifyPropertyChanged
|
||||
private long _sizeBytes;
|
||||
|
||||
public GameEntry(
|
||||
string name, string? titleId, string path, long sizeBytes, string? coverPath, string? backgroundPath)
|
||||
string name, string? titleId, string? version, string path, long sizeBytes,
|
||||
string? coverPath, string? backgroundPath)
|
||||
{
|
||||
Name = name;
|
||||
TitleId = titleId;
|
||||
Version = version;
|
||||
Path = path;
|
||||
_sizeBytes = sizeBytes;
|
||||
CoverPath = coverPath;
|
||||
@@ -46,6 +48,9 @@ public sealed class GameEntry : INotifyPropertyChanged
|
||||
|
||||
public string? TitleId { get; }
|
||||
|
||||
/// <summary>Content version from sce_sys/param.json, e.g. "01.000.000".</summary>
|
||||
public string? Version { get; }
|
||||
|
||||
public string Path { get; }
|
||||
|
||||
/// <summary>
|
||||
@@ -65,7 +70,7 @@ public sealed class GameEntry : INotifyPropertyChanged
|
||||
|
||||
_sizeBytes = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SizeBytes)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Detail)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SizeText)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,9 +111,15 @@ public sealed class GameEntry : INotifyPropertyChanged
|
||||
|
||||
public bool HasCover => _cover is not null;
|
||||
|
||||
public string Detail => TitleId is not null
|
||||
? $"{TitleId} • {FormatSize(SizeBytes)}"
|
||||
: FormatSize(SizeBytes);
|
||||
public bool HasTitleId => TitleId is not null;
|
||||
|
||||
/// <summary>Badge text shown in the launch bar, e.g. "v01.000.000".</summary>
|
||||
public string? VersionText => Version is null ? null : $"v{Version}";
|
||||
|
||||
public bool HasVersion => Version is not null;
|
||||
|
||||
/// <summary>Formatted install size badge shown in the launch bar.</summary>
|
||||
public string SizeText => FormatSize(SizeBytes);
|
||||
|
||||
private static string ComputeInitials(string name)
|
||||
{
|
||||
|
||||
@@ -23,14 +23,38 @@ public sealed class GuiSettings
|
||||
|
||||
public bool StrictDynlibResolution { get; set; }
|
||||
|
||||
/// <summary>Mirror emulator output to user/logs/<titleId>-<timestamp>.log.</summary>
|
||||
/// <summary>
|
||||
/// Mirror emulator output to user/logs/<titleId>-<timestamp>.log, if <see cref="LogFilePath"/> is null.
|
||||
/// </summary>
|
||||
public bool LogToFile { get; set; }
|
||||
|
||||
/// <summary>If <see cref="LogToFile"/> is true it logs to this file path.</summary>
|
||||
public string? LogFilePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If <see cref="OverrideLogFile"/> is false it appends <titleId>-<timestamp> to the filename specified by
|
||||
/// <see cref="LogFilePath"/>. Otherwise it uses the exact filename from <see cref="LogFilePath"/>
|
||||
/// </summary>
|
||||
public bool OverrideLogFile { get; set; }
|
||||
|
||||
/// <summary>Loop the selected game's sce_sys/snd0.at9 preview music.</summary>
|
||||
public bool PlayTitleMusic { get; set; } = true;
|
||||
|
||||
public string? EmulatorPath { get; set; }
|
||||
|
||||
/// <summary>UI language, matching a file code under Languages/ (e.g. "en", "tr").</summary>
|
||||
public string Language { get; set; } = "en";
|
||||
|
||||
/// <summary>Publish launcher/game status to Discord Rich Presence.</summary>
|
||||
public bool DiscordRichPresence { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Discord application ID used for Rich Presence; the default is the
|
||||
/// SharpEmu application. Override to rebrand what Discord shows as
|
||||
/// "Playing …" (register at discord.com/developers/applications).
|
||||
/// </summary>
|
||||
public string DiscordClientId { get; set; } = "1525606762248540221";
|
||||
|
||||
// The emulator is portable and keeps its data next to the executable;
|
||||
// the GUI follows the same convention.
|
||||
public static string SettingsPath => Path.Combine(AppContext.BaseDirectory, "gui-settings.json");
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"_languageName": "English",
|
||||
|
||||
"Page.Library": "Library",
|
||||
"Page.Options": "Options",
|
||||
"Page.GameCount.One": "1 game",
|
||||
"Page.GameCount.Other": "{0} games",
|
||||
|
||||
"Library.SearchWatermark": "Search library…",
|
||||
"Library.AddFolder": "+ Add folder",
|
||||
"Library.Rescan": "⟳ Rescan",
|
||||
"Library.OpenFile": "Open file…",
|
||||
|
||||
"Library.Context.Launch": "Launch",
|
||||
"Library.Context.OpenFolder": "Open game folder",
|
||||
"Library.Context.CopyPath": "Copy path",
|
||||
"Library.Context.CopyTitleId": "Copy title ID",
|
||||
"Library.Context.Remove": "Remove from library",
|
||||
|
||||
"Library.Empty.Title": "Your library is empty",
|
||||
"Library.Empty.Hint": "Add a folder containing your games to get started.",
|
||||
"Library.Empty.SearchTitle": "No games match your search",
|
||||
"Library.Empty.SearchHint": "Nothing in the library matches “{0}”.",
|
||||
"Library.Empty.AddFolder": "+ Add game folder",
|
||||
|
||||
"Library.Loading": "Loading library…",
|
||||
|
||||
"Options.General": "General",
|
||||
"Options.Section.Emulation": "EMULATION",
|
||||
"Options.Section.Logging": "LOGGING",
|
||||
"Options.Section.Launcher": "LAUNCHER",
|
||||
|
||||
"Options.CpuEngine.Label": "CPU engine",
|
||||
"Options.CpuEngine.Desc": "Execution engine used to run game code.",
|
||||
"Options.CpuEngine.Native": "Native",
|
||||
|
||||
"Options.Strict.Label": "Strict dynlib resolution",
|
||||
"Options.Strict.Desc": "Fail the launch when an imported symbol cannot be resolved.",
|
||||
|
||||
"Options.LogLevel.Label": "Log level",
|
||||
"Options.LogLevel.Desc": "Verbosity of the emulator console output.",
|
||||
"Options.LogLevel.Trace": "Trace",
|
||||
"Options.LogLevel.Debug": "Debug",
|
||||
"Options.LogLevel.Info": "Info",
|
||||
"Options.LogLevel.Warning": "Warning",
|
||||
"Options.LogLevel.Error": "Error",
|
||||
"Options.LogLevel.Critical": "Critical",
|
||||
|
||||
"Options.TraceImports.Label": "Import trace limit",
|
||||
"Options.TraceImports.Desc": "Trace the first N imports per module (0 = off).",
|
||||
|
||||
"Options.LogToFile.Label": "Log to file",
|
||||
"Options.LogToFile.Desc": "Mirror emulator output to a log file.",
|
||||
|
||||
"Options.LogFilePath.Label": "Log file path",
|
||||
"Options.LogFilePath.Default": "No custom path — logs go to user/logs next to the emulator.",
|
||||
"Options.LogFilePath.Select": "Select…",
|
||||
|
||||
"Options.OverrideLogFile.Label": "Override log file",
|
||||
"Options.OverrideLogFile.Desc": "Use the exact file path instead of appending title ID and timestamp.",
|
||||
|
||||
"Options.TitleMusic.Label": "Title music",
|
||||
"Options.TitleMusic.Desc": "Loop the selected game's preview music in the library.",
|
||||
|
||||
"Options.Discord.Label": "Discord presence",
|
||||
"Options.Discord.Desc": "Show the running game on your Discord profile.",
|
||||
|
||||
"Options.Language.Label": "Emulator language",
|
||||
"Options.Language.Desc": "Language used throughout the launcher. Applies immediately.",
|
||||
|
||||
"Common.On": "On",
|
||||
"Common.Off": "Off",
|
||||
|
||||
"Console.Title": "CONSOLE",
|
||||
"Console.SearchWatermark": "Search...",
|
||||
"Console.AutoScroll": "Auto-scroll",
|
||||
"Console.Split": "Split",
|
||||
"Console.Copy": "Copy",
|
||||
"Console.Clear": "Clear",
|
||||
"Console.WindowTitle": "SharpEmu Console",
|
||||
|
||||
"Launch.NoGameSelected": "No game selected",
|
||||
"Launch.NoGameHint": "Pick a game from the library, or open an eboot.bin directly.",
|
||||
"Launch.Idle": "Idle",
|
||||
"Launch.Console": "≡ Console",
|
||||
"Launch.Launch": "▶ Launch",
|
||||
"Launch.Stop": "■ Stop",
|
||||
"Launch.Running": "Running — {0}",
|
||||
"Launch.Stopping": "Stopping…",
|
||||
"Launch.Exited": "Exited with code {0} ({1})",
|
||||
"Launch.ExeNotFound": "SharpEmu executable not found. Build the SharpEmu.CLI project first (dotnet build).",
|
||||
"Launch.LogFile": "Log file: {0}",
|
||||
"Launch.Command": "$ SharpEmu {0}",
|
||||
"Launch.StartFailed": "Failed to start the emulator: {0}",
|
||||
"Launch.ProcessExited": "Process exited with code {0} ({1}).",
|
||||
|
||||
"Exit.Ok": "OK",
|
||||
"Exit.InvalidArguments": "invalid arguments",
|
||||
"Exit.EbootNotFound": "eboot not found",
|
||||
"Exit.RuntimeException": "runtime exception",
|
||||
"Exit.EmulationError": "emulation error",
|
||||
"Exit.Unknown": "unknown",
|
||||
|
||||
"Status.EmulatorLocating": "Emulator: locating…",
|
||||
"Status.EmulatorPath": "Emulator: {0}",
|
||||
"Status.EmulatorNotFound": "Emulator: SharpEmu executable not found — build SharpEmu.CLI first.",
|
||||
"Status.ScanningLibrary": "Scanning library…",
|
||||
"Status.AddFolderPrompt": "Add a game folder to populate the library.",
|
||||
"Status.LibraryScanned": "Library scanned: {0} game(s) in {1} folder(s).",
|
||||
"Status.CouldNotOpenFolder": "Could not open folder: {0}",
|
||||
"Status.CopiedToClipboard": "{0} copied to clipboard.",
|
||||
"Status.RemovedFromLibrary": "Removed “{0}” from the library. Re-add its folder to restore it.",
|
||||
"Status.Running": "Running {0}",
|
||||
"Status.Stopping": "Stopping…",
|
||||
"Status.Idle": "Idle",
|
||||
|
||||
"Clipboard.Path": "Path",
|
||||
"Clipboard.TitleId": "Title ID",
|
||||
|
||||
"Discord.Playing": "Playing {0}",
|
||||
"Discord.Browsing": "Browsing the library",
|
||||
|
||||
"Dialog.ChooseGameFolder": "Choose a folder containing games",
|
||||
"Dialog.OpenExecutable": "Open an executable to launch",
|
||||
"Dialog.PsExecutables": "PS executables",
|
||||
"Dialog.SaveLogFile": "Select where to save the Log file",
|
||||
"Dialog.PlainTextFiles": "Plain Text Files",
|
||||
"Dialog.LogFiles": "Log Files"
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"_languageName": "Türkçe",
|
||||
|
||||
"Page.Library": "Kütüphane",
|
||||
"Page.Options": "Ayarlar",
|
||||
"Page.GameCount.One": "1 oyun",
|
||||
"Page.GameCount.Other": "{0} oyun",
|
||||
|
||||
"Library.SearchWatermark": "Kütüphanede ara…",
|
||||
"Library.AddFolder": "+ Klasör ekle",
|
||||
"Library.Rescan": "⟳ Yeniden tara",
|
||||
"Library.OpenFile": "Dosya aç…",
|
||||
|
||||
"Library.Context.Launch": "Başlat",
|
||||
"Library.Context.OpenFolder": "Oyun klasörünü aç",
|
||||
"Library.Context.CopyPath": "Yolu kopyala",
|
||||
"Library.Context.CopyTitleId": "Title ID'yi kopyala",
|
||||
"Library.Context.Remove": "Kütüphaneden kaldır",
|
||||
|
||||
"Library.Empty.Title": "Kütüphaneniz boş",
|
||||
"Library.Empty.Hint": "Başlamak için oyunlarınızı içeren bir klasör ekleyin.",
|
||||
"Library.Empty.SearchTitle": "Aramanızla eşleşen oyun yok",
|
||||
"Library.Empty.SearchHint": "Kütüphanede “{0}” ile eşleşen bir şey yok.",
|
||||
"Library.Empty.AddFolder": "+ Oyun klasörü ekle",
|
||||
|
||||
"Library.Loading": "Kütüphane yükleniyor…",
|
||||
|
||||
"Options.General": "Genel",
|
||||
"Options.Section.Emulation": "EMÜLASYON",
|
||||
"Options.Section.Logging": "GÜNLÜKLEME",
|
||||
"Options.Section.Launcher": "BAŞLATICI",
|
||||
|
||||
"Options.CpuEngine.Label": "CPU motoru",
|
||||
"Options.CpuEngine.Desc": "Oyun kodunu çalıştırmak için kullanılan yürütme motoru.",
|
||||
"Options.CpuEngine.Native": "Native",
|
||||
|
||||
"Options.Strict.Label": "Katı dynlib çözümü",
|
||||
"Options.Strict.Desc": "İçe aktarılan bir sembol çözümlenemediğinde başlatmayı başarısız kılar.",
|
||||
|
||||
"Options.LogLevel.Label": "Günlük seviyesi",
|
||||
"Options.LogLevel.Desc": "Emülatör konsol çıktısının ayrıntı düzeyi.",
|
||||
"Options.LogLevel.Trace": "Trace",
|
||||
"Options.LogLevel.Debug": "Debug",
|
||||
"Options.LogLevel.Info": "Info",
|
||||
"Options.LogLevel.Warning": "Warning",
|
||||
"Options.LogLevel.Error": "Error",
|
||||
"Options.LogLevel.Critical": "Critical",
|
||||
|
||||
"Options.TraceImports.Label": "Import izleme sınırı",
|
||||
"Options.TraceImports.Desc": "Modül başına ilk N import'u izle (0 = kapalı).",
|
||||
|
||||
"Options.LogToFile.Label": "Dosyaya günlükle",
|
||||
"Options.LogToFile.Desc": "Emülatör çıktısını bir günlük dosyasına yansıt.",
|
||||
|
||||
"Options.LogFilePath.Label": "Günlük dosyası yolu",
|
||||
"Options.LogFilePath.Default": "Özel yol yok — günlükler emülatörün yanındaki user/logs klasörüne yazılır.",
|
||||
"Options.LogFilePath.Select": "Seç…",
|
||||
|
||||
"Options.OverrideLogFile.Label": "Günlük dosyasının üzerine yaz",
|
||||
"Options.OverrideLogFile.Desc": "Title ID ve zaman damgası eklemek yerine tam dosya yolunu kullan.",
|
||||
|
||||
"Options.TitleMusic.Label": "Oyun müziği",
|
||||
"Options.TitleMusic.Desc": "Kütüphanede seçili oyunun önizleme müziğini döngüye al.",
|
||||
|
||||
"Options.Discord.Label": "Discord durumu",
|
||||
"Options.Discord.Desc": "Discord profilinde çalışan oyunu göster.",
|
||||
|
||||
"Options.Language.Label": "Emülatör dili",
|
||||
"Options.Language.Desc": "Başlatıcı genelinde kullanılan dil. Hemen uygulanır.",
|
||||
|
||||
"Common.On": "Açık",
|
||||
"Common.Off": "Kapalı",
|
||||
|
||||
"Console.Title": "KONSOL",
|
||||
"Console.SearchWatermark": "Ara...",
|
||||
"Console.AutoScroll": "Otomatik kaydır",
|
||||
"Console.Split": "Ayır",
|
||||
"Console.Copy": "Kopyala",
|
||||
"Console.Clear": "Temizle",
|
||||
"Console.WindowTitle": "SharpEmu Konsol",
|
||||
|
||||
"Launch.NoGameSelected": "Oyun seçilmedi",
|
||||
"Launch.NoGameHint": "Kütüphaneden bir oyun seçin veya doğrudan bir eboot.bin açın.",
|
||||
"Launch.Idle": "Boşta",
|
||||
"Launch.Console": "≡ Konsol",
|
||||
"Launch.Launch": "▶ Başlat",
|
||||
"Launch.Stop": "■ Durdur",
|
||||
"Launch.Running": "Çalışıyor — {0}",
|
||||
"Launch.Stopping": "Durduruluyor…",
|
||||
"Launch.Exited": "Çıkış kodu {0} ({1})",
|
||||
"Launch.ExeNotFound": "SharpEmu çalıştırılabilir dosyası bulunamadı. Önce SharpEmu.CLI projesini derleyin (dotnet build).",
|
||||
"Launch.LogFile": "Günlük dosyası: {0}",
|
||||
"Launch.Command": "$ SharpEmu {0}",
|
||||
"Launch.StartFailed": "Emülatör başlatılamadı: {0}",
|
||||
"Launch.ProcessExited": "İşlem {0} kodla sonlandı ({1}).",
|
||||
|
||||
"Exit.Ok": "Tamam",
|
||||
"Exit.InvalidArguments": "geçersiz argümanlar",
|
||||
"Exit.EbootNotFound": "eboot bulunamadı",
|
||||
"Exit.RuntimeException": "çalışma zamanı hatası",
|
||||
"Exit.EmulationError": "emülasyon hatası",
|
||||
"Exit.Unknown": "bilinmiyor",
|
||||
|
||||
"Status.EmulatorLocating": "Emülatör: bulunuyor…",
|
||||
"Status.EmulatorPath": "Emülatör: {0}",
|
||||
"Status.EmulatorNotFound": "Emülatör: SharpEmu çalıştırılabilir dosyası bulunamadı — önce SharpEmu.CLI'yi derleyin.",
|
||||
"Status.ScanningLibrary": "Kütüphane taranıyor…",
|
||||
"Status.AddFolderPrompt": "Kütüphaneyi doldurmak için bir oyun klasörü ekleyin.",
|
||||
"Status.LibraryScanned": "Kütüphane tarandı: {1} klasörde {0} oyun.",
|
||||
"Status.CouldNotOpenFolder": "Klasör açılamadı: {0}",
|
||||
"Status.CopiedToClipboard": "{0} panoya kopyalandı.",
|
||||
"Status.RemovedFromLibrary": "“{0}” kütüphaneden kaldırıldı. Geri getirmek için klasörünü yeniden ekleyin.",
|
||||
"Status.Running": "{0} çalışıyor",
|
||||
"Status.Stopping": "Durduruluyor…",
|
||||
"Status.Idle": "Boşta",
|
||||
|
||||
"Clipboard.Path": "Yol",
|
||||
"Clipboard.TitleId": "Title ID",
|
||||
|
||||
"Discord.Playing": "{0} oynuyor",
|
||||
"Discord.Browsing": "Kütüphaneye göz atıyor",
|
||||
|
||||
"Dialog.ChooseGameFolder": "Oyunlarınızı içeren bir klasör seçin",
|
||||
"Dialog.OpenExecutable": "Başlatılacak bir çalıştırılabilir dosya aç",
|
||||
"Dialog.PsExecutables": "PS çalıştırılabilirleri",
|
||||
"Dialog.SaveLogFile": "Günlük dosyasının kaydedileceği yeri seçin",
|
||||
"Dialog.PlainTextFiles": "Düz Metin Dosyaları",
|
||||
"Dialog.LogFiles": "Günlük Dosyaları"
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
/// <summary>
|
||||
/// Loads UI strings for the launcher. Every language ships embedded in the
|
||||
/// assembly (see SharpEmu.GUI.csproj) so a release build is fully
|
||||
/// self-contained; an optional Languages/<code>.json file next to the
|
||||
/// executable overrides the embedded copy for that code, so a translation
|
||||
/// fix or a brand-new language never needs a rebuild.
|
||||
/// </summary>
|
||||
public sealed class Localization
|
||||
{
|
||||
public static Localization Instance { get; } = new();
|
||||
|
||||
public sealed record LanguageInfo(string Code, string NativeName);
|
||||
|
||||
private const string EmbeddedResourcePrefix = "Languages.";
|
||||
private const string EmbeddedResourceSuffix = ".json";
|
||||
|
||||
private Dictionary<string, string> _strings = new();
|
||||
|
||||
private Localization()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Directory holding optional *.json language overrides, next to the executable.</summary>
|
||||
public static string LanguagesDirectory => Path.Combine(AppContext.BaseDirectory, "Languages");
|
||||
|
||||
public string CurrentCode { get; private set; } = "en";
|
||||
|
||||
public string Get(string key) => _strings.TryGetValue(key, out var value) ? value : key;
|
||||
|
||||
public string Format(string key, params object?[] args) => string.Format(Get(key), args);
|
||||
|
||||
/// <summary>
|
||||
/// Languages available either embedded in the binary or as a loose
|
||||
/// override file, sorted by code. A loose file's declared name wins when
|
||||
/// the same code exists in both places.
|
||||
/// </summary>
|
||||
public List<LanguageInfo> DiscoverLanguages()
|
||||
{
|
||||
var languages = new Dictionary<string, LanguageInfo>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var code in EmbeddedLanguageCodes())
|
||||
{
|
||||
using var stream = OpenEmbeddedLanguageStream(code);
|
||||
if (stream is not null)
|
||||
{
|
||||
languages[code] = new LanguageInfo(code, ReadLanguageName(stream) ?? code);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(LanguagesDirectory, "*.json"))
|
||||
{
|
||||
var code = Path.GetFileNameWithoutExtension(file);
|
||||
using var stream = File.OpenRead(file);
|
||||
languages[code] = new LanguageInfo(code, ReadLanguageName(stream) ?? code);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// No loose Languages directory: the embedded languages still stand.
|
||||
}
|
||||
|
||||
var result = languages.Values.ToList();
|
||||
result.Sort((a, b) => string.CompareOrdinal(a.Code, b.Code));
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Loads a language by code (e.g. "en"): a loose override file first, then the embedded copy.</summary>
|
||||
public void Load(string code)
|
||||
{
|
||||
if (!TryLoadLooseFile(code) && !TryLoadEmbedded(code) &&
|
||||
!string.Equals(code, "en", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_ = TryLoadLooseFile("en") || TryLoadEmbedded("en");
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EmbeddedLanguageCodes()
|
||||
{
|
||||
foreach (var name in typeof(Localization).Assembly.GetManifestResourceNames())
|
||||
{
|
||||
if (name.StartsWith(EmbeddedResourcePrefix, StringComparison.Ordinal) &&
|
||||
name.EndsWith(EmbeddedResourceSuffix, StringComparison.Ordinal))
|
||||
{
|
||||
yield return name[EmbeddedResourcePrefix.Length..^EmbeddedResourceSuffix.Length];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Stream? OpenEmbeddedLanguageStream(string code) =>
|
||||
typeof(Localization).Assembly.GetManifestResourceStream($"{EmbeddedResourcePrefix}{code}{EmbeddedResourceSuffix}");
|
||||
|
||||
private static string? ReadLanguageName(Stream stream)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(stream);
|
||||
if (document.RootElement.TryGetProperty("_languageName", out var name) &&
|
||||
name.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return name.GetString();
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Malformed file: fall back to the code as its own display name.
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool TryLoadLooseFile(string code)
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = Path.Combine(LanguagesDirectory, $"{code}.json");
|
||||
return File.Exists(path) && TryLoad(code, File.ReadAllText(path));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryLoadEmbedded(string code)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = OpenEmbeddedLanguageStream(code);
|
||||
if (stream is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
using var reader = new StreamReader(stream);
|
||||
return TryLoad(code, reader.ReadToEnd());
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryLoad(string code, string json)
|
||||
{
|
||||
var loaded = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
|
||||
if (loaded is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_strings = loaded;
|
||||
CurrentCode = code;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+309
-150
@@ -9,14 +9,16 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
Title="SharpEmu"
|
||||
Width="1280" Height="820"
|
||||
MinWidth="980" MinHeight="640"
|
||||
WindowState="Maximized"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
Background="{StaticResource BgBrush}"
|
||||
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">
|
||||
<Grid RowDefinitions="Auto,*,Auto">
|
||||
|
||||
<!-- Selected-game backdrop: key art behind the main content, dimmed by
|
||||
a scrim so tiles and text stay readable. Fades on selection. -->
|
||||
@@ -40,8 +42,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
</Border>
|
||||
</Panel>
|
||||
|
||||
<!-- Title bar -->
|
||||
<Grid x:Name="TitleBar" Grid.Row="0" Background="{StaticResource ChromeBrush}">
|
||||
<!-- Title bar; hidden in fullscreen (F11) along with the status bar, so
|
||||
the game gets the whole screen. -->
|
||||
<Grid x:Name="TitleBar" Grid.Row="0" Height="44" Background="{StaticResource ChromeBrush}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10" Margin="16,0" VerticalAlignment="Center">
|
||||
<Image Source="avares://SharpEmu.GUI/Assets/SharpEmu.ico" Width="20" Height="20"
|
||||
RenderOptions.BitmapInterpolationMode="HighQuality" />
|
||||
@@ -53,119 +56,290 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
</Grid>
|
||||
|
||||
<!-- Main content -->
|
||||
<Grid Grid.Row="1" Margin="18,14,18,14" RowDefinitions="Auto,*,Auto,Auto">
|
||||
<Grid Grid.Row="1" Margin="32,24,32,20" RowDefinitions="Auto,*,Auto,Auto">
|
||||
|
||||
<!-- Library toolbar -->
|
||||
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,*,Auto,Auto,Auto,Auto" Margin="6,0,6,12">
|
||||
<TextBlock Grid.Column="0" Text="Library" FontSize="22" FontWeight="Bold" VerticalAlignment="Center" />
|
||||
<Border Grid.Column="1" Classes="pill" Margin="12,2,0,0" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="GameCountText" Text="0 games" FontSize="11" Foreground="{StaticResource MutedBrush}" />
|
||||
</Border>
|
||||
<TextBox Grid.Column="3" x:Name="SearchBox" Watermark="Search library…" Width="280"
|
||||
VerticalAlignment="Center" Margin="0,0,12,0" />
|
||||
<Button Grid.Column="4" x:Name="AddFolderButton" Classes="ghost" Content="+ Add folder"
|
||||
VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<Button Grid.Column="5" x:Name="RescanButton" Classes="ghost" Content="⟳ Rescan"
|
||||
VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<Button Grid.Column="6" x:Name="OpenFileButton" Classes="ghost" Content="Open file…"
|
||||
VerticalAlignment="Center" />
|
||||
<!-- Library / Options page switcher, with the library toolbar sharing
|
||||
the same row on the right. Plain buttons (not TabItem) so there is
|
||||
no underline; LB/RB hint chips flank the pair and the gamepad's
|
||||
shoulder buttons actually switch pages from anywhere. -->
|
||||
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto" Margin="0,0,0,20">
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="14" VerticalAlignment="Center">
|
||||
<Border Classes="padHint" VerticalAlignment="Center">
|
||||
<TextBlock Text="LB" FontSize="11" FontWeight="Bold" Foreground="{StaticResource MutedBrush}" />
|
||||
</Border>
|
||||
<Button x:Name="LibraryTabButton" Classes="segment active" Content="Library" />
|
||||
<Button x:Name="OptionsTabButton" Classes="segment" Content="Options" />
|
||||
<Border Classes="padHint" VerticalAlignment="Center">
|
||||
<TextBlock Text="RB" FontSize="11" FontWeight="Bold" Foreground="{StaticResource MutedBrush}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="2" x:Name="LibraryToolbar" Orientation="Horizontal" Spacing="8"
|
||||
VerticalAlignment="Center">
|
||||
<TextBox x:Name="SearchBox" Watermark="Search library…" Width="240" VerticalAlignment="Center" />
|
||||
<Button x:Name="AddFolderButton" Classes="ghost" Content="+ Add folder" VerticalAlignment="Center" />
|
||||
<Button x:Name="RescanButton" Classes="ghost" Content="⟳ Rescan" VerticalAlignment="Center" />
|
||||
<Button x:Name="OpenFileButton" Classes="ghost" Content="Open file…" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Cover-art grid -->
|
||||
<Panel Grid.Row="1">
|
||||
<ListBox x:Name="GameList" Classes="tileGrid" Background="Transparent"
|
||||
SelectionMode="Single" Padding="0">
|
||||
<ListBox.ContextMenu>
|
||||
<ContextMenu x:Name="GameContextMenu" Placement="Pointer">
|
||||
<MenuItem x:Name="CtxLaunch" Header="Launch" FontWeight="SemiBold">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="▶" FontSize="11" Foreground="{StaticResource AccentHoverBrush}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem x:Name="CtxOpenFolder" Header="Open game folder">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="📂" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem x:Name="CtxCopyPath" Header="Copy path">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem x:Name="CtxCopyTitleId" Header="Copy title ID">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem x:Name="CtxRemove" Header="Remove from library"
|
||||
Foreground="{StaticResource DangerHoverBrush}">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="✕" FontSize="12" Foreground="{StaticResource DangerHoverBrush}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
</ContextMenu>
|
||||
</ListBox.ContextMenu>
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel />
|
||||
</ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Width="128" Height="186" Spacing="7">
|
||||
<Border Classes="coverShadow" Width="128" Height="128">
|
||||
<Border Classes="coverClip">
|
||||
<Panel>
|
||||
<Border Background="{Binding PlaceholderBrush}" IsVisible="{Binding !HasCover}">
|
||||
<TextBlock Text="{Binding Initials}" FontSize="36" FontWeight="Bold"
|
||||
Foreground="#E8ECF4" Opacity="0.85"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<Image Source="{Binding Cover}" Stretch="UniformToFill" IsVisible="{Binding HasCover}" />
|
||||
</Panel>
|
||||
</Border>
|
||||
</Border>
|
||||
<StackPanel Spacing="2">
|
||||
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="SemiBold"
|
||||
TextWrapping="Wrap" MaxLines="2" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Text="{Binding Detail}" FontSize="11" Foreground="{StaticResource MutedBrush}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<!-- Empty state -->
|
||||
<StackPanel x:Name="EmptyState" Spacing="10" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
IsVisible="False">
|
||||
<TextBlock Text="🎮" FontSize="44" HorizontalAlignment="Center" Opacity="0.7" />
|
||||
<TextBlock x:Name="EmptyStateTitle" Text="Your library is empty" FontSize="18" FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center" />
|
||||
<TextBlock x:Name="EmptyStateHint" Text="Add a folder containing your games to get started."
|
||||
FontSize="13" Foreground="{StaticResource MutedBrush}" HorizontalAlignment="Center" />
|
||||
<Button x:Name="EmptyAddFolderButton" Classes="accent" Content="+ Add game folder"
|
||||
HorizontalAlignment="Center" Margin="0,8,0,0" />
|
||||
</StackPanel>
|
||||
<!-- Library page. The tile row gets extra top margin so it sits
|
||||
closer to eye level (PS5 home-screen style) instead of hugging
|
||||
the Library/Options switcher above it. -->
|
||||
<Panel x:Name="LibraryPage" Margin="0,64,0,0">
|
||||
<ListBox x:Name="GameList" Classes="tileGrid" Background="Transparent"
|
||||
SelectionMode="Single" Padding="0">
|
||||
<ListBox.ContextMenu>
|
||||
<ContextMenu x:Name="GameContextMenu" Placement="Pointer">
|
||||
<MenuItem x:Name="CtxLaunch" Header="Launch" FontWeight="SemiBold">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="▶" FontSize="11" Foreground="{StaticResource AccentHoverBrush}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem x:Name="CtxOpenFolder" Header="Open game folder">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="📂" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem x:Name="CtxCopyPath" Header="Copy path">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem x:Name="CtxCopyTitleId" Header="Copy title ID">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem x:Name="CtxRemove" Header="Remove from library"
|
||||
Foreground="{StaticResource DangerHoverBrush}">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="✕" FontSize="12" Foreground="{StaticResource DangerHoverBrush}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
</ContextMenu>
|
||||
</ListBox.ContextMenu>
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel />
|
||||
</ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Width="128" Height="172" Spacing="7">
|
||||
<Border Classes="coverShadow" Width="128" Height="128">
|
||||
<Border Classes="coverClip">
|
||||
<Panel>
|
||||
<Border Background="{Binding PlaceholderBrush}" IsVisible="{Binding !HasCover}">
|
||||
<TextBlock Text="{Binding Initials}" FontSize="36" FontWeight="Bold"
|
||||
Foreground="#E8ECF4" Opacity="0.85"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<Image Source="{Binding Cover}" Stretch="UniformToFill" IsVisible="{Binding HasCover}" />
|
||||
</Panel>
|
||||
</Border>
|
||||
</Border>
|
||||
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="SemiBold"
|
||||
TextWrapping="Wrap" MaxLines="2" TextTrimming="CharacterEllipsis"
|
||||
TextAlignment="Center" HorizontalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<!-- Empty state -->
|
||||
<StackPanel x:Name="EmptyState" Spacing="10" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
IsVisible="False">
|
||||
<TextBlock Text="🎮" FontSize="44" HorizontalAlignment="Center" Opacity="0.7" />
|
||||
<TextBlock x:Name="EmptyStateTitle" Text="Your library is empty" FontSize="18" FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center" />
|
||||
<TextBlock x:Name="EmptyStateHint" Text="Add a folder containing your games to get started."
|
||||
FontSize="13" Foreground="{StaticResource MutedBrush}" HorizontalAlignment="Center" />
|
||||
<Button x:Name="EmptyAddFolderButton" Classes="accent" Content="+ Add game folder"
|
||||
HorizontalAlignment="Center" Margin="0,8,0,0" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Loading state: covers the grid while a scan (initial load,
|
||||
rescan, or a just-added folder) is in flight, so the screen
|
||||
never looks blank mid-scan. -->
|
||||
<StackPanel x:Name="LoadingState" Spacing="14" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
IsVisible="False">
|
||||
<ProgressBar IsIndeterminate="True" Width="180" Height="3" />
|
||||
<TextBlock x:Name="LoadingStateText" Text="Loading library…" FontSize="13"
|
||||
Foreground="{StaticResource MutedBrush}" HorizontalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
|
||||
<!-- Options page: sub-tabs so future categories (Graphics, …) slot
|
||||
in next to General. Card-grouped sections match the visual
|
||||
language used by the console panel and launch bar below. -->
|
||||
<Grid x:Name="OptionsPage" IsVisible="False">
|
||||
<TabControl>
|
||||
<TabItem x:Name="GeneralTabItem" Header="General" FontSize="15">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
|
||||
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="14">
|
||||
<TextBlock x:Name="EmulationSectionTitle" Classes="sectionTitle" Text="EMULATION" />
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="CpuEngineLabel" Text="CPU engine" FontSize="13" />
|
||||
<TextBlock x:Name="CpuEngineDesc" Text="Execution engine used to run game code."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ComboBox Grid.Column="1" x:Name="CpuEngineBox" Width="160" SelectedIndex="0"
|
||||
VerticalAlignment="Center" CornerRadius="8">
|
||||
<ComboBoxItem x:Name="CpuEngineNativeItem" Content="Native" />
|
||||
</ComboBox>
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="StrictLabel" Text="Strict dynlib resolution" FontSize="13" />
|
||||
<TextBlock x:Name="StrictDesc" Text="Fail the launch when an imported symbol cannot be resolved."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="StrictToggle" OnContent="On" OffContent="Off"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="14">
|
||||
<TextBlock x:Name="LoggingSectionTitle" Classes="sectionTitle" Text="LOGGING" />
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="LogLevelLabel" Text="Log level" FontSize="13" />
|
||||
<TextBlock x:Name="LogLevelDesc" Text="Verbosity of the emulator console output."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ComboBox Grid.Column="1" x:Name="LogLevelBox" Width="160" SelectedIndex="2"
|
||||
VerticalAlignment="Center" CornerRadius="8">
|
||||
<ComboBoxItem x:Name="LogLevelTraceItem" Content="Trace" />
|
||||
<ComboBoxItem x:Name="LogLevelDebugItem" Content="Debug" />
|
||||
<ComboBoxItem x:Name="LogLevelInfoItem" Content="Info" />
|
||||
<ComboBoxItem x:Name="LogLevelWarningItem" Content="Warning" />
|
||||
<ComboBoxItem x:Name="LogLevelErrorItem" Content="Error" />
|
||||
<ComboBoxItem x:Name="LogLevelCriticalItem" Content="Critical" />
|
||||
</ComboBox>
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="TraceImportsLabel" Text="Import trace limit" FontSize="13" />
|
||||
<TextBlock x:Name="TraceImportsDesc" Text="Trace the first N imports per module (0 = off)."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<NumericUpDown Grid.Column="1" x:Name="TraceImportsBox" Width="160" Minimum="0"
|
||||
Maximum="4096" Increment="16" Value="0" FormatString="0"
|
||||
VerticalAlignment="Center" CornerRadius="8" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="LogToFileLabel" Text="Log to file" FontSize="13" />
|
||||
<TextBlock x:Name="LogToFileDesc" Text="Mirror emulator output to a log file."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="LogToFileToggle" OnContent="On" OffContent="Off"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="LogFilePathLabel" Text="Log file path" FontSize="13" />
|
||||
<TextBlock x:Name="LogFilePathText" Text="No custom path"
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" x:Name="SelectLogFilePathButton" Classes="ghost" Content="Select…"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="OverrideLogFileLabel" Text="Override log file" FontSize="13" />
|
||||
<TextBlock x:Name="OverrideLogFileDesc"
|
||||
Text="Use the exact file path instead of appending title ID and timestamp."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="OverrideLogFileToggle" OnContent="On" OffContent="Off"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="14">
|
||||
<TextBlock x:Name="LauncherSectionTitle" Classes="sectionTitle" Text="LAUNCHER" />
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="LanguageLabel" Text="Emulator language" FontSize="13" />
|
||||
<TextBlock x:Name="LanguageDesc"
|
||||
Text="Language used throughout the launcher. Applies immediately."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ComboBox Grid.Column="1" x:Name="LanguageBox" Width="160"
|
||||
VerticalAlignment="Center" CornerRadius="8"
|
||||
DisplayMemberBinding="{Binding NativeName}" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="TitleMusicLabel" Text="Title music" FontSize="13" />
|
||||
<TextBlock x:Name="TitleMusicDesc" Text="Loop the selected game's preview music in the library."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="TitleMusicToggle" OnContent="On" OffContent="Off"
|
||||
IsChecked="True" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="DiscordLabel" Text="Discord presence" FontSize="13" />
|
||||
<TextBlock x:Name="DiscordDesc" Text="Show the running game on your Discord profile."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="DiscordToggle" OnContent="On" OffContent="Off"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</Grid>
|
||||
</Panel>
|
||||
|
||||
<!-- Console (collapsible) -->
|
||||
<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" Margin="16,12,16,8">
|
||||
<TextBlock Classes="sectionTitle" Text="CONSOLE" VerticalAlignment="Center" />
|
||||
<CheckBox Grid.Column="1" x:Name="AutoScrollCheck" Content="Auto-scroll" IsChecked="True"
|
||||
FontSize="12" Margin="0,0,12,0" />
|
||||
<Button Grid.Column="2" x:Name="CopyLogButton" Classes="ghost" Content="Copy" FontSize="12"
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto,Auto,Auto" Margin="16,12,16,8">
|
||||
<TextBlock x:Name="ConsoleSectionTitle" Classes="sectionTitle" Text="CONSOLE" VerticalAlignment="Center" />
|
||||
<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="DetachConsoleButton" Classes="ghost" Content="Split" FontSize="12"
|
||||
Padding="10,4" Margin="0,0,8,0" />
|
||||
<Button Grid.Column="3" 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"
|
||||
@@ -179,8 +353,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Launch bar -->
|
||||
<Border Grid.Row="3" Classes="card" Margin="0,12,0,0" Padding="14">
|
||||
<!-- Launch bar; capped so it does not sprawl on a maximized window. -->
|
||||
<Border Grid.Row="3" Classes="card" Margin="0,12,0,0" Padding="14" MaxWidth="1280">
|
||||
<StackPanel Spacing="14">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
|
||||
@@ -199,9 +373,32 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
</Panel>
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Column="1" Spacing="3" VerticalAlignment="Center" Margin="14,0,14,0">
|
||||
<TextBlock x:Name="SelectedGameTitle" Text="No game selected" FontSize="16" FontWeight="Bold"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<StackPanel Grid.Column="1" Spacing="4" VerticalAlignment="Center" Margin="14,0,14,0">
|
||||
<Grid ColumnDefinitions="Auto,Auto,*">
|
||||
<TextBlock Grid.Column="0" x:Name="SelectedGameTitle" Text="No game selected" FontSize="16"
|
||||
FontWeight="Bold" TextTrimming="CharacterEllipsis" VerticalAlignment="Center"
|
||||
MaxWidth="340" Margin="0,0,10,0" />
|
||||
|
||||
<!-- Title id / version / size badges, right next to the
|
||||
title. The title's own MaxWidth (not a "*" column) is
|
||||
what keeps them from drifting to the far right. -->
|
||||
<StackPanel Grid.Column="1" x:Name="SelectedBadgesRow" Orientation="Horizontal" Spacing="6"
|
||||
IsVisible="False" VerticalAlignment="Center">
|
||||
<Border Classes="pill" IsVisible="{Binding HasTitleId, FallbackValue=False}">
|
||||
<TextBlock Text="{Binding TitleId}" FontSize="10" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource MutedBrush}" />
|
||||
</Border>
|
||||
<Border Classes="pill" IsVisible="{Binding HasVersion, FallbackValue=False}">
|
||||
<TextBlock Text="{Binding VersionText}" FontSize="10" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource MutedBrush}" />
|
||||
</Border>
|
||||
<Border Classes="pill">
|
||||
<TextBlock Text="{Binding SizeText, FallbackValue=''}" FontSize="10" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource MutedBrush}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<TextBlock x:Name="SelectedGamePath" Text="Pick a game from the library, or open an eboot.bin directly."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextTrimming="CharacterEllipsis" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="7">
|
||||
@@ -213,56 +410,18 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||
<ToggleButton x:Name="OptionsToggle" Classes="ghost" Content="⚙ Options" />
|
||||
<ToggleButton x:Name="ConsoleToggle" Classes="ghost" Content="≡ Console" />
|
||||
<Button x:Name="LaunchButton" Classes="accent" Content="▶ Launch" IsEnabled="False" />
|
||||
<Button x:Name="StopButton" Classes="danger" Content="■ Stop" IsEnabled="False" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Launch options (collapsible) -->
|
||||
<WrapPanel x:Name="OptionsPanel" IsVisible="False">
|
||||
<StackPanel Margin="0,0,24,0">
|
||||
<TextBlock Classes="fieldLabel" Text="CPU engine" />
|
||||
<ComboBox x:Name="CpuEngineBox" Width="150" SelectedIndex="0" CornerRadius="8">
|
||||
<ComboBoxItem Content="Native" />
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,24,0">
|
||||
<TextBlock Classes="fieldLabel" Text="Log level" />
|
||||
<ComboBox x:Name="LogLevelBox" Width="150" SelectedIndex="2" CornerRadius="8">
|
||||
<ComboBoxItem Content="Trace" />
|
||||
<ComboBoxItem Content="Debug" />
|
||||
<ComboBoxItem Content="Info" />
|
||||
<ComboBoxItem Content="Warning" />
|
||||
<ComboBoxItem Content="Error" />
|
||||
<ComboBoxItem Content="Critical" />
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,24,0">
|
||||
<TextBlock Classes="fieldLabel" Text="Import trace limit (0 = off)" />
|
||||
<NumericUpDown x:Name="TraceImportsBox" Width="170" Minimum="0" Maximum="4096" Increment="16"
|
||||
Value="0" FormatString="0" CornerRadius="8" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,24,0">
|
||||
<TextBlock Classes="fieldLabel" Text="Strict dynlib resolution" />
|
||||
<ToggleSwitch x:Name="StrictToggle" OnContent="On" OffContent="Off" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,24,0">
|
||||
<TextBlock Classes="fieldLabel" Text="Log to file" />
|
||||
<ToggleSwitch x:Name="LogToFileToggle" OnContent="On" OffContent="Off" />
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Classes="fieldLabel" Text="Title music" />
|
||||
<ToggleSwitch x:Name="TitleMusicToggle" OnContent="On" OffContent="Off" IsChecked="True" />
|
||||
</StackPanel>
|
||||
</WrapPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Status bar -->
|
||||
<Grid Grid.Row="2" Background="{StaticResource ChromeBrush}" ColumnDefinitions="*,Auto">
|
||||
<Grid x:Name="StatusBar" Grid.Row="2" Height="32" Background="{StaticResource ChromeBrush}"
|
||||
ColumnDefinitions="*,Auto">
|
||||
<TextBlock x:Name="EmulatorPathText" Grid.Column="0" Text="Emulator: locating…" FontSize="11"
|
||||
Foreground="{StaticResource FaintBrush}" VerticalAlignment="Center" Margin="16,0"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,14 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Languages\*.json">
|
||||
<LogicalName>Languages.%(Filename)%(Extension)</LogicalName>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
|
||||
<!-- The controller readers (DualSense raw HID + Xbox XInput) are shared
|
||||
with the emulator's pad HLE. They are dependency-free, so they are
|
||||
compiled in directly rather than pulling a reference to all of
|
||||
|
||||
@@ -47,7 +47,7 @@ internal sealed class SndPreviewPlayer
|
||||
{
|
||||
// Debounce so skimming through the library does not decode (or
|
||||
// start) a preview per tile.
|
||||
await Task.Delay(300).ConfigureAwait(false);
|
||||
await Task.Delay(120).ConfigureAwait(false);
|
||||
|
||||
byte[]? wav;
|
||||
lock (_sync)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace SharpEmu.HLE;
|
||||
|
||||
@@ -12,6 +13,7 @@ public sealed class ModuleManager : IModuleManager
|
||||
private readonly ConcurrentDictionary<string, ExportedFunction> _exportTable = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, ExportedFunction> _exportNameTable = new(StringComparer.Ordinal);
|
||||
private readonly object _registrationGate = new();
|
||||
private readonly HashSet<(Assembly Assembly, Generation Generation)> _scannedAssemblies = new();
|
||||
private bool _isFrozen;
|
||||
|
||||
public int RegisterFromAssembly(Assembly assembly, Generation generation, ISymbolCatalog? symbolCatalog = null)
|
||||
@@ -25,6 +27,12 @@ public sealed class ModuleManager : IModuleManager
|
||||
throw new InvalidOperationException("Module registration is frozen.");
|
||||
}
|
||||
|
||||
// Deduplicated: one assembly is reached through many types.
|
||||
if (!_scannedAssemblies.Add((assembly, generation)))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var registeredCount = 0;
|
||||
var instances = new Dictionary<Type, object>();
|
||||
|
||||
@@ -73,8 +81,195 @@ public sealed class ModuleManager : IModuleManager
|
||||
{
|
||||
_isFrozen = true;
|
||||
}
|
||||
|
||||
WarmHleTypeInitializers();
|
||||
}
|
||||
|
||||
// A .cctor or first JIT running on a guest thread's hijacked stack fail-fasts the CLR.
|
||||
// Run every HLE type's initializer and JIT its methods here first, on a host thread.
|
||||
private void WarmHleTypeInitializers()
|
||||
{
|
||||
Assembly[] assemblies;
|
||||
lock (_registrationGate)
|
||||
{
|
||||
assemblies = _scannedAssemblies.Select(entry => entry.Assembly).Distinct().ToArray();
|
||||
}
|
||||
|
||||
assemblies = WithGuestReachableDependencies(assemblies);
|
||||
var bclWarmed = WarmFrameworkTypeInitializers();
|
||||
|
||||
var warmed = 0;
|
||||
var failed = 0;
|
||||
var jitted = 0;
|
||||
var jitFailed = 0;
|
||||
foreach (var assembly in assemblies)
|
||||
{
|
||||
Type[] types;
|
||||
try
|
||||
{
|
||||
types = assembly.GetTypes();
|
||||
}
|
||||
catch (ReflectionTypeLoadException ex)
|
||||
{
|
||||
types = ex.Types.Where(t => t is not null).ToArray()!;
|
||||
}
|
||||
|
||||
const BindingFlags allMembers = BindingFlags.Public | BindingFlags.NonPublic |
|
||||
BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
|
||||
|
||||
foreach (var type in types)
|
||||
{
|
||||
if (type is null || type.ContainsGenericParameters)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
RuntimeHelpers.RunClassConstructor(type.TypeHandle);
|
||||
warmed++;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// A throw here beats a guest-thread fail-fast later; swallow and continue.
|
||||
failed++;
|
||||
}
|
||||
|
||||
// Force-JIT (not execute) every method so no guest thread compiles one first.
|
||||
MethodBase[] members;
|
||||
try
|
||||
{
|
||||
members = type.GetConstructors(allMembers)
|
||||
.Concat<MethodBase>(type.GetMethods(allMembers))
|
||||
.ToArray();
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var member in members)
|
||||
{
|
||||
if (member.ContainsGenericParameters || member.IsAbstract || member.MethodImplementationFlags.HasFlag(MethodImplAttributes.InternalCall))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
RuntimeHelpers.PrepareMethod(member.MethodHandle);
|
||||
jitted++;
|
||||
}
|
||||
catch
|
||||
{
|
||||
jitFailed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[HLE] Warmed {warmed} type initializers ({failed} threw) + JIT-compiled {jitted} methods ({jitFailed} skipped) across {assemblies.Length} HLE assemblies, plus {bclWarmed} framework type initializers.");
|
||||
}
|
||||
|
||||
// Framework .cctors too (but not JIT — the BCL is too large).
|
||||
private static int WarmFrameworkTypeInitializers()
|
||||
{
|
||||
var warmed = 0;
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
var name = assembly.GetName().Name;
|
||||
if (name is null || !IsFrameworkAssembly(name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Type[] types;
|
||||
try
|
||||
{
|
||||
types = assembly.GetTypes();
|
||||
}
|
||||
catch (ReflectionTypeLoadException ex)
|
||||
{
|
||||
types = ex.Types.Where(t => t is not null).ToArray()!;
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var type in types)
|
||||
{
|
||||
if (type is null || type.ContainsGenericParameters)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
RuntimeHelpers.RunClassConstructor(type.TypeHandle);
|
||||
warmed++;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return warmed;
|
||||
}
|
||||
|
||||
private static bool IsFrameworkAssembly(string assemblyName) =>
|
||||
assemblyName.StartsWith("System", StringComparison.Ordinal) ||
|
||||
string.Equals(assemblyName, "netstandard", StringComparison.Ordinal);
|
||||
|
||||
// Warm the interop assemblies guest threads reach (e.g. Silk.NET via the flip path).
|
||||
private static Assembly[] WithGuestReachableDependencies(Assembly[] scanned)
|
||||
{
|
||||
var result = new Dictionary<string, Assembly>(StringComparer.Ordinal);
|
||||
foreach (var assembly in scanned)
|
||||
{
|
||||
result[assembly.FullName ?? assembly.GetName().Name ?? string.Empty] = assembly;
|
||||
}
|
||||
|
||||
foreach (var assembly in scanned)
|
||||
{
|
||||
AssemblyName[] references;
|
||||
try
|
||||
{
|
||||
references = assembly.GetReferencedAssemblies();
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var reference in references)
|
||||
{
|
||||
var name = reference.Name;
|
||||
if (name is null || !IsGuestReachableInterop(name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var loaded = Assembly.Load(reference);
|
||||
result[loaded.FullName ?? name] = loaded;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.Values.ToArray();
|
||||
}
|
||||
|
||||
private static bool IsGuestReachableInterop(string assemblyName) =>
|
||||
assemblyName.StartsWith("Silk.NET", StringComparison.Ordinal) ||
|
||||
assemblyName.StartsWith("SharpEmu", StringComparison.Ordinal);
|
||||
|
||||
public bool TryGetFunction(string nid, out Delegate function)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(nid);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {}
|
||||
"net10.0": {
|
||||
"sharpemu.logging": {
|
||||
"type": "Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -621,6 +621,8 @@ internal static class Gen5ShaderTranslator
|
||||
0x10 => "SSendmsg",
|
||||
0x16 => "STtraceData",
|
||||
0x20 => "SInstPrefetch",
|
||||
0x21 => "SClause",
|
||||
0x23 => "SWaitcntDepctr",
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
@@ -718,7 +720,7 @@ internal static class Gen5ShaderTranslator
|
||||
}
|
||||
|
||||
var src0 = word & 0x1FF;
|
||||
sizeDwords = opcode is 0x20 or 0x21 || src0 is 0xF9 or 0xFA or 0xFF ? 2u : 1u;
|
||||
sizeDwords = opcode is 0x20 or 0x21 or 0x2C or 0x2D || src0 is 0xF9 or 0xFA or 0xFF ? 2u : 1u;
|
||||
error = string.Empty;
|
||||
name = opcode switch
|
||||
{
|
||||
@@ -757,7 +759,9 @@ internal static class Gen5ShaderTranslator
|
||||
0x28 => "VAddcU32",
|
||||
0x29 => "VSubbU32",
|
||||
0x2A => "VSubbrevU32",
|
||||
0x2B => "VLdexpF32",
|
||||
0x2B => "VFmacF32",
|
||||
0x2C => "VFmamkF32",
|
||||
0x2D => "VFmaakF32",
|
||||
0x2F => "VCvtPkrtzF16F32",
|
||||
0x30 => "VCvtPkU16U32",
|
||||
0x31 => "VCvtPkI16I32",
|
||||
@@ -870,6 +874,7 @@ internal static class Gen5ShaderTranslator
|
||||
0x10F => "VMinF32",
|
||||
0x110 => "VMaxF32",
|
||||
0x11F => "VMacF32",
|
||||
0x12B => "VFmacF32",
|
||||
0x12F => "VCvtPkrtzF16F32",
|
||||
0x141 => "VMadF32",
|
||||
0x143 => "VMadU32U24",
|
||||
@@ -897,6 +902,7 @@ internal static class Gen5ShaderTranslator
|
||||
0x169 => "VMulLoU32",
|
||||
0x16A => "VMulHiU32",
|
||||
0x16B => "VMulLoI32",
|
||||
0x16C => "VMulHiI32",
|
||||
0x360 => "VMadU32U16",
|
||||
0x361 => "VMulLoU32",
|
||||
0x362 => "VLdexpF32",
|
||||
@@ -1379,7 +1385,7 @@ internal static class Gen5ShaderTranslator
|
||||
Gen5Operand.Source(word & 0x1FF, literal),
|
||||
Gen5Operand.Vector((word >> 9) & 0xFF),
|
||||
];
|
||||
if (opcode == "VMadMkF32" && literal.HasValue)
|
||||
if (opcode is "VMadMkF32" or "VFmamkF32" && literal.HasValue)
|
||||
{
|
||||
sources =
|
||||
[
|
||||
@@ -1388,7 +1394,7 @@ internal static class Gen5ShaderTranslator
|
||||
sources[1],
|
||||
];
|
||||
}
|
||||
else if (opcode == "VMadAkF32" && literal.HasValue)
|
||||
else if (opcode is "VMadAkF32" or "VFmaakF32" && literal.HasValue)
|
||||
{
|
||||
sources =
|
||||
[
|
||||
|
||||
@@ -255,6 +255,8 @@ internal static partial class Gen5SpirvTranslator
|
||||
case "VFmaF32":
|
||||
case "VMadMkF32":
|
||||
case "VMadAkF32":
|
||||
case "VFmamkF32":
|
||||
case "VFmaakF32":
|
||||
result = EmitFloatResult(
|
||||
instruction,
|
||||
Ext(
|
||||
@@ -265,6 +267,7 @@ internal static partial class Gen5SpirvTranslator
|
||||
GetFloatSource(instruction, 2)));
|
||||
break;
|
||||
case "VMacF32":
|
||||
case "VFmacF32":
|
||||
{
|
||||
var addend = Bitcast(_floatType, LoadV(destination));
|
||||
result = EmitFloatResult(
|
||||
@@ -334,6 +337,7 @@ internal static partial class Gen5SpirvTranslator
|
||||
result = EmitSubtractWithBorrow(instruction, reverse: true);
|
||||
break;
|
||||
case "VMulLoU32":
|
||||
case "VMulLoI32":
|
||||
case "VMulU32U24":
|
||||
result = EmitIntegerBinary(instruction, SpirvOp.IMul);
|
||||
break;
|
||||
@@ -369,6 +373,29 @@ internal static partial class Gen5SpirvTranslator
|
||||
_module.Constant64(_ulongType, 32)));
|
||||
break;
|
||||
}
|
||||
case "VMulHiI32":
|
||||
{
|
||||
var wideLeft = _module.AddInstruction(
|
||||
SpirvOp.SConvert,
|
||||
_longType,
|
||||
Bitcast(_intType, GetRawSource(instruction, 0)));
|
||||
var wideRight = _module.AddInstruction(
|
||||
SpirvOp.SConvert,
|
||||
_longType,
|
||||
Bitcast(_intType, GetRawSource(instruction, 1)));
|
||||
var product = _module.AddInstruction(
|
||||
SpirvOp.IMul,
|
||||
_longType,
|
||||
wideLeft,
|
||||
wideRight);
|
||||
result = _module.AddInstruction(
|
||||
SpirvOp.UConvert,
|
||||
_uintType,
|
||||
ShiftRightLogical64(
|
||||
Bitcast(_ulongType, product),
|
||||
_module.Constant64(_ulongType, 32)));
|
||||
break;
|
||||
}
|
||||
case "VMadU32U24":
|
||||
{
|
||||
var left = BitwiseAnd(
|
||||
@@ -1109,6 +1136,52 @@ internal static partial class Gen5SpirvTranslator
|
||||
left,
|
||||
right);
|
||||
break;
|
||||
case "SMulHiU32":
|
||||
{
|
||||
var wideLeft = _module.AddInstruction(
|
||||
SpirvOp.UConvert,
|
||||
_ulongType,
|
||||
left);
|
||||
var wideRight = _module.AddInstruction(
|
||||
SpirvOp.UConvert,
|
||||
_ulongType,
|
||||
right);
|
||||
var product = _module.AddInstruction(
|
||||
SpirvOp.IMul,
|
||||
_ulongType,
|
||||
wideLeft,
|
||||
wideRight);
|
||||
result = _module.AddInstruction(
|
||||
SpirvOp.UConvert,
|
||||
_uintType,
|
||||
ShiftRightLogical64(
|
||||
product,
|
||||
_module.Constant64(_ulongType, 32)));
|
||||
break;
|
||||
}
|
||||
case "SMulHiI32":
|
||||
{
|
||||
var wideLeft = _module.AddInstruction(
|
||||
SpirvOp.SConvert,
|
||||
_longType,
|
||||
Bitcast(_intType, left));
|
||||
var wideRight = _module.AddInstruction(
|
||||
SpirvOp.SConvert,
|
||||
_longType,
|
||||
Bitcast(_intType, right));
|
||||
var product = _module.AddInstruction(
|
||||
SpirvOp.IMul,
|
||||
_longType,
|
||||
wideLeft,
|
||||
wideRight);
|
||||
result = _module.AddInstruction(
|
||||
SpirvOp.UConvert,
|
||||
_uintType,
|
||||
ShiftRightLogical64(
|
||||
Bitcast(_ulongType, product),
|
||||
_module.Constant64(_ulongType, 32)));
|
||||
break;
|
||||
}
|
||||
case "SAndB32":
|
||||
result = BitwiseAnd(left, right);
|
||||
Store(_scc, IsNotZero(result));
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ public static class KernelPthreadCompatExports
|
||||
|
||||
private static readonly object _stateGate = new();
|
||||
private static readonly ConcurrentDictionary<ulong, PthreadMutexState> _mutexStates = new();
|
||||
private static readonly ConcurrentDictionary<ulong, string> _mutexWakeKeys = new();
|
||||
private static readonly ConcurrentDictionary<ulong, string> _condWakeKeys = new();
|
||||
private static readonly Dictionary<ulong, PthreadMutexAttrState> _mutexAttrStates = new();
|
||||
private static readonly Dictionary<ulong, PthreadCondState> _condStates = new();
|
||||
private static readonly Dictionary<ulong, object> _onceGates = new();
|
||||
@@ -35,6 +37,13 @@ public static class KernelPthreadCompatExports
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREAD_CONDS"), "1", StringComparison.Ordinal);
|
||||
private static readonly HashSet<ulong>? _tracePthreadMutexFilter = ParseTraceAddressFilter(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREAD_MUTEX_FILTER"));
|
||||
private static readonly bool _enableMutexLockBlocking =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_MUTEX_LOCK_BLOCKING"), "1", StringComparison.Ordinal);
|
||||
|
||||
// On the outer class deliberately: a static on the nested state classes gives them a
|
||||
// type initializer that first runs on a guest thread and fail-fasts the CLR.
|
||||
private static long _nextMutexWakeId;
|
||||
private static long _nextCondWakeId;
|
||||
|
||||
private sealed class PthreadMutexState
|
||||
{
|
||||
@@ -43,6 +52,10 @@ public static class KernelPthreadCompatExports
|
||||
public int RecursionCount { get; set; }
|
||||
public int Type { get; set; } = MutexTypeErrorCheck;
|
||||
public int Protocol { get; set; }
|
||||
|
||||
// Keyed on state identity, not the resolved address (which can differ between a
|
||||
// lock and its unlock for the same state, stranding the waiter).
|
||||
public string WakeKey { get; } = "pthread_mutex#" + Interlocked.Increment(ref _nextMutexWakeId).ToString("X");
|
||||
}
|
||||
|
||||
private sealed class PthreadMutexWaiter
|
||||
@@ -56,6 +69,23 @@ public static class KernelPthreadCompatExports
|
||||
public object SyncRoot { get; } = new();
|
||||
public ulong SignalEpoch { get; set; }
|
||||
public int Waiters { get; set; }
|
||||
|
||||
// See PthreadMutexState.WakeKey.
|
||||
public string WakeKey { get; } = "pthread_cond#" + Interlocked.Increment(ref _nextCondWakeId).ToString("X");
|
||||
|
||||
// A signal with no waiter stays pending; the guest often signals before the wait.
|
||||
public int PendingSignals { get; set; }
|
||||
|
||||
public bool TryConsumePendingSignal()
|
||||
{
|
||||
if (PendingSignals <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
PendingSignals--;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct PthreadMutexAttrState(int Type, int Protocol);
|
||||
@@ -112,6 +142,27 @@ public static class KernelPthreadCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "GBUY7ywdULE",
|
||||
ExportName = "scePthreadRename",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadRename(CpuContext ctx)
|
||||
{
|
||||
if (_tracePthreads)
|
||||
{
|
||||
var nameAddress = ctx[CpuRegister.Rsi];
|
||||
var name = nameAddress != 0 &&
|
||||
KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, nameAddress, 64, out var value)
|
||||
? value
|
||||
: "<unreadable>";
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] pthread.rename thread=0x{ctx[CpuRegister.Rdi]:X16} name=\"{name}\"");
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "EI-5-jlq2dE",
|
||||
ExportName = "scePthreadGetthreadid",
|
||||
@@ -293,6 +344,38 @@ public static class KernelPthreadCompatExports
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadCondTimedwait(CpuContext ctx) => PthreadCondWaitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], timed: true, timeoutUsec: unchecked((uint)ctx[CpuRegister.Rdx]));
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "27bAgiJmOh0",
|
||||
ExportName = "pthread_cond_timedwait",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixPthreadCondTimedwait(CpuContext ctx)
|
||||
{
|
||||
// POSIX passes `const struct timespec *abstime` (absolute deadline), not relative microseconds (#113).
|
||||
var abstimeAddress = ctx[CpuRegister.Rdx];
|
||||
if (abstimeAddress == 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt64(abstimeAddress, out var deadlineSeconds) ||
|
||||
!ctx.TryReadUInt64(abstimeAddress + 8, out var deadlineNanoseconds))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
var nowMicroseconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1000L;
|
||||
var deadlineMicroseconds =
|
||||
deadlineSeconds >= long.MaxValue / 1_000_000UL
|
||||
? long.MaxValue
|
||||
: (long)(deadlineSeconds * 1_000_000UL + Math.Min(deadlineNanoseconds, 999_999_999UL) / 1000UL);
|
||||
var remainingMicroseconds = deadlineMicroseconds - nowMicroseconds;
|
||||
var timeoutUsec = remainingMicroseconds <= 0
|
||||
? 1u
|
||||
: (uint)Math.Min(remainingMicroseconds, uint.MaxValue);
|
||||
return PthreadCondWaitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], timed: true, timeoutUsec: timeoutUsec);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "kDh-NfxgMtE",
|
||||
ExportName = "scePthreadCondSignal",
|
||||
@@ -327,12 +410,6 @@ public static class KernelPthreadCompatExports
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixPthreadCondSignal(CpuContext ctx) => PthreadCondSignalCore(ctx, ctx[CpuRegister.Rdi], broadcast: false);
|
||||
[SysAbiExport(
|
||||
Nid = "27bAgiJmOh0",
|
||||
ExportName = "pthread_cond_timedwait",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixPthreadCondTimedwait(CpuContext ctx) => PthreadCondWaitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], timed: true, timeoutUsec: unchecked((uint)ctx[CpuRegister.Rdx]));
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "m5-2bsNfv7s",
|
||||
@@ -559,42 +636,42 @@ public static class KernelPthreadCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||
}
|
||||
|
||||
// Normal/adaptive mutexes do not report EDEADLK on self-lock.
|
||||
// Under the current single-host-thread guest execution model,
|
||||
// treating them as nested ownership keeps init paths moving
|
||||
// without turning a would-block path into a hard error.
|
||||
state.RecursionCount++;
|
||||
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
var ownedResult = tryOnly
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
||||
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, ownedResult);
|
||||
return ownedResult;
|
||||
else
|
||||
{
|
||||
var ownedResult = tryOnly
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
||||
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, ownedResult);
|
||||
return ownedResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var acquired = state.Semaphore.Wait(0);
|
||||
if (!acquired)
|
||||
{
|
||||
// Guest-thread blocking for pthread_mutex_lock is currently disabled.
|
||||
// Demon's Souls deadlocks during PS5SyncEvent initialization.
|
||||
/* var waiter = new PthreadMutexWaiter { ThreadId = currentThreadId };
|
||||
if (!tryOnly &&
|
||||
var waiter = new PthreadMutexWaiter { ThreadId = currentThreadId };
|
||||
// Cooperative deschedule on a contended lock corrupts fiber state
|
||||
// (Demon's Souls sceFiberSwitch -> ESRCH). Off by default: fall through to the
|
||||
// synchronous host-thread wait below.
|
||||
if (_enableMutexLockBlocking &&
|
||||
!tryOnly &&
|
||||
GuestThreadExecution.IsGuestThread &&
|
||||
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
|
||||
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"pthread_mutex_lock",
|
||||
GetMutexWakeKey(resolvedAddress),
|
||||
state.WakeKey,
|
||||
() => CompleteBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter),
|
||||
() => TryReserveBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter)))
|
||||
{
|
||||
TracePthreadMutex(ctx, "lock-block", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
} */
|
||||
}
|
||||
|
||||
if (!tryOnly)
|
||||
{
|
||||
@@ -611,6 +688,7 @@ public static class KernelPthreadCompatExports
|
||||
|
||||
lock (state)
|
||||
{
|
||||
DetectMutexDoubleOwner(resolvedAddress, state, currentThreadId, "lock");
|
||||
state.OwnerThreadId = currentThreadId;
|
||||
state.RecursionCount = 1;
|
||||
}
|
||||
@@ -633,16 +711,24 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
|
||||
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
||||
var lenientOwnership = state.Type is MutexTypeNormal or MutexTypeAdaptiveNp;
|
||||
|
||||
var shouldRelease = false;
|
||||
lock (state)
|
||||
{
|
||||
if (state.RecursionCount <= 0)
|
||||
{
|
||||
if (lenientOwnership)
|
||||
{
|
||||
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (requireOwner && state.OwnerThreadId != currentThreadId)
|
||||
if (requireOwner && !lenientOwnership && state.OwnerThreadId != currentThreadId)
|
||||
{
|
||||
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
||||
@@ -661,12 +747,15 @@ public static class KernelPthreadCompatExports
|
||||
try
|
||||
{
|
||||
state.Semaphore.Release();
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetMutexWakeKey(resolvedAddress), 1);
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(state.WakeKey, 1);
|
||||
}
|
||||
catch (SemaphoreFullException)
|
||||
{
|
||||
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
if (!lenientOwnership)
|
||||
{
|
||||
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1090,20 +1179,28 @@ public static class KernelPthreadCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (!TryResolveCondState(ctx, condAddress, createIfZero: true, out _, out var state))
|
||||
if (!TryResolveCondState(ctx, condAddress, createIfZero: true, out var resolvedCondAddress, out var state))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
var waitResult = (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
var spuriousWake = false;
|
||||
var releasedRecursion = 0;
|
||||
lock (state.SyncRoot)
|
||||
{
|
||||
state.Waiters++;
|
||||
var observedEpoch = state.SignalEpoch;
|
||||
TracePthreadCond("wait-enter", condAddress, mutexAddress, state, timed, waitResult);
|
||||
var consumedPendingSignal = state.TryConsumePendingSignal();
|
||||
TracePthreadCond(
|
||||
consumedPendingSignal ? "wait-enter-pending" : "wait-enter",
|
||||
condAddress,
|
||||
mutexAddress,
|
||||
state,
|
||||
timed,
|
||||
waitResult);
|
||||
|
||||
var unlockResult = PthreadMutexUnlockCore(ctx, mutexAddress, requireOwner: true);
|
||||
var unlockResult = PthreadMutexFullUnlockForCondWait(ctx, mutexAddress, out releasedRecursion);
|
||||
if (unlockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
state.Waiters--;
|
||||
@@ -1111,10 +1208,34 @@ public static class KernelPthreadCompatExports
|
||||
return unlockResult;
|
||||
}
|
||||
|
||||
var scheduler = GuestThreadExecution.Scheduler;
|
||||
if (!timed && GuestThreadExecution.RequestCurrentThreadBlock("pthread_cond_wait"))
|
||||
if (consumedPendingSignal)
|
||||
{
|
||||
TracePthreadCond("wait-block", condAddress, mutexAddress, state, timed, waitResult);
|
||||
// Leave SyncRoot before relocking the mutex to avoid lock-order inversion with cond-signal (#113).
|
||||
state.Waiters = Math.Max(0, state.Waiters - 1);
|
||||
TracePthreadCond("wait-wake-pending", condAddress, mutexAddress, state, timed, waitResult);
|
||||
Monitor.Exit(state.SyncRoot);
|
||||
try
|
||||
{
|
||||
var pendingLockResult = PthreadMutexRelockForCondWait(ctx, mutexAddress, releasedRecursion);
|
||||
return pendingLockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK ? pendingLockResult : waitResult;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Monitor.Enter(state.SyncRoot);
|
||||
}
|
||||
}
|
||||
|
||||
var scheduler = GuestThreadExecution.Scheduler;
|
||||
if (GuestThreadExecution.IsGuestThread &&
|
||||
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
timed ? "pthread_cond_timedwait" : "pthread_cond_wait",
|
||||
state.WakeKey,
|
||||
() => ResumePthreadCondWait(ctx, condAddress, mutexAddress, state, observedEpoch, timed, releasedRecursion),
|
||||
() => state.SignalEpoch != observedEpoch,
|
||||
timed ? GuestThreadExecution.ComputeDeadlineTimestamp(GetCondWaitTimeout(timeoutUsec)) : 0))
|
||||
{
|
||||
TracePthreadCond(timed ? "wait-block-timed" : "wait-block", condAddress, mutexAddress, state, timed, waitResult);
|
||||
return waitResult;
|
||||
}
|
||||
|
||||
@@ -1137,6 +1258,21 @@ public static class KernelPthreadCompatExports
|
||||
{
|
||||
if (!Monitor.Wait(state.SyncRoot, GetCondSpuriousWakeTimeout()))
|
||||
{
|
||||
if (scheduler is not null && !GuestThreadExecution.IsGuestThread)
|
||||
{
|
||||
Monitor.Exit(state.SyncRoot);
|
||||
try
|
||||
{
|
||||
scheduler.Pump(ctx, "pthread_cond_wait");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Monitor.Enter(state.SyncRoot);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
spuriousWake = true;
|
||||
break;
|
||||
}
|
||||
@@ -1163,7 +1299,7 @@ public static class KernelPthreadCompatExports
|
||||
waitResult);
|
||||
}
|
||||
|
||||
var lockResult = PthreadMutexLockCore(ctx, mutexAddress, tryOnly: false);
|
||||
var lockResult = PthreadMutexRelockForCondWait(ctx, mutexAddress, releasedRecursion);
|
||||
if (lockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
TracePthreadCond("wait-relock-fail", condAddress, mutexAddress, state, timed, lockResult);
|
||||
@@ -1189,16 +1325,19 @@ public static class KernelPthreadCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (!TryResolveCondState(ctx, condAddress, createIfZero: true, out _, out var state))
|
||||
if (!TryResolveCondState(ctx, condAddress, createIfZero: true, out var resolvedCondAddress, out var state))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
var shouldWakeScheduler = false;
|
||||
lock (state.SyncRoot)
|
||||
{
|
||||
// Advance the epoch even with no waiter registered — it's the wait predicate.
|
||||
state.SignalEpoch++;
|
||||
if (state.Waiters > 0)
|
||||
{
|
||||
state.SignalEpoch++;
|
||||
shouldWakeScheduler = true;
|
||||
if (broadcast)
|
||||
{
|
||||
Monitor.PulseAll(state.SyncRoot);
|
||||
@@ -1208,15 +1347,135 @@ public static class KernelPthreadCompatExports
|
||||
Monitor.Pulse(state.SyncRoot);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
state.PendingSignals++;
|
||||
}
|
||||
|
||||
TracePthreadCond(broadcast ? "broadcast" : "signal", condAddress, mutexAddress: 0, state, timed: false, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
if (shouldWakeScheduler)
|
||||
{
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(
|
||||
state.WakeKey,
|
||||
broadcast ? int.MaxValue : 1);
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static int ResumePthreadCondWait(
|
||||
CpuContext ctx,
|
||||
ulong condAddress,
|
||||
ulong mutexAddress,
|
||||
PthreadCondState state,
|
||||
ulong observedEpoch,
|
||||
bool timed,
|
||||
int releasedRecursion)
|
||||
{
|
||||
var waitResult = (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
lock (state.SyncRoot)
|
||||
{
|
||||
state.Waiters = Math.Max(0, state.Waiters - 1);
|
||||
if (timed && state.SignalEpoch == observedEpoch)
|
||||
{
|
||||
waitResult = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||
}
|
||||
|
||||
TracePthreadCond(
|
||||
waitResult == (int)OrbisGen2Result.ORBIS_GEN2_OK ? "wait-resume" : "wait-resume-timeout",
|
||||
condAddress,
|
||||
mutexAddress,
|
||||
state,
|
||||
timed,
|
||||
waitResult);
|
||||
}
|
||||
|
||||
var lockResult = PthreadMutexRelockForCondWait(ctx, mutexAddress, releasedRecursion);
|
||||
return lockResult == (int)OrbisGen2Result.ORBIS_GEN2_OK ? waitResult : lockResult;
|
||||
}
|
||||
|
||||
private static string GetCondWakeKey(ulong resolvedCondAddress) =>
|
||||
_condWakeKeys.GetOrAdd(resolvedCondAddress, static address => string.Create(
|
||||
31,
|
||||
address,
|
||||
static (destination, value) =>
|
||||
{
|
||||
"pthread_cond:0x".AsSpan().CopyTo(destination);
|
||||
_ = value.TryFormat(destination[15..], out _, "X16");
|
||||
}));
|
||||
|
||||
private static int PthreadMutexFullUnlockForCondWait(CpuContext ctx, ulong mutexAddress, out int releasedRecursion)
|
||||
{
|
||||
releasedRecursion = 0;
|
||||
if (!TryResolveMutexState(ctx, mutexAddress, createIfZero: false, out _, out var state))
|
||||
{
|
||||
return PthreadMutexUnlockCore(ctx, mutexAddress, requireOwner: true);
|
||||
}
|
||||
|
||||
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
||||
lock (state)
|
||||
{
|
||||
if (state.RecursionCount <= 0 || state.OwnerThreadId != currentThreadId)
|
||||
{
|
||||
return PthreadMutexUnlockCore(ctx, mutexAddress, requireOwner: true);
|
||||
}
|
||||
|
||||
releasedRecursion = state.RecursionCount;
|
||||
state.RecursionCount = 1;
|
||||
}
|
||||
|
||||
return PthreadMutexUnlockCore(ctx, mutexAddress, requireOwner: true);
|
||||
}
|
||||
|
||||
private static int PthreadMutexRelockForCondWait(CpuContext ctx, ulong mutexAddress, int releasedRecursion)
|
||||
{
|
||||
var result = PthreadMutexLockCore(ctx, mutexAddress, tryOnly: false);
|
||||
if (result != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
if (releasedRecursion > 1 &&
|
||||
TryResolveMutexState(ctx, mutexAddress, createIfZero: false, out _, out var state))
|
||||
{
|
||||
lock (state)
|
||||
{
|
||||
if (state.OwnerThreadId == KernelPthreadState.GetCurrentThreadHandle())
|
||||
{
|
||||
state.RecursionCount = releasedRecursion;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
public static string? DumpMutexStateForStall(ulong mutexAddress)
|
||||
{
|
||||
if (!_mutexStates.TryGetValue(mutexAddress, out var state))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var waiterThreads = string.Join(",", _mutexStates
|
||||
.Where(pair => ReferenceEquals(pair.Value, state))
|
||||
.Select(pair => $"0x{pair.Key:X}"));
|
||||
|
||||
return $"mutex=0x{mutexAddress:X16} owner=0x{state.OwnerThreadId:X} recursion={state.RecursionCount} " +
|
||||
$"type={state.Type} semaphore_count={state.Semaphore.CurrentCount} aliases=[{waiterThreads}]";
|
||||
}
|
||||
|
||||
private static string GetMutexWakeKey(ulong resolvedMutexAddress) =>
|
||||
$"pthread_mutex:0x{resolvedMutexAddress:X16}";
|
||||
_mutexWakeKeys.GetOrAdd(resolvedMutexAddress, static address => string.Create(
|
||||
32,
|
||||
address,
|
||||
static (destination, value) =>
|
||||
{
|
||||
"pthread_mutex:0x".AsSpan().CopyTo(destination);
|
||||
_ = value.TryFormat(destination[16..], out _, "X16");
|
||||
}));
|
||||
|
||||
private static bool TryReserveBlockedMutexLock(
|
||||
CpuContext ctx,
|
||||
@@ -1233,6 +1492,13 @@ public static class KernelPthreadCompatExports
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!state.Semaphore.Wait(0))
|
||||
{
|
||||
TracePthreadMutex(ctx, "lock-reserve-busy", mutexAddress, resolvedAddress, state, waiter.ThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||
return false;
|
||||
}
|
||||
|
||||
DetectMutexDoubleOwner(resolvedAddress, state, waiter.ThreadId, "reserve");
|
||||
state.OwnerThreadId = waiter.ThreadId;
|
||||
state.RecursionCount = 1;
|
||||
Interlocked.Exchange(ref waiter.Reserved, 1);
|
||||
@@ -1264,6 +1530,7 @@ public static class KernelPthreadCompatExports
|
||||
|
||||
lock (state)
|
||||
{
|
||||
DetectMutexDoubleOwner(resolvedAddress, state, currentThreadId, "resume");
|
||||
state.OwnerThreadId = currentThreadId;
|
||||
state.RecursionCount = 1;
|
||||
}
|
||||
@@ -1272,6 +1539,19 @@ public static class KernelPthreadCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// A nonzero foreign prior owner means two guest threads are in the same critical
|
||||
// section. Call while holding lock(state).
|
||||
private static void DetectMutexDoubleOwner(ulong resolvedAddress, PthreadMutexState state, ulong currentThreadId, string site)
|
||||
{
|
||||
var priorOwner = state.OwnerThreadId;
|
||||
if (priorOwner != 0 && priorOwner != currentThreadId)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][ERROR] MUTEX DOUBLE-OWNER at {site}: resolved=0x{resolvedAddress:X} acquirer=0x{currentThreadId:X} " +
|
||||
$"prior_owner=0x{priorOwner:X} recursion={state.RecursionCount} type={state.Type} sem_count={state.Semaphore.CurrentCount}");
|
||||
}
|
||||
}
|
||||
|
||||
private static TimeSpan GetCondWaitTimeout(uint timeoutUsec)
|
||||
{
|
||||
if (timeoutUsec == 0)
|
||||
@@ -1409,7 +1689,8 @@ public static class KernelPthreadCompatExports
|
||||
$"[LOADER][TRACE] pthread_{operation}: mutex=0x{mutexAddress:X16} resolved=0x{resolvedAddress:X16} " +
|
||||
$"guest[0]=0x{guestWord0:X16} guest[8]=0x{guestWord1:X16} " +
|
||||
$"current=0x{currentThreadId:X16} owner=0x{(state?.OwnerThreadId ?? 0):X16} " +
|
||||
$"recursion={(state?.RecursionCount ?? 0)} type={(state?.Type ?? 0)} result=0x{unchecked((uint)result):X8}");
|
||||
$"recursion={(state?.RecursionCount ?? 0)} type={(state?.Type ?? 0)} result=0x{unchecked((uint)result):X8} " +
|
||||
$"guest_thread=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} managed={Environment.CurrentManagedThreadId}");
|
||||
}
|
||||
|
||||
private static void TracePthreadCond(string operation, ulong condAddress, ulong mutexAddress, PthreadCondState? state, bool timed, int result)
|
||||
|
||||
@@ -31,6 +31,8 @@ public static class KernelPthreadExtendedCompatExports
|
||||
private static long _nextSyntheticRwlockHandleId = 1;
|
||||
private static long _nextSyntheticPthreadAttrHandleId = 1;
|
||||
private static long _nextSyntheticRwlockAttrHandleId = 1;
|
||||
private static readonly bool _strictRwlockWriterPreference =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_STRICT_RWLOCK_WRITER_PREFERENCE"), "1", StringComparison.Ordinal);
|
||||
|
||||
private static readonly ConcurrentDictionary<ulong, ConcurrentDictionary<int, ulong>> _threadLocalSpecific = new();
|
||||
|
||||
@@ -85,14 +87,23 @@ public static class KernelPthreadExtendedCompatExports
|
||||
public PthreadAttrState Attributes { get; set; } = PthreadAttrState.Default;
|
||||
}
|
||||
|
||||
// On the outer class deliberately: a static on the nested state class gives it a type
|
||||
// initializer that first runs on a guest thread and fail-fasts the CLR.
|
||||
private static long _nextRwlockWakeId;
|
||||
|
||||
private sealed class PthreadRwlockState
|
||||
{
|
||||
public object SyncRoot { get; } = new();
|
||||
public Dictionary<ulong, int> ReaderCounts { get; } = new();
|
||||
public Dictionary<ulong, int> CompatWriterCounts { get; } = new();
|
||||
public int ReaderTotalCount { get; set; }
|
||||
public int CompatWriterTotalCount { get; set; }
|
||||
public ulong WriterThreadId { get; set; }
|
||||
public int WaitingWriters { get; set; }
|
||||
|
||||
// See PthreadMutexState.WakeKey.
|
||||
public string WakeKey { get; } = "pthread_rwlock#" + Interlocked.Increment(ref _nextRwlockWakeId).ToString("X");
|
||||
|
||||
public int GetReaderCount(ulong threadId)
|
||||
{
|
||||
return ReaderCounts.TryGetValue(threadId, out var count) ? count : 0;
|
||||
@@ -105,6 +116,33 @@ public static class KernelPthreadExtendedCompatExports
|
||||
ReaderTotalCount++;
|
||||
}
|
||||
|
||||
public void AddCompatWriter(ulong threadId)
|
||||
{
|
||||
CompatWriterCounts.TryGetValue(threadId, out var currentCount);
|
||||
CompatWriterCounts[threadId] = currentCount + 1;
|
||||
CompatWriterTotalCount++;
|
||||
}
|
||||
|
||||
public bool RemoveCompatWriter(ulong threadId)
|
||||
{
|
||||
if (!CompatWriterCounts.TryGetValue(threadId, out var currentCount) || currentCount <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (currentCount == 1)
|
||||
{
|
||||
CompatWriterCounts.Remove(threadId);
|
||||
}
|
||||
else
|
||||
{
|
||||
CompatWriterCounts[threadId] = currentCount - 1;
|
||||
}
|
||||
|
||||
CompatWriterTotalCount = Math.Max(0, CompatWriterTotalCount - 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool RemoveReader(ulong threadId)
|
||||
{
|
||||
if (!ReaderCounts.TryGetValue(threadId, out var currentCount) || currentCount <= 0)
|
||||
@@ -935,7 +973,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
|
||||
lock (state.SyncRoot)
|
||||
{
|
||||
if (state.WriterThreadId != 0 || state.ReaderTotalCount != 0 || state.WaitingWriters != 0)
|
||||
if (state.WriterThreadId != 0 || state.ReaderTotalCount != 0 || state.WaitingWriters != 0 || state.CompatWriterTotalCount != 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||
}
|
||||
@@ -1003,7 +1041,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: false, out _, out var rwlock))
|
||||
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: false, out var resolvedAddress, out var rwlock))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
@@ -1014,7 +1052,11 @@ public static class KernelPthreadExtendedCompatExports
|
||||
{
|
||||
lock (rwlock.SyncRoot)
|
||||
{
|
||||
if (rwlock.WriterThreadId == currentThreadId)
|
||||
if (rwlock.RemoveCompatWriter(currentThreadId))
|
||||
{
|
||||
Monitor.PulseAll(rwlock.SyncRoot);
|
||||
}
|
||||
else if (rwlock.WriterThreadId == currentThreadId)
|
||||
{
|
||||
rwlock.WriterThreadId = 0;
|
||||
Monitor.PulseAll(rwlock.SyncRoot);
|
||||
@@ -1037,6 +1079,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
||||
}
|
||||
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(rwlock.WakeKey);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -1227,7 +1270,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: true, out _, out var rwlock))
|
||||
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: true, out var resolvedAddress, out var rwlock))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
@@ -1242,20 +1285,60 @@ public static class KernelPthreadExtendedCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
||||
}
|
||||
|
||||
if (rwlock.CompatWriterCounts.GetValueOrDefault(currentThreadId) > 0)
|
||||
{
|
||||
rwlock.AddCompatWriter(currentThreadId);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (GuestThreadExecution.IsGuestThread &&
|
||||
!_strictRwlockWriterPreference &&
|
||||
rwlock.WriterThreadId == 0 &&
|
||||
rwlock.ReaderTotalCount == 0 &&
|
||||
rwlock.CompatWriterTotalCount == 0)
|
||||
{
|
||||
rwlock.AddCompatWriter(currentThreadId);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (rwlock.WriterThreadId == 0 && rwlock.ReaderTotalCount == 0 && rwlock.CompatWriterTotalCount == 0)
|
||||
{
|
||||
DetectRwlockWriterConflict(resolvedAddress, rwlock, currentThreadId, "wrlock");
|
||||
rwlock.WriterThreadId = currentThreadId;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
rwlock.WaitingWriters++;
|
||||
var transferredToScheduler = false;
|
||||
try
|
||||
{
|
||||
while (rwlock.WriterThreadId != 0 || rwlock.ReaderTotalCount != 0)
|
||||
if (GuestThreadExecution.IsGuestThread &&
|
||||
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
|
||||
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"pthread_rwlock_wrlock",
|
||||
rwlock.WakeKey,
|
||||
static () => (int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||
() => TryAcquireBlockedRwlock(rwlock, currentThreadId, write: true)))
|
||||
{
|
||||
transferredToScheduler = true;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
while (rwlock.WriterThreadId != 0 || rwlock.ReaderTotalCount != 0 || rwlock.CompatWriterTotalCount != 0)
|
||||
{
|
||||
Monitor.Wait(rwlock.SyncRoot);
|
||||
}
|
||||
|
||||
rwlock.WriterThreadId = currentThreadId;
|
||||
}
|
||||
finally
|
||||
{
|
||||
rwlock.WaitingWriters--;
|
||||
if (!transferredToScheduler)
|
||||
{
|
||||
rwlock.WaitingWriters = Math.Max(0, rwlock.WaitingWriters - 1);
|
||||
}
|
||||
}
|
||||
|
||||
rwlock.WriterThreadId = currentThreadId;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1264,12 +1347,30 @@ public static class KernelPthreadExtendedCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
||||
}
|
||||
|
||||
while (rwlock.WriterThreadId != 0 ||
|
||||
(rwlock.WaitingWriters > 0 && rwlock.GetReaderCount(currentThreadId) == 0))
|
||||
while (ReaderMustWaitForRwlock(rwlock, currentThreadId))
|
||||
{
|
||||
if (GuestThreadExecution.IsGuestThread &&
|
||||
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
|
||||
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"pthread_rwlock_rdlock",
|
||||
rwlock.WakeKey,
|
||||
static () => (int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||
() => TryAcquireBlockedRwlock(rwlock, currentThreadId, write: false)))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
Monitor.Wait(rwlock.SyncRoot);
|
||||
}
|
||||
|
||||
if (rwlock.WriterThreadId != 0 ||
|
||||
rwlock.CompatWriterTotalCount > rwlock.CompatWriterCounts.GetValueOrDefault(currentThreadId))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][ERROR] RWLOCK READER/WRITER COEXIST: resolved=0x{resolvedAddress:X} reader=0x{currentThreadId:X} " +
|
||||
$"writer=0x{rwlock.WriterThreadId:X} compat_total={rwlock.CompatWriterTotalCount} readers_total={rwlock.ReaderTotalCount}");
|
||||
}
|
||||
rwlock.AddReader(currentThreadId);
|
||||
}
|
||||
}
|
||||
@@ -1277,6 +1378,86 @@ public static class KernelPthreadExtendedCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static bool TryAcquireBlockedRwlock(PthreadRwlockState rwlock, ulong currentThreadId, bool write)
|
||||
{
|
||||
lock (rwlock.SyncRoot)
|
||||
{
|
||||
if (write)
|
||||
{
|
||||
if (rwlock.WriterThreadId != 0 || rwlock.ReaderTotalCount != 0 || rwlock.CompatWriterTotalCount != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
DetectRwlockWriterConflict(0, rwlock, currentThreadId, "wrlock-resume");
|
||||
rwlock.WriterThreadId = currentThreadId;
|
||||
rwlock.WaitingWriters = Math.Max(0, rwlock.WaitingWriters - 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ReaderMustWaitForRwlock(rwlock, currentThreadId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
rwlock.AddReader(currentThreadId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Call while holding lock(rwlock.SyncRoot): an existing reader/writer here means a
|
||||
// writer would share the rwlock with another holder — a data race.
|
||||
private static void DetectRwlockWriterConflict(ulong resolvedAddress, PthreadRwlockState rwlock, ulong currentThreadId, string site)
|
||||
{
|
||||
if (rwlock.WriterThreadId != 0 ||
|
||||
rwlock.ReaderTotalCount != 0 ||
|
||||
rwlock.CompatWriterTotalCount > rwlock.CompatWriterCounts.GetValueOrDefault(currentThreadId))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][ERROR] RWLOCK WRITER CONFLICT at {site}: resolved=0x{resolvedAddress:X} writer=0x{currentThreadId:X} " +
|
||||
$"existing_writer=0x{rwlock.WriterThreadId:X} readers_total={rwlock.ReaderTotalCount} compat_total={rwlock.CompatWriterTotalCount}");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ReaderMustWaitForRwlock(PthreadRwlockState rwlock, ulong currentThreadId)
|
||||
{
|
||||
if (rwlock.WriterThreadId != 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (rwlock.CompatWriterTotalCount > rwlock.CompatWriterCounts.GetValueOrDefault(currentThreadId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return rwlock.WaitingWriters > 0 &&
|
||||
rwlock.GetReaderCount(currentThreadId) == 0;
|
||||
}
|
||||
|
||||
private static string GetRwlockWakeKey(ulong rwlockAddress) => $"pthread_rwlock:0x{rwlockAddress:X16}";
|
||||
|
||||
public static string? DumpRwlockStateForStall(ulong rwlockAddress)
|
||||
{
|
||||
PthreadRwlockState? rwlock;
|
||||
lock (_stateGate)
|
||||
{
|
||||
if (!_rwlockStates.TryGetValue(rwlockAddress, out rwlock))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
lock (rwlock.SyncRoot)
|
||||
{
|
||||
var readers = string.Join(",", rwlock.ReaderCounts.Select(pair => $"0x{pair.Key:X}x{pair.Value}"));
|
||||
var compatWriters = string.Join(",", rwlock.CompatWriterCounts.Select(pair => $"0x{pair.Key:X}x{pair.Value}"));
|
||||
return $"rwlock=0x{rwlockAddress:X16} writer=0x{rwlock.WriterThreadId:X} waiting_writers={rwlock.WaitingWriters} " +
|
||||
$"readers_total={rwlock.ReaderTotalCount} readers=[{readers}] " +
|
||||
$"compat_writers_total={rwlock.CompatWriterTotalCount} compat_writers=[{compatWriters}]";
|
||||
}
|
||||
}
|
||||
|
||||
private static ulong ResolveRwlockHandle(CpuContext ctx, ulong rwlockAddress)
|
||||
{
|
||||
if (rwlockAddress == 0)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,189 @@ namespace SharpEmu.Libs.Rtc;
|
||||
public static class RtcExports
|
||||
{
|
||||
private const long DateTimeTicksPerMicrosecond = 10;
|
||||
private const ulong MicrosecondsPerSecond = 1_000_000UL;
|
||||
private const ulong MicrosecondsPerMinute = 60_000_000UL;
|
||||
private const ulong MicrosecondsPerHour = 3_600_000_000UL;
|
||||
private const ulong MicrosecondsPerDay = 86_400_000_000UL;
|
||||
private const ulong MicrosecondsPerWeek = 604_800_000_000UL;
|
||||
private const ulong UnixEpochTicks = 62_135_596_800_000_000UL;
|
||||
private const ulong Win32FileTimeEpochTicks = 50_491_123_200_000_000UL;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "lPEBYdVX0XQ",
|
||||
ExportName = "sceRtcCheckValid",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcCheckValid(CpuContext ctx)
|
||||
{
|
||||
var timeAddress = ctx[CpuRegister.Rdi];
|
||||
if (timeAddress == 0)
|
||||
{
|
||||
return unchecked((int)0x80B50002);
|
||||
}
|
||||
|
||||
if (!TryReadRtcDateTime(ctx, timeAddress, out var time))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
return ValidateRtcDateTime(time);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "fNaZ4DbzHAE",
|
||||
ExportName = "sceRtcCompareTick",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcCompareTick(CpuContext ctx)
|
||||
{
|
||||
var tick1Address = ctx[CpuRegister.Rdi];
|
||||
var tick2Address = ctx[CpuRegister.Rsi];
|
||||
if (tick1Address == 0 || tick2Address == 0)
|
||||
{
|
||||
return unchecked((int)0x80B50002);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt64(tick1Address, out var tick1) || !ctx.TryReadUInt64(tick2Address, out var tick2))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
return tick1 < tick2 ? -1 : tick1 > tick2 ? 1 : 0;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "8Yr143yEnRo",
|
||||
ExportName = "sceRtcConvertLocalTimeToUtc",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcConvertLocalTimeToUtc(CpuContext ctx)
|
||||
{
|
||||
var tickLocalAddress = ctx[CpuRegister.Rdi];
|
||||
var tickUtcAddress = ctx[CpuRegister.Rsi];
|
||||
if (tickLocalAddress == 0 || tickUtcAddress == 0)
|
||||
{
|
||||
return unchecked((int)0x80B50002);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt64(tickLocalAddress, out var tickLocal))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (!TryConvertTickToDateTime(tickLocal, out var localDateTime))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
DateTime utcDateTime;
|
||||
try
|
||||
{
|
||||
utcDateTime = TimeZoneInfo.ConvertTimeToUtc(localDateTime, TimeZoneInfo.Local);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (!TryWriteTickFromDateTime(ctx, tickUtcAddress, utcDateTime))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "M1TvFst-jrM",
|
||||
ExportName = "sceRtcConvertUtcToLocalTime",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcConvertUtcToLocalTime(CpuContext ctx)
|
||||
{
|
||||
var tickUtcAddress = ctx[CpuRegister.Rdi];
|
||||
var tickLocalAddress = ctx[CpuRegister.Rsi];
|
||||
if (tickUtcAddress == 0 || tickLocalAddress == 0)
|
||||
{
|
||||
return unchecked((int)0x80B50002);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt64(tickUtcAddress, out var tickUtc))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (!TryConvertTickToDateTime(tickUtc, out var utcDateTime))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
DateTime localDateTime;
|
||||
try
|
||||
{
|
||||
localDateTime = TimeZoneInfo.ConvertTimeFromUtc(utcDateTime, TimeZoneInfo.Local);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (!TryWriteTickFromDateTime(ctx, tickLocalAddress, localDateTime))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "8SljQx6pDP8",
|
||||
ExportName = "sceRtcEnd",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcEnd(CpuContext ctx)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "LN3Zcb72Q0c",
|
||||
ExportName = "sceRtcGetCurrentAdNetworkTick",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcGetCurrentAdNetworkTick(CpuContext ctx) => RtcGetCurrentTick(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "8lfvnRMqwEM",
|
||||
ExportName = "sceRtcGetCurrentClock",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcGetCurrentClock(CpuContext ctx)
|
||||
{
|
||||
var timeAddress = ctx[CpuRegister.Rdi];
|
||||
if (timeAddress == 0)
|
||||
{
|
||||
return unchecked((int)0x80B50002);
|
||||
}
|
||||
|
||||
var timeZoneMinutes = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
DateTimeOffset currentTime;
|
||||
try
|
||||
{
|
||||
currentTime = DateTimeOffset.UtcNow.ToOffset(TimeSpan.FromMinutes(timeZoneMinutes));
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (!TryWriteRtcDateTime(ctx, timeAddress, ToRtcDateTime(currentTime.DateTime)))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "ZPD1YOKI+Kw",
|
||||
@@ -20,30 +203,38 @@ public static class RtcExports
|
||||
var timeAddress = ctx[CpuRegister.Rdi];
|
||||
if (timeAddress == 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
return unchecked((int)0x80B50002);
|
||||
}
|
||||
|
||||
var now = DateTimeOffset.Now;
|
||||
Span<byte> rtcDateTime = stackalloc byte[16];
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[0..2], checked((ushort)now.Year));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[2..4], checked((ushort)now.Month));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[4..6], checked((ushort)now.Day));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[6..8], checked((ushort)now.Hour));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[8..10], checked((ushort)now.Minute));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[10..12], checked((ushort)now.Second));
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(
|
||||
rtcDateTime[12..16],
|
||||
checked((uint)((now.Ticks % TimeSpan.TicksPerSecond) / 10)));
|
||||
|
||||
if (!ctx.Memory.TryWrite(timeAddress, rtcDateTime))
|
||||
if (!TryWriteRtcDateTime(ctx, timeAddress, ToRtcDateTime(DateTimeOffset.Now.DateTime)))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Ot1DE3gif84",
|
||||
ExportName = "sceRtcGetCurrentDebugNetworkTick",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcGetCurrentDebugNetworkTick(CpuContext ctx) => RtcGetCurrentTick(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "zO9UL3qIINQ",
|
||||
ExportName = "sceRtcGetCurrentNetworkTick",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcGetCurrentNetworkTick(CpuContext ctx) => RtcGetCurrentTick(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "HWxHOdbM-Pg",
|
||||
ExportName = "sceRtcGetCurrentRawNetworkTick",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcGetCurrentRawNetworkTick(CpuContext ctx) => RtcGetCurrentTick(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "18B2NS1y9UU",
|
||||
ExportName = "sceRtcGetCurrentTick",
|
||||
@@ -54,7 +245,7 @@ public static class RtcExports
|
||||
var tickAddress = ctx[CpuRegister.Rdi];
|
||||
if (tickAddress == 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
return unchecked((int)0x80B50002);
|
||||
}
|
||||
|
||||
var tickValue = unchecked((ulong)(DateTime.UtcNow.Ticks / DateTimeTicksPerMicrosecond));
|
||||
@@ -63,7 +254,87 @@ public static class RtcExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "CyIK-i4XdgQ",
|
||||
ExportName = "sceRtcGetDayOfWeek",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcGetDayOfWeek(CpuContext ctx)
|
||||
{
|
||||
var year = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var month = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
var day = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||
if (!IsValidCalendarDate(year, month, day))
|
||||
{
|
||||
return unchecked((int)0x80B50004);
|
||||
}
|
||||
|
||||
return (int)new DateTime(year, month, day).DayOfWeek;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "3O7Ln8AqJ1o",
|
||||
ExportName = "sceRtcGetDaysInMonth",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcGetDaysInMonth(CpuContext ctx)
|
||||
{
|
||||
var year = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var month = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
if (year < 1 || year > 9999)
|
||||
{
|
||||
return unchecked((int)0x80B50008);
|
||||
}
|
||||
|
||||
if (month < 1 || month > 12)
|
||||
{
|
||||
return unchecked((int)0x80B50009);
|
||||
}
|
||||
|
||||
return DateTime.DaysInMonth(year, month);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "E7AR4o7Ny7E",
|
||||
ExportName = "sceRtcGetDosTime",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcGetDosTime(CpuContext ctx)
|
||||
{
|
||||
var timeAddress = ctx[CpuRegister.Rdi];
|
||||
var dosTimeAddress = ctx[CpuRegister.Rsi];
|
||||
if (timeAddress == 0 || dosTimeAddress == 0)
|
||||
{
|
||||
return unchecked((int)0x80B50002);
|
||||
}
|
||||
|
||||
if (!TryReadRtcDateTime(ctx, timeAddress, out var time))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
var validationResult = ValidateRtcDateTime(time);
|
||||
if (validationResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
uint dosTime = 0;
|
||||
dosTime |= (uint)((time.Second / 2) & 0x1F);
|
||||
dosTime |= (uint)(time.Minute & 0x3F) << 5;
|
||||
dosTime |= (uint)(time.Hour & 0x1F) << 11;
|
||||
dosTime |= (uint)(time.Day & 0x1F) << 16;
|
||||
dosTime |= (uint)(time.Month & 0x0F) << 21;
|
||||
dosTime |= (uint)((time.Year - 1980) & 0x7F) << 25;
|
||||
|
||||
if (!ctx.TryWriteUInt32(dosTimeAddress, dosTime))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -74,45 +345,220 @@ public static class RtcExports
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcGetTick(CpuContext ctx)
|
||||
{
|
||||
var dateTimeAddress = ctx[CpuRegister.Rdi];
|
||||
var timeAddress = ctx[CpuRegister.Rdi];
|
||||
var tickAddress = ctx[CpuRegister.Rsi];
|
||||
if (dateTimeAddress == 0 || tickAddress == 0)
|
||||
if (timeAddress == 0 || tickAddress == 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
return unchecked((int)0x80B50002);
|
||||
}
|
||||
|
||||
if (!TryReadRtcDateTime(ctx, dateTimeAddress, out var rtcDateTime))
|
||||
if (!TryReadRtcDateTime(ctx, timeAddress, out var time))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ulong tickValue;
|
||||
try
|
||||
var validationResult = ValidateRtcDateTime(time);
|
||||
if (validationResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
var baseDateTime = new DateTime(
|
||||
rtcDateTime.Year,
|
||||
rtcDateTime.Month,
|
||||
rtcDateTime.Day,
|
||||
rtcDateTime.Hour,
|
||||
rtcDateTime.Minute,
|
||||
rtcDateTime.Second,
|
||||
DateTimeKind.Utc);
|
||||
tickValue = checked((ulong)((baseDateTime.Ticks / DateTimeTicksPerMicrosecond) + rtcDateTime.Microsecond));
|
||||
return validationResult;
|
||||
}
|
||||
catch (Exception ex) when (ex is ArgumentOutOfRangeException or OverflowException)
|
||||
|
||||
if (!TryConvertRtcDateTimeToTick(time, out var tick))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt64(tickAddress, tickValue))
|
||||
if (!ctx.TryWriteUInt64(tickAddress, tick))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "jMNwqYr4R-k",
|
||||
ExportName = "sceRtcGetTickResolution",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcGetTickResolution(CpuContext ctx)
|
||||
{
|
||||
return (int)MicrosecondsPerSecond;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "BtqmpTRXHgk",
|
||||
ExportName = "sceRtcGetTime_t",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcGetTimeT(CpuContext ctx)
|
||||
{
|
||||
var timeAddress = ctx[CpuRegister.Rdi];
|
||||
var timeTAddress = ctx[CpuRegister.Rsi];
|
||||
if (timeAddress == 0 || timeTAddress == 0)
|
||||
{
|
||||
return unchecked((int)0x80B50002);
|
||||
}
|
||||
|
||||
if (!TryReadRtcDateTime(ctx, timeAddress, out var time))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
var validationResult = ValidateRtcDateTime(time);
|
||||
if (validationResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
if (!TryConvertRtcDateTimeToTick(time, out var tick))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var unixSeconds = tick < UnixEpochTicks ? 0UL : (tick - UnixEpochTicks) / MicrosecondsPerSecond;
|
||||
if (!ctx.TryWriteUInt64(timeTAddress, unixSeconds))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "jfRO0uTjtzA",
|
||||
ExportName = "sceRtcGetWin32FileTime",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcGetWin32FileTime(CpuContext ctx)
|
||||
{
|
||||
var timeAddress = ctx[CpuRegister.Rdi];
|
||||
var fileTimeAddress = ctx[CpuRegister.Rsi];
|
||||
if (timeAddress == 0 || fileTimeAddress == 0)
|
||||
{
|
||||
return unchecked((int)0x80B50002);
|
||||
}
|
||||
|
||||
if (!TryReadRtcDateTime(ctx, timeAddress, out var time))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
var validationResult = ValidateRtcDateTime(time);
|
||||
if (validationResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
if (!TryConvertRtcDateTimeToTick(time, out var tick))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var win32Time = tick < Win32FileTimeEpochTicks ? 0UL : (tick - Win32FileTimeEpochTicks) * 10UL;
|
||||
if (!ctx.TryWriteUInt64(fileTimeAddress, win32Time))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "LlodCMDbk3o",
|
||||
ExportName = "sceRtcInit",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcInit(CpuContext ctx)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Ug8pCwQvh0c",
|
||||
ExportName = "sceRtcIsLeapYear",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcIsLeapYear(CpuContext ctx)
|
||||
{
|
||||
var year = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (year < 1 || year > 9999)
|
||||
{
|
||||
return unchecked((int)0x80B50008);
|
||||
}
|
||||
|
||||
return DateTime.IsLeapYear(year) ? 1 : 0;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "NR1J0N7L2xY",
|
||||
ExportName = "sceRtcTickAddDays",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcTickAddDays(CpuContext ctx) => AddTickDelta(ctx, MicrosecondsPerDay);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "MDc5cd8HfCA",
|
||||
ExportName = "sceRtcTickAddHours",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcTickAddHours(CpuContext ctx) => AddTickDelta(ctx, MicrosecondsPerHour);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "XPIiw58C+GM",
|
||||
ExportName = "sceRtcTickAddMicroseconds",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcTickAddMicroseconds(CpuContext ctx) => AddTickDelta(ctx, 1UL);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "mn-tf4QiFzk",
|
||||
ExportName = "sceRtcTickAddMinutes",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcTickAddMinutes(CpuContext ctx) => AddTickDelta(ctx, MicrosecondsPerMinute);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "CL6y9q-XbuQ",
|
||||
ExportName = "sceRtcTickAddMonths",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcTickAddMonths(CpuContext ctx)
|
||||
{
|
||||
return AddCalendarDelta(ctx, dateTime => dateTime.AddMonths(unchecked((int)ctx[CpuRegister.Rdx])));
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "07O525HgICs",
|
||||
ExportName = "sceRtcTickAddSeconds",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcTickAddSeconds(CpuContext ctx) => AddTickDelta(ctx, MicrosecondsPerSecond);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "AqVMssr52Rc",
|
||||
ExportName = "sceRtcTickAddTicks",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcTickAddTicks(CpuContext ctx) => AddTickDelta(ctx, 1UL);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "gI4t194c2W8",
|
||||
ExportName = "sceRtcTickAddWeeks",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcTickAddWeeks(CpuContext ctx) => AddTickDelta(ctx, MicrosecondsPerWeek);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "-5y2uJ62qS8",
|
||||
ExportName = "sceRtcTickAddYears",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcTickAddYears(CpuContext ctx)
|
||||
{
|
||||
return AddCalendarDelta(ctx, dateTime => dateTime.AddYears(unchecked((int)ctx[CpuRegister.Rdx])));
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "ueega6v3GUw",
|
||||
ExportName = "sceRtcSetTick",
|
||||
@@ -120,11 +566,11 @@ public static class RtcExports
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcSetTick(CpuContext ctx)
|
||||
{
|
||||
var dateTimeAddress = ctx[CpuRegister.Rdi];
|
||||
var timeAddress = ctx[CpuRegister.Rdi];
|
||||
var tickAddress = ctx[CpuRegister.Rsi];
|
||||
if (dateTimeAddress == 0 || tickAddress == 0)
|
||||
if (timeAddress == 0 || tickAddress == 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
return unchecked((int)0x80B50002);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt64(tickAddress, out var tickValue))
|
||||
@@ -132,40 +578,249 @@ public static class RtcExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (tickValue > long.MaxValue / DateTimeTicksPerMicrosecond)
|
||||
if (!TryConvertTickToRtcDateTime(tickValue, out var time))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
return unchecked((int)0x80B50004);
|
||||
}
|
||||
|
||||
DateTime dateTime;
|
||||
try
|
||||
{
|
||||
dateTime = new DateTime(checked((long)tickValue * DateTimeTicksPerMicrosecond), DateTimeKind.Utc);
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (!TryWriteRtcDateTime(ctx, dateTimeAddress, dateTime))
|
||||
if (!TryWriteRtcDateTime(ctx, timeAddress, time))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static bool TryReadRtcDateTime(CpuContext ctx, ulong address, out RtcDateTime rtcDateTime)
|
||||
[SysAbiExport(
|
||||
Nid = "aYPCd1cChyg",
|
||||
ExportName = "sceRtcSetDosTime",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcSetDosTime(CpuContext ctx)
|
||||
{
|
||||
var timeAddress = ctx[CpuRegister.Rdi];
|
||||
if (timeAddress == 0)
|
||||
{
|
||||
return unchecked((int)0x80B50002);
|
||||
}
|
||||
|
||||
var dosTime = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||
var time = new RtcDateTime(
|
||||
(ushort)(1980 + ((dosTime >> 25) & 0x7F)),
|
||||
(ushort)((dosTime >> 21) & 0x0F),
|
||||
(ushort)((dosTime >> 16) & 0x1F),
|
||||
(ushort)((dosTime >> 11) & 0x1F),
|
||||
(ushort)((dosTime >> 5) & 0x3F),
|
||||
(ushort)((dosTime << 1) & 0x3E),
|
||||
0);
|
||||
|
||||
if (!TryWriteRtcDateTime(ctx, timeAddress, time))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "bDEVVP4bTjQ",
|
||||
ExportName = "sceRtcSetTime_t",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcSetTimeT(CpuContext ctx)
|
||||
{
|
||||
var timeAddress = ctx[CpuRegister.Rdi];
|
||||
if (timeAddress == 0)
|
||||
{
|
||||
return unchecked((int)0x80B50002);
|
||||
}
|
||||
|
||||
var timeSeconds = unchecked((long)ctx[CpuRegister.Rsi]);
|
||||
if (timeSeconds < 0)
|
||||
{
|
||||
return unchecked((int)0x80B50003);
|
||||
}
|
||||
|
||||
var tick = UnixEpochTicks + unchecked((ulong)timeSeconds) * MicrosecondsPerSecond;
|
||||
if (!TryConvertTickToRtcDateTime(tick, out var time))
|
||||
{
|
||||
return unchecked((int)0x80B50003);
|
||||
}
|
||||
|
||||
if (!TryWriteRtcDateTime(ctx, timeAddress, time))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "n5JiAJXsbcs",
|
||||
ExportName = "sceRtcSetWin32FileTime",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcSetWin32FileTime(CpuContext ctx)
|
||||
{
|
||||
var timeAddress = ctx[CpuRegister.Rdi];
|
||||
if (timeAddress == 0)
|
||||
{
|
||||
return unchecked((int)0x80B50002);
|
||||
}
|
||||
|
||||
var fileTime = unchecked((long)ctx[CpuRegister.Rsi]);
|
||||
if (fileTime < 0)
|
||||
{
|
||||
return unchecked((int)0x80B50003);
|
||||
}
|
||||
|
||||
var tick = Win32FileTimeEpochTicks + unchecked((ulong)fileTime / 10UL);
|
||||
if (!TryConvertTickToRtcDateTime(tick, out var time))
|
||||
{
|
||||
return unchecked((int)0x80B50003);
|
||||
}
|
||||
|
||||
if (!TryWriteRtcDateTime(ctx, timeAddress, time))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static int AddTickDelta(CpuContext ctx, ulong microsecondsPerUnit)
|
||||
{
|
||||
var destinationAddress = ctx[CpuRegister.Rdi];
|
||||
var sourceAddress = ctx[CpuRegister.Rsi];
|
||||
var delta = unchecked((long)ctx[CpuRegister.Rdx]);
|
||||
if (destinationAddress == 0 || sourceAddress == 0)
|
||||
{
|
||||
return unchecked((int)0x80B50002);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt64(sourceAddress, out var sourceTick))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var resultTick = checked((long)sourceTick + checked(delta * (long)microsecondsPerUnit));
|
||||
if (resultTick < 0)
|
||||
{
|
||||
return unchecked((int)0x80B50003);
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt64(destinationAddress, unchecked((ulong)resultTick)))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
}
|
||||
catch (OverflowException)
|
||||
{
|
||||
return unchecked((int)0x80B50003);
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static int AddCalendarDelta(CpuContext ctx, Func<DateTime, DateTime> transform)
|
||||
{
|
||||
var destinationAddress = ctx[CpuRegister.Rdi];
|
||||
var sourceAddress = ctx[CpuRegister.Rsi];
|
||||
if (destinationAddress == 0 || sourceAddress == 0)
|
||||
{
|
||||
return unchecked((int)0x80B50002);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt64(sourceAddress, out var sourceTick))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (!TryConvertTickToDateTime(sourceTick, out var sourceDateTime))
|
||||
{
|
||||
return unchecked((int)0x80B50003);
|
||||
}
|
||||
|
||||
DateTime resultDateTime;
|
||||
try
|
||||
{
|
||||
resultDateTime = transform(sourceDateTime);
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
return unchecked((int)0x80B50003);
|
||||
}
|
||||
|
||||
if (!TryWriteTickFromDateTime(ctx, destinationAddress, resultDateTime))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static int ValidateRtcDateTime(RtcDateTime time)
|
||||
{
|
||||
if (time.Year < 1 || time.Year > 9999)
|
||||
{
|
||||
return unchecked((int)0x80B50008);
|
||||
}
|
||||
|
||||
if (time.Month < 1 || time.Month > 12)
|
||||
{
|
||||
return unchecked((int)0x80B50009);
|
||||
}
|
||||
|
||||
if (time.Day < 1 || time.Day > DateTime.DaysInMonth(time.Year, time.Month))
|
||||
{
|
||||
return unchecked((int)0x80B5000A);
|
||||
}
|
||||
|
||||
if (time.Hour > 23)
|
||||
{
|
||||
return unchecked((int)0x80B5000B);
|
||||
}
|
||||
|
||||
if (time.Minute > 59)
|
||||
{
|
||||
return unchecked((int)0x80B5000C);
|
||||
}
|
||||
|
||||
if (time.Second > 59)
|
||||
{
|
||||
return unchecked((int)0x80B5000D);
|
||||
}
|
||||
|
||||
if (time.Microsecond > 999_999)
|
||||
{
|
||||
return unchecked((int)0x80B5000E);
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static bool IsValidCalendarDate(int year, int month, int day)
|
||||
{
|
||||
if (year < 1 || year > 9999 || month < 1 || month > 12)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return day >= 1 && day <= DateTime.DaysInMonth(year, month);
|
||||
}
|
||||
|
||||
private static bool TryReadRtcDateTime(CpuContext ctx, ulong address, out RtcDateTime time)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[16];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
rtcDateTime = default;
|
||||
time = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
rtcDateTime = new RtcDateTime(
|
||||
time = new RtcDateTime(
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(buffer[0..2]),
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(buffer[2..4]),
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(buffer[4..6]),
|
||||
@@ -176,19 +831,117 @@ public static class RtcExports
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteRtcDateTime(CpuContext ctx, ulong address, DateTime dateTime)
|
||||
private static bool TryWriteRtcDateTime(CpuContext ctx, ulong address, RtcDateTime dateTime)
|
||||
{
|
||||
Span<byte> rtcDateTime = stackalloc byte[16];
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[0..2], checked((ushort)dateTime.Year));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[2..4], checked((ushort)dateTime.Month));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[4..6], checked((ushort)dateTime.Day));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[6..8], checked((ushort)dateTime.Hour));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[8..10], checked((ushort)dateTime.Minute));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[10..12], checked((ushort)dateTime.Second));
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(
|
||||
rtcDateTime[12..16],
|
||||
checked((uint)((dateTime.Ticks % TimeSpan.TicksPerSecond) / DateTimeTicksPerMicrosecond)));
|
||||
return ctx.Memory.TryWrite(address, rtcDateTime);
|
||||
Span<byte> buffer = stackalloc byte[16];
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buffer[0..2], dateTime.Year);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buffer[2..4], dateTime.Month);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buffer[4..6], dateTime.Day);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buffer[6..8], dateTime.Hour);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buffer[8..10], dateTime.Minute);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buffer[10..12], dateTime.Second);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer[12..16], dateTime.Microsecond);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static RtcDateTime ToRtcDateTime(DateTime dateTime)
|
||||
{
|
||||
return new RtcDateTime(
|
||||
(ushort)dateTime.Year,
|
||||
(ushort)dateTime.Month,
|
||||
(ushort)dateTime.Day,
|
||||
(ushort)dateTime.Hour,
|
||||
(ushort)dateTime.Minute,
|
||||
(ushort)dateTime.Second,
|
||||
(uint)(dateTime.Ticks % TimeSpan.TicksPerSecond / DateTimeTicksPerMicrosecond));
|
||||
}
|
||||
|
||||
private static RtcDateTime ToRtcDateTime(DateTimeOffset dateTimeOffset)
|
||||
{
|
||||
return ToRtcDateTime(dateTimeOffset.DateTime);
|
||||
}
|
||||
|
||||
private static bool TryWriteTickFromDateTime(CpuContext ctx, ulong address, DateTime dateTime)
|
||||
{
|
||||
return ctx.TryWriteUInt64(address, unchecked((ulong)(dateTime.Ticks / DateTimeTicksPerMicrosecond)));
|
||||
}
|
||||
|
||||
private static bool TryConvertTickToDateTime(ulong tick, out DateTime dateTime)
|
||||
{
|
||||
var maxSupportedTick = unchecked((ulong)(DateTime.MaxValue.Ticks / DateTimeTicksPerMicrosecond));
|
||||
if (tick > maxSupportedTick)
|
||||
{
|
||||
dateTime = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
dateTime = new DateTime(checked((long)(tick * (ulong)DateTimeTicksPerMicrosecond)), DateTimeKind.Utc);
|
||||
return true;
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
dateTime = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryConvertRtcDateTimeToTick(RtcDateTime time, out ulong tick)
|
||||
{
|
||||
try
|
||||
{
|
||||
var year = time.Year;
|
||||
var month = time.Month;
|
||||
var day = time.Day;
|
||||
if (month > 2)
|
||||
{
|
||||
month -= 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
month += 9;
|
||||
year -= 1;
|
||||
}
|
||||
|
||||
var century = year / 100;
|
||||
var yearOfCentury = year - (100 * century);
|
||||
|
||||
ulong days = ((146097UL * (ulong)century) >> 2)
|
||||
+ ((1461UL * (ulong)yearOfCentury) >> 2)
|
||||
+ ((153UL * (ulong)month + 2UL) / 5UL)
|
||||
+ day;
|
||||
days -= 307UL;
|
||||
days *= MicrosecondsPerDay;
|
||||
|
||||
var timeOfDay = (ulong)time.Hour * MicrosecondsPerHour
|
||||
+ (ulong)time.Minute * MicrosecondsPerMinute
|
||||
+ (ulong)time.Second * MicrosecondsPerSecond
|
||||
+ time.Microsecond;
|
||||
|
||||
tick = days + timeOfDay;
|
||||
return true;
|
||||
}
|
||||
catch (OverflowException)
|
||||
{
|
||||
tick = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryConvertTickToRtcDateTime(ulong tick, out RtcDateTime time)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dateTime = new DateTime(checked((long)(tick * DateTimeTicksPerMicrosecond)), DateTimeKind.Utc);
|
||||
time = ToRtcDateTime(dateTime);
|
||||
return true;
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
time = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct RtcDateTime(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,59 @@ public static class VideoOutExports
|
||||
private static readonly object _stateGate = new();
|
||||
private static readonly object _frameDumpGate = new();
|
||||
private static readonly Dictionary<int, VideoOutPortState> _ports = new();
|
||||
|
||||
// Hardware raises vblank autonomously; UE blocks its frame loop on it.
|
||||
private static Timer? _vblankPumpTimer;
|
||||
private static int _vblankPumpActive;
|
||||
private const int VblankPumpIntervalMs = 16;
|
||||
|
||||
private static void EnsureVblankPumpStarted()
|
||||
{
|
||||
if (_vblankPumpTimer is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_vblankPumpTimer = new Timer(
|
||||
static _ => PumpVblanks(),
|
||||
null,
|
||||
VblankPumpIntervalMs,
|
||||
VblankPumpIntervalMs);
|
||||
}
|
||||
|
||||
private static void PumpVblanks()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _vblankPumpActive, 1) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
VideoOutPortState[] ports;
|
||||
lock (_stateGate)
|
||||
{
|
||||
if (_ports.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Signalling reaches WakeBlockedThreads -> Pump(), which serialises on one global
|
||||
// flag. Waking an unwatched queue would hold it 60x/sec and starve guest threads.
|
||||
ports = _ports.Values.Where(static port => port.VblankEvents.Count != 0).ToArray();
|
||||
}
|
||||
|
||||
foreach (var port in ports)
|
||||
{
|
||||
SignalVblank(port);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Volatile.Write(ref _vblankPumpActive, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Dictionary<(int Handle, int BufferIndex, ulong Address), ulong> _lastFrameFingerprints = new();
|
||||
private static int _nextHandle = 1;
|
||||
private static int _frameDumpCount;
|
||||
@@ -64,7 +117,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 +132,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}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,6 +230,7 @@ public static class VideoOutExports
|
||||
{
|
||||
Handle = handle,
|
||||
};
|
||||
EnsureVblankPumpStarted();
|
||||
return handle;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,6 +178,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
private static uint _windowHeight;
|
||||
private static bool _closed;
|
||||
private const string DebugUtilsExtensionName = "VK_EXT_debug_utils";
|
||||
private const uint NvidiaVendorId = 0x10DE;
|
||||
private static bool _splashHidden;
|
||||
private static long _enqueuedGuestWorkSequence;
|
||||
private static long _completedGuestWorkSequence;
|
||||
@@ -1502,6 +1503,13 @@ internal static unsafe class VulkanVideoPresenter
|
||||
Check(_vk.EnumeratePhysicalDevices(_instance, &deviceCount, devicePointer), "vkEnumeratePhysicalDevices");
|
||||
}
|
||||
|
||||
// Hybrid laptops enumerate the iGPU first, and AMD's integrated driver
|
||||
// segfaults compiling some translated shaders, so rank rather than take
|
||||
// the first hit. SHARPEMU_VK_DEVICE=<substring> pins an adapter by name.
|
||||
var deviceOverride = Environment.GetEnvironmentVariable("SHARPEMU_VK_DEVICE");
|
||||
var bestScore = int.MinValue;
|
||||
var found = false;
|
||||
|
||||
foreach (var device in devices)
|
||||
{
|
||||
uint queueCount = 0;
|
||||
@@ -1521,13 +1529,60 @@ internal static unsafe class VulkanVideoPresenter
|
||||
continue;
|
||||
}
|
||||
|
||||
_physicalDevice = device;
|
||||
_queueFamilyIndex = index;
|
||||
return;
|
||||
_vk.GetPhysicalDeviceProperties(device, out var properties);
|
||||
var name = SilkMarshal.PtrToString((nint)properties.DeviceName) ?? string.Empty;
|
||||
var score = ScorePhysicalDevice(properties, name, deviceOverride);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] Vulkan candidate: {name} ({properties.DeviceType}) score={score}");
|
||||
|
||||
if (score > bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
_physicalDevice = device;
|
||||
_queueFamilyIndex = index;
|
||||
found = true;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("No Vulkan graphics/present queue was found.");
|
||||
if (!found)
|
||||
{
|
||||
throw new InvalidOperationException("No Vulkan graphics/present queue was found.");
|
||||
}
|
||||
|
||||
_vk.GetPhysicalDeviceProperties(_physicalDevice, out var selected);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] Vulkan device: {SilkMarshal.PtrToString((nint)selected.DeviceName)} ({selected.DeviceType})");
|
||||
}
|
||||
|
||||
private static int ScorePhysicalDevice(
|
||||
PhysicalDeviceProperties properties,
|
||||
string name,
|
||||
string? deviceOverride)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(deviceOverride))
|
||||
{
|
||||
return name.Contains(deviceOverride, StringComparison.OrdinalIgnoreCase) ? 1000 : -1000;
|
||||
}
|
||||
|
||||
var score = properties.DeviceType switch
|
||||
{
|
||||
PhysicalDeviceType.DiscreteGpu => 300,
|
||||
PhysicalDeviceType.VirtualGpu => 100,
|
||||
PhysicalDeviceType.Cpu => 50,
|
||||
// Last resort: only picked when nothing else can present.
|
||||
PhysicalDeviceType.IntegratedGpu => -100,
|
||||
_ => 10,
|
||||
};
|
||||
|
||||
if (properties.VendorID == NvidiaVendorId)
|
||||
{
|
||||
score += 500;
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
private void CreateDevice()
|
||||
|
||||
@@ -98,6 +98,12 @@
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
},
|
||||
"sharpemu.hle": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"SharpEmu.Logging": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.logging": {
|
||||
"type": "Project"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user