mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-26 04:39:17 +08:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2060dacaf1 | |||
| f73c9e8c3f | |||
| 3d2c30b151 | |||
| 61d28e9e08 | |||
| 4bd42795c7 | |||
| c03ca32a02 | |||
| 6e2878f2ff | |||
| 2e09b015bc | |||
| 7b9efd1539 | |||
| fbf2c2d00a | |||
| 8c1507777c | |||
| 8ebc43e758 | |||
| 5aadb7495a | |||
| cdee77521e | |||
| d2fca37716 | |||
| 5bee81df70 | |||
| d4bb282c49 | |||
| e1cf5b13ef | |||
| de4fc1e1a8 |
@@ -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 |
@@ -39,3 +39,4 @@ Desktop.ini
|
||||
ehthumbs.db
|
||||
|
||||
.vs/
|
||||
.idea/
|
||||
|
||||
@@ -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
|
||||
|
||||
+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,
|
||||
|
||||
@@ -216,7 +216,10 @@
|
||||
}
|
||||
},
|
||||
"sharpemu.hle": {
|
||||
"type": "Project"
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"SharpEmu.Logging": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.libs": {
|
||||
"type": "Project",
|
||||
|
||||
@@ -38,6 +38,71 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
}
|
||||
|
||||
private void RecordDeferredBootstrapTrace(
|
||||
long dispatchIndex,
|
||||
ulong op,
|
||||
ulong symbolPointer,
|
||||
ulong outputPointer,
|
||||
ulong returnRip)
|
||||
{
|
||||
lock (_deferredBootstrapTraceGate)
|
||||
{
|
||||
_deferredBootstrapTrace[_deferredBootstrapTraceWriteIndex] = new DeferredBootstrapTraceEntry(
|
||||
dispatchIndex,
|
||||
op,
|
||||
symbolPointer,
|
||||
outputPointer,
|
||||
returnRip);
|
||||
_deferredBootstrapTraceWriteIndex =
|
||||
(_deferredBootstrapTraceWriteIndex + 1) % _deferredBootstrapTrace.Length;
|
||||
if (_deferredBootstrapTraceCount < _deferredBootstrapTrace.Length)
|
||||
{
|
||||
_deferredBootstrapTraceCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrainDeferredBootstrapTraces()
|
||||
{
|
||||
if (!_logBootstrap)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DeferredBootstrapTraceEntry[] pending;
|
||||
lock (_deferredBootstrapTraceGate)
|
||||
{
|
||||
if (_deferredBootstrapTraceCount == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
pending = new DeferredBootstrapTraceEntry[_deferredBootstrapTraceCount];
|
||||
var readIndex = (_deferredBootstrapTraceWriteIndex - _deferredBootstrapTraceCount +
|
||||
_deferredBootstrapTrace.Length) % _deferredBootstrapTrace.Length;
|
||||
for (var i = 0; i < _deferredBootstrapTraceCount; i++)
|
||||
{
|
||||
pending[i] = _deferredBootstrapTrace[(readIndex + i) % _deferredBootstrapTrace.Length];
|
||||
}
|
||||
|
||||
_deferredBootstrapTraceCount = 0;
|
||||
}
|
||||
|
||||
foreach (var entry in pending)
|
||||
{
|
||||
var symbolText = "<unreadable>";
|
||||
if (TryReadAsciiZ(entry.SymbolPointer, 256, out var sym))
|
||||
{
|
||||
symbolText = sym;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] bootstrap_call#{entry.DispatchIndex}: op=0x{entry.Op:X16} " +
|
||||
$"sym_ptr=0x{entry.SymbolPointer:X16} sym='{symbolText}' " +
|
||||
$"out_ptr=0x{entry.OutputPointer:X16} ret=0x{entry.ReturnRip:X16}");
|
||||
}
|
||||
}
|
||||
|
||||
private void DumpRecentImportTrace()
|
||||
{
|
||||
if (_recentImportTraceCount == 0)
|
||||
@@ -170,14 +235,15 @@ public sealed partial class DirectExecutionBackend
|
||||
ulong callRip = returnRip + (ulong)i;
|
||||
ulong target = unchecked((ulong)((long)(callRip + 5) + rel32));
|
||||
Log.Debug($"Import#{dispatchIndex} near-call @{callRip:X16}: target=0x{target:X16}");
|
||||
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
|
||||
var importEntries = _importEntries;
|
||||
for (int importIndex = 0; importIndex < importEntries.Length; importIndex++)
|
||||
{
|
||||
if (_importEntries[importIndex].Address != target)
|
||||
if (importEntries[importIndex].Address != target)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string nid = _importEntries[importIndex].Nid;
|
||||
string nid = importEntries[importIndex].Nid;
|
||||
if (_moduleManager.TryGetExport(nid, out var export))
|
||||
{
|
||||
Log.Debug(
|
||||
@@ -204,14 +270,14 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
Log.Debug(
|
||||
$"Import#{dispatchIndex} near-call PLT slot: [0x{slot:X16}] = 0x{slotTarget:X16}");
|
||||
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
|
||||
for (int importIndex = 0; importIndex < importEntries.Length; importIndex++)
|
||||
{
|
||||
if (_importEntries[importIndex].Address != slotTarget)
|
||||
if (importEntries[importIndex].Address != slotTarget)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string nid = _importEntries[importIndex].Nid;
|
||||
string nid = importEntries[importIndex].Nid;
|
||||
if (_moduleManager.TryGetExport(nid, out var export))
|
||||
{
|
||||
Log.Debug(
|
||||
|
||||
@@ -103,6 +103,15 @@ public sealed partial class DirectExecutionBackend
|
||||
ulong rip = ReadCtxU64(contextRecord, 248);
|
||||
ulong rsp = ReadCtxU64(contextRecord, 152);
|
||||
|
||||
// Thread-mode probe: a hardware exception raised while this thread is inside
|
||||
// the managed import gateway means the VEH->managed reentry happened from
|
||||
// cooperative GC mode — a ReversePInvokeBadTransition candidate.
|
||||
if (LogThreadMode && _threadModeGatewayDepth > 0)
|
||||
{
|
||||
TraceThreadMode(
|
||||
$"veh_in_gateway code=0x{exceptionCode:X8} rip=0x{rip:X16} gateway_depth={_threadModeGatewayDepth}");
|
||||
}
|
||||
|
||||
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
|
||||
{
|
||||
return -1;
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using SharpEmu.Core.Cpu;
|
||||
using SharpEmu.Core.Loader;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
@@ -19,6 +20,16 @@ public sealed partial class DirectExecutionBackend
|
||||
|
||||
private static ulong ImportDispatchGatewayManaged(nint backendHandle, int importIndex, nint argPackPtr)
|
||||
{
|
||||
if (LogThreadMode)
|
||||
{
|
||||
_threadModeGatewayCalls++;
|
||||
_threadModeGatewayDepth++;
|
||||
if (!_threadModeGatewayFirstLogged)
|
||||
{
|
||||
_threadModeGatewayFirstLogged = true;
|
||||
TraceThreadMode($"gateway_first import={importIndex} total={_threadModeGatewayCalls}");
|
||||
}
|
||||
}
|
||||
try
|
||||
{
|
||||
if (!(GCHandle.FromIntPtr(backendHandle).Target is DirectExecutionBackend directExecutionBackend))
|
||||
@@ -36,6 +47,13 @@ public sealed partial class DirectExecutionBackend
|
||||
$"[LOADER][ERROR] ImportDispatchGatewayManaged exception: {ex.GetType().Name}: {ex.Message}");
|
||||
return 18446744071562199298uL;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (LogThreadMode)
|
||||
{
|
||||
_threadModeGatewayDepth--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe static int RawVectoredHandlerManaged(void* exceptionInfo)
|
||||
@@ -69,6 +87,12 @@ public sealed partial class DirectExecutionBackend
|
||||
WriteCtxU64(contextRecord, 152, nextRsp);
|
||||
WriteCtxU64(contextRecord, 248, returnRip);
|
||||
Interlocked.Increment(ref _rawSentinelRecoveries);
|
||||
if (LogThreadMode)
|
||||
{
|
||||
TraceThreadMode(
|
||||
$"sentinel_recover rip=0x{value:X16} -> 0x{returnRip:X16} rsp=0x{rsp:X16} -> 0x{nextRsp:X16} " +
|
||||
$"gateway_depth={_threadModeGatewayDepth}");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
@@ -87,12 +111,13 @@ public sealed partial class DirectExecutionBackend
|
||||
LastError = "Import dispatch called without active CPU context";
|
||||
return 18446744071562199298uL;
|
||||
}
|
||||
if ((uint)importIndex >= (uint)_importEntries.Length)
|
||||
var importEntries = _importEntries;
|
||||
if ((uint)importIndex >= (uint)importEntries.Length)
|
||||
{
|
||||
LastError = $"Import dispatch index out of range: {importIndex}";
|
||||
return 18446744071562199042uL;
|
||||
}
|
||||
ImportStubEntry importStubEntry = _importEntries[importIndex];
|
||||
ImportStubEntry importStubEntry = importEntries[importIndex];
|
||||
int num2 = Volatile.Read(in _rawSentinelRecoveries);
|
||||
if (num2 != _lastReportedRawSentinelRecoveries)
|
||||
{
|
||||
@@ -136,6 +161,14 @@ public sealed partial class DirectExecutionBackend
|
||||
*(ulong*)(xmmSlot + 8));
|
||||
}
|
||||
cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL;
|
||||
if (string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
|
||||
{
|
||||
NormalizeKernelDynlibDlsymArguments(cpuContext, out _, out _);
|
||||
*(ulong*)argPackPtr = cpuContext[CpuRegister.Rdi];
|
||||
*(ulong*)(argPackPtr + 8) = cpuContext[CpuRegister.Rsi];
|
||||
*(ulong*)(argPackPtr + 16) = cpuContext[CpuRegister.Rdx];
|
||||
}
|
||||
|
||||
ulong value = cpuContext[CpuRegister.Rdi];
|
||||
ulong value2 = cpuContext[CpuRegister.Rsi];
|
||||
ulong num3 = cpuContext[CpuRegister.Rdx];
|
||||
@@ -186,16 +219,6 @@ public sealed partial class DirectExecutionBackend
|
||||
TraceGuestContext(
|
||||
$"import dispatch={num} nid={importStubEntry.Nid} ret=0x{num7:X16} managed={Environment.CurrentManagedThreadId} guest=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} fiber=0x{GuestThreadExecution.CurrentFiberAddress:X16} active={HasActiveExecutionThread}");
|
||||
}
|
||||
if (_logBootstrap && string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
|
||||
{
|
||||
string symbolText = "<unreadable>";
|
||||
if (TryReadAsciiZ(value2, 256, out var sym))
|
||||
{
|
||||
symbolText = sym;
|
||||
}
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] bootstrap_call#{num}: op=0x{value:X16} sym_ptr=0x{value2:X16} sym='{symbolText}' out_ptr=0x{num3:X16} ret=0x{num7:X16}");
|
||||
}
|
||||
if (!isGuestWorker &&
|
||||
!ActiveForcedGuestExit &&
|
||||
ShouldForceGuestExitOnImportLoop(importStubEntry.Nid, num7, num, value, value2) &&
|
||||
@@ -204,6 +227,17 @@ public sealed partial class DirectExecutionBackend
|
||||
cpuContext[CpuRegister.Rax] = 1uL;
|
||||
return 1uL;
|
||||
}
|
||||
// Backend teardown in progress: unwind this worker to the host instead of
|
||||
// dispatching. RunGuestThread parks it as Blocked with no wake handler, its
|
||||
// host thread exits, and RequestGuestThreadTeardown can finish before any
|
||||
// executable memory is freed.
|
||||
if (isGuestWorker &&
|
||||
_guestTeardownRequested &&
|
||||
TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, "backend teardown"))
|
||||
{
|
||||
cpuContext[CpuRegister.Rax] = 0uL;
|
||||
return 0uL;
|
||||
}
|
||||
bool flag0 = ShouldSuppressStrlenTrace(importStubEntry.Nid);
|
||||
bool flag = num7 >= 2156221920u && num7 <= 2156225024u;
|
||||
bool flag2 = num7 >= 2156351360u && num7 <= 2156352080u;
|
||||
@@ -349,6 +383,11 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
if (string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
|
||||
{
|
||||
if (_logBootstrap)
|
||||
{
|
||||
RecordDeferredBootstrapTrace(num, value, value2, num3, num7);
|
||||
}
|
||||
|
||||
orbisGen2Result = DispatchBootstrapBridge();
|
||||
}
|
||||
else if (string.Equals(importStubEntry.Nid, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal) ||
|
||||
@@ -511,6 +550,7 @@ public sealed partial class DirectExecutionBackend
|
||||
cpuContext.GetXmmRegister(0, out var returnXmm0Low, out var returnXmm0High);
|
||||
*(ulong*)(argPackPtr - 0x80) = returnXmm0Low;
|
||||
*(ulong*)(argPackPtr - 0x80 + 8) = returnXmm0High;
|
||||
DrainDeferredBootstrapTraces();
|
||||
return cpuContext[CpuRegister.Rax];
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -519,6 +559,7 @@ public sealed partial class DirectExecutionBackend
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] {LastError}");
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] {ex.StackTrace}");
|
||||
cpuContext[CpuRegister.Rax] = 18446744071562199298uL;
|
||||
DrainDeferredBootstrapTraces();
|
||||
return 18446744071562199298uL;
|
||||
}
|
||||
}
|
||||
@@ -671,6 +712,7 @@ public sealed partial class DirectExecutionBackend
|
||||
"eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
|
||||
"qvMUCyyaCSI" or // sceKernelAprSubmitCommandBufferAndGetId
|
||||
"Q2V+iqvjgC0" or // vsnprintf
|
||||
"AV6ipCNa4Rw" or // strcasecmp
|
||||
"q1cHNfGycLI" or // scePadRead
|
||||
"xk0AcarP3V4" or // scePadOpen
|
||||
"yH17Q6NWtVg" or // sceUserServiceGetEvent
|
||||
@@ -835,6 +877,7 @@ public sealed partial class DirectExecutionBackend
|
||||
"WkkeywLJcgU" or // wcslen
|
||||
"Ovb2dSJOAuE" or // strcmp
|
||||
"aesyjrHVWy4" or // strncmp
|
||||
"AV6ipCNa4Rw" or // strcasecmp
|
||||
"pNtJdE3x49E" or // wcscmp
|
||||
"fV2xHER+bKE" or // wcscoll
|
||||
"E8wCoUEbfzk" or // wcsncmp
|
||||
@@ -860,7 +903,12 @@ public sealed partial class DirectExecutionBackend
|
||||
"vz+pg2zdopI" or // sceKernelGetEventUserData
|
||||
"mJ7aghmgvfc" or // sceKernelGetEventId
|
||||
"23CPPI1tyBY" or // sceKernelGetEventFilter
|
||||
"kwGyyjohI50"; // sceKernelGetEventData
|
||||
"kwGyyjohI50" or // sceKernelGetEventData
|
||||
// _Getpctype is a per-character ctype table lookup; the embedded mcpp preprocessor
|
||||
// re-fetches the table pointer on every isdigit()/isalpha() check, so classifying a
|
||||
// large input legitimately calls it hundreds of thousands of times back-to-back -
|
||||
// real, finite work that would otherwise trip the loop guard.
|
||||
"sUP1hBaouOw"; // _Getpctype
|
||||
}
|
||||
|
||||
private long NextImportDispatchIndex()
|
||||
@@ -1270,30 +1318,485 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
ulong symbolNameAddress = cpuContext[CpuRegister.Rsi];
|
||||
ulong outputAddress = cpuContext[CpuRegister.Rdx];
|
||||
if (!TryReadAsciiZ(symbolNameAddress, 512, out var symbolName))
|
||||
|
||||
NormalizeKernelDynlibDlsymArguments(cpuContext, out var symbolNameAddress, out var outputAddress);
|
||||
try
|
||||
{
|
||||
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
|
||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
if (!TryReadAsciiZ(symbolNameAddress, 512, out var symbolName) ||
|
||||
!TryResolveDlsymGuestAddress(symbolName, out var resolvedAddress) ||
|
||||
outputAddress == 0 ||
|
||||
!TryWriteUInt64Compat(outputAddress, resolvedAddress))
|
||||
{
|
||||
return CompleteKernelDynlibDlsymFailure(cpuContext, outputAddress);
|
||||
}
|
||||
}
|
||||
if (!TryResolveRuntimeSymbolAddress(symbolName, out var resolvedAddress) &&
|
||||
!TryResolveRuntimeSymbolAlias(symbolName, out resolvedAddress))
|
||||
catch
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] sceKernelDlsym failed: handle=0x{cpuContext[CpuRegister.Rdi]:X} symbol='{symbolName}'");
|
||||
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
|
||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
if (outputAddress == 0L || !TryWriteUInt64Compat(outputAddress, resolvedAddress))
|
||||
{
|
||||
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
|
||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
return CompleteKernelDynlibDlsymFailure(cpuContext, outputAddress);
|
||||
}
|
||||
|
||||
cpuContext[CpuRegister.Rax] = 0uL;
|
||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static void NormalizeKernelDynlibDlsymArguments(
|
||||
CpuContext cpuContext,
|
||||
out ulong symbolNameAddress,
|
||||
out ulong outputAddress)
|
||||
{
|
||||
var handle = cpuContext[CpuRegister.Rdi];
|
||||
symbolNameAddress = cpuContext[CpuRegister.Rsi];
|
||||
outputAddress = cpuContext[CpuRegister.Rdx];
|
||||
|
||||
// Standalone bootstrap loaders sometimes call through the bridge with
|
||||
// (symbol_ptr, handle, out) while sceKernelDlsym is (handle, symbol_ptr, out).
|
||||
// Heuristic only: valid when RSI looks like a small handle and RDI is a guest pointer.
|
||||
if (symbolNameAddress < 0x10000 &&
|
||||
IsPlausibleDynlibSymbolPointer(handle))
|
||||
{
|
||||
symbolNameAddress = handle;
|
||||
handle = cpuContext[CpuRegister.Rsi];
|
||||
cpuContext[CpuRegister.Rdi] = handle;
|
||||
cpuContext[CpuRegister.Rsi] = symbolNameAddress;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsPlausibleDynlibSymbolPointer(ulong address)
|
||||
{
|
||||
return address >= 0x10000 && address < 0x0000_8000_0000_0000UL;
|
||||
}
|
||||
|
||||
private OrbisGen2Result CompleteKernelDynlibDlsymFailure(CpuContext cpuContext, ulong outputAddress)
|
||||
{
|
||||
if (outputAddress != 0)
|
||||
{
|
||||
_ = TryWriteUInt64Compat(outputAddress, 0);
|
||||
}
|
||||
|
||||
cpuContext[CpuRegister.Rax] = ulong.MaxValue;
|
||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private void ResetLazyDlsymStubState()
|
||||
{
|
||||
lock (_lazyDlsymStubGate)
|
||||
{
|
||||
_lazyDlsymStubCache.Clear();
|
||||
_lazyImportStubPoolMapped = false;
|
||||
_lazyImportStubPoolBase = 0;
|
||||
_lazyImportStubNextSlot = 0;
|
||||
_lazyImportStubPoolLimit = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryResolveDlsymGuestAddress(string symbolName, out ulong guestAddress)
|
||||
{
|
||||
guestAddress = 0;
|
||||
if (string.IsNullOrWhiteSpace(symbolName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Tier 0: ELF runtime symbols and aliases.
|
||||
if (TryResolveRuntimeSymbolAddress(symbolName, out guestAddress) ||
|
||||
TryResolveRuntimeSymbolAlias(symbolName, out guestAddress))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Tier 1: existing import stub entries (duplicate prevention).
|
||||
if (TryFindImportStubGuestAddress(symbolName, out guestAddress))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var hasAerolibSymbol = Aerolib.Instance.TryGetByExportName(symbolName, out var hleSymbol);
|
||||
if (hasAerolibSymbol)
|
||||
{
|
||||
if (HleDataSymbols.TryGetAddress(hleSymbol.Nid, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (TryResolveRuntimeSymbolAddress(hleSymbol.Nid, out guestAddress) ||
|
||||
TryResolveRuntimeSymbolAddress(hleSymbol.ExportName, out guestAddress) ||
|
||||
TryFindImportStubGuestAddress(hleSymbol.Nid, out guestAddress) ||
|
||||
TryFindImportStubGuestAddress(hleSymbol.ExportName, out guestAddress))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (Aerolib.Instance.TryGetByNid(symbolName, out hleSymbol))
|
||||
{
|
||||
if (HleDataSymbols.TryGetAddress(hleSymbol.Nid, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (TryFindImportStubGuestAddress(hleSymbol.Nid, out guestAddress) ||
|
||||
TryFindImportStubGuestAddress(hleSymbol.ExportName, out guestAddress))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
hasAerolibSymbol = true;
|
||||
}
|
||||
|
||||
// Tier 3: lazy materialization for callable HLE symbols missing from ELF imports.
|
||||
if (!TryResolveLazyDispatchTarget(symbolName, hasAerolibSymbol, in hleSymbol, out var dispatchNid, out var export))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryGetOrCreateLazyImportStub(
|
||||
dispatchNid,
|
||||
symbolName,
|
||||
hasAerolibSymbol ? hleSymbol : null,
|
||||
export,
|
||||
out guestAddress);
|
||||
}
|
||||
|
||||
private bool TryFindImportStubGuestAddress(string identifier, out ulong guestAddress)
|
||||
{
|
||||
guestAddress = 0;
|
||||
if (string.IsNullOrWhiteSpace(identifier))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var importEntries = _importEntries;
|
||||
for (var i = 0; i < importEntries.Length; i++)
|
||||
{
|
||||
var entry = importEntries[i];
|
||||
if (!ImportStubEntryMatchesIdentifier(entry, identifier))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.Address >= 0x10000)
|
||||
{
|
||||
guestAddress = entry.Address;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool ImportStubEntryMatchesIdentifier(in ImportStubEntry entry, string identifier)
|
||||
{
|
||||
if (string.Equals(entry.Nid, identifier, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (entry.Export is not { } export)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return string.Equals(export.Name, identifier, StringComparison.Ordinal) ||
|
||||
string.Equals(export.Nid, identifier, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool IsKernelDynlibDlsymIdentifier(string identifier)
|
||||
{
|
||||
return string.Equals(identifier, "sceKernelDlsym", StringComparison.Ordinal) ||
|
||||
string.Equals(identifier, KernelDynlibDlsymAerolibNid, StringComparison.Ordinal) ||
|
||||
string.Equals(identifier, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private bool TryResolveLazyDispatchTarget(
|
||||
string symbolName,
|
||||
bool hasAerolibSymbol,
|
||||
in SysAbiSymbol hleSymbol,
|
||||
out string dispatchNid,
|
||||
out ExportedFunction? export)
|
||||
{
|
||||
export = null;
|
||||
dispatchNid = string.Empty;
|
||||
|
||||
if (IsKernelDynlibDlsymIdentifier(symbolName))
|
||||
{
|
||||
dispatchNid = KernelDynlibDlsymAerolibNid;
|
||||
_ = _moduleManager.TryGetExport(dispatchNid, out export);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!hasAerolibSymbol)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (HleDataSymbols.TryGetAddress(hleSymbol.Nid, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_moduleManager.TryGetExport(hleSymbol.Nid, out export))
|
||||
{
|
||||
dispatchNid = hleSymbol.Nid;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryGetOrCreateLazyImportStub(
|
||||
string dispatchNid,
|
||||
string symbolName,
|
||||
SysAbiSymbol? hleSymbol,
|
||||
ExportedFunction? export,
|
||||
out ulong guestAddress)
|
||||
{
|
||||
guestAddress = 0;
|
||||
if (string.IsNullOrWhiteSpace(dispatchNid))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (_lazyDlsymStubGate)
|
||||
{
|
||||
if (TryGetCachedLazyDlsymAddress(dispatchNid, symbolName, out guestAddress))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!EnsureLazyImportStubPoolMapped())
|
||||
{
|
||||
LogLazyImportStubMaterializationFailure(
|
||||
"import stub region unresolved",
|
||||
dispatchNid,
|
||||
symbolName);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryAllocateLazyImportStubSlot(out guestAddress))
|
||||
{
|
||||
LogLazyImportStubMaterializationFailure(
|
||||
"lazy import stub pool exhausted",
|
||||
dispatchNid,
|
||||
symbolName);
|
||||
return false;
|
||||
}
|
||||
|
||||
var previousEntries = _importEntries;
|
||||
var importIndex = previousEntries.Length;
|
||||
var newEntries = new ImportStubEntry[importIndex + 1];
|
||||
if (importIndex > 0)
|
||||
{
|
||||
Array.Copy(previousEntries, newEntries, importIndex);
|
||||
}
|
||||
|
||||
newEntries[importIndex] = new ImportStubEntry(guestAddress, dispatchNid, export);
|
||||
|
||||
var hostTrampoline = CreateImportHandlerTrampoline(importIndex);
|
||||
if (hostTrampoline == 0 ||
|
||||
!PatchImportStub((nint)(long)guestAddress, hostTrampoline))
|
||||
{
|
||||
guestAddress = 0;
|
||||
LogLazyImportStubMaterializationFailure(
|
||||
"failed to patch lazy import stub trampoline",
|
||||
dispatchNid,
|
||||
symbolName);
|
||||
return false;
|
||||
}
|
||||
|
||||
_importEntries = newEntries;
|
||||
|
||||
RegisterDlsymRuntimeSymbolKeys(symbolName, dispatchNid, hleSymbol, guestAddress);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGetCachedLazyDlsymAddress(string dispatchNid, string symbolName, out ulong guestAddress)
|
||||
{
|
||||
if (_lazyDlsymStubCache.TryGetValue(dispatchNid, out guestAddress) ||
|
||||
_lazyDlsymStubCache.TryGetValue(symbolName, out guestAddress))
|
||||
{
|
||||
return guestAddress >= 0x10000;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void RegisterDlsymRuntimeSymbolKeys(
|
||||
string symbolName,
|
||||
string dispatchNid,
|
||||
SysAbiSymbol? hleSymbol,
|
||||
ulong guestAddress)
|
||||
{
|
||||
RegisterDlsymRuntimeSymbolKey(symbolName, guestAddress);
|
||||
RegisterDlsymRuntimeSymbolKey(dispatchNid, guestAddress);
|
||||
|
||||
if (IsKernelDynlibDlsymIdentifier(symbolName) || IsKernelDynlibDlsymIdentifier(dispatchNid))
|
||||
{
|
||||
RegisterDlsymRuntimeSymbolKey(RuntimeStubNids.KernelDynlibDlsym, guestAddress);
|
||||
RegisterDlsymRuntimeSymbolKey(KernelDynlibDlsymAerolibNid, guestAddress);
|
||||
RegisterDlsymRuntimeSymbolKey("sceKernelDlsym", guestAddress);
|
||||
}
|
||||
|
||||
if (hleSymbol.HasValue)
|
||||
{
|
||||
RegisterDlsymRuntimeSymbolKey(hleSymbol.Value.Nid, guestAddress);
|
||||
RegisterDlsymRuntimeSymbolKey(hleSymbol.Value.ExportName, guestAddress);
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterDlsymRuntimeSymbolKey(string key, ulong guestAddress)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key) || !IsRuntimeSymbolAddressUsable(guestAddress))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_runtimeSymbolsByName[key] = guestAddress;
|
||||
_lazyDlsymStubCache[key] = guestAddress;
|
||||
}
|
||||
|
||||
private void LogLazyImportStubMaterializationFailure(string reason, string dispatchNid, string symbolName)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Lazy import stub materialization failed ({reason}): " +
|
||||
$"nid={dispatchNid} symbol='{symbolName}'");
|
||||
}
|
||||
|
||||
private unsafe bool TryResolveImportStubRegionBounds(
|
||||
ImportStubEntry[] importEntries,
|
||||
out ulong regionBase,
|
||||
out ulong regionLimit)
|
||||
{
|
||||
regionBase = 0;
|
||||
regionLimit = 0;
|
||||
|
||||
for (var candidateIndex = 0; candidateIndex < 64; candidateIndex++)
|
||||
{
|
||||
var candidateBase = ImportStubRegionCanonicalBase -
|
||||
(ulong)candidateIndex * ImportStubRegionAddressStride;
|
||||
if (VirtualQuery((void*)candidateBase, out var memoryInfo, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0 ||
|
||||
memoryInfo.RegionSize == 0 ||
|
||||
memoryInfo.State != 4096)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var candidateLimit = candidateBase + memoryInfo.RegionSize;
|
||||
var hasStub = false;
|
||||
for (var i = 0; i < importEntries.Length; i++)
|
||||
{
|
||||
var entryAddress = importEntries[i].Address;
|
||||
if (entryAddress < candidateBase || entryAddress >= candidateLimit)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((entryAddress - candidateBase) % LazyImportStubSlotSize != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
hasStub = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!hasStub)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
regionBase = candidateBase;
|
||||
regionLimit = candidateLimit;
|
||||
return true;
|
||||
}
|
||||
|
||||
ulong maxStubEnd = 0;
|
||||
for (var i = 0; i < importEntries.Length; i++)
|
||||
{
|
||||
var entryAddress = importEntries[i].Address;
|
||||
if (entryAddress < ImportStubRegionCanonicalBase)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((entryAddress - ImportStubRegionCanonicalBase) % LazyImportStubSlotSize != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var entryEnd = entryAddress + LazyImportStubSlotSize;
|
||||
if (entryEnd > maxStubEnd)
|
||||
{
|
||||
maxStubEnd = entryEnd;
|
||||
regionBase = ImportStubRegionCanonicalBase;
|
||||
}
|
||||
}
|
||||
|
||||
if (regionBase == 0 || maxStubEnd <= regionBase)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var spanBytes = maxStubEnd - regionBase;
|
||||
regionLimit = regionBase + AlignUp(spanBytes, ImportStubRegionPageSize);
|
||||
return regionLimit > regionBase;
|
||||
}
|
||||
|
||||
private bool EnsureLazyImportStubPoolMapped()
|
||||
{
|
||||
if (_lazyImportStubPoolMapped && _lazyImportStubPoolBase != 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var importEntries = _importEntries;
|
||||
if (!TryResolveImportStubRegionBounds(importEntries, out var importStubRegionBase, out var importStubRegionLimit))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ulong nextSlot = importStubRegionBase;
|
||||
for (var i = 0; i < importEntries.Length; i++)
|
||||
{
|
||||
var entryAddress = importEntries[i].Address;
|
||||
if (entryAddress < importStubRegionBase || entryAddress >= importStubRegionLimit)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var entryEnd = entryAddress + LazyImportStubSlotSize;
|
||||
if (entryEnd > nextSlot)
|
||||
{
|
||||
nextSlot = entryEnd;
|
||||
}
|
||||
}
|
||||
|
||||
if (nextSlot >= importStubRegionLimit)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_lazyImportStubPoolBase = importStubRegionBase;
|
||||
_lazyImportStubNextSlot = nextSlot;
|
||||
_lazyImportStubPoolLimit = importStubRegionLimit;
|
||||
_lazyImportStubPoolMapped = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryAllocateLazyImportStubSlot(out ulong guestAddress)
|
||||
{
|
||||
guestAddress = 0;
|
||||
if (!_lazyImportStubPoolMapped ||
|
||||
_lazyImportStubNextSlot < _lazyImportStubPoolBase ||
|
||||
_lazyImportStubNextSlot + LazyImportStubSlotSize > _lazyImportStubPoolLimit)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
guestAddress = _lazyImportStubNextSlot;
|
||||
_lazyImportStubNextSlot += LazyImportStubSlotSize;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryResolveRuntimeSymbolAlias(string symbolName, out ulong address)
|
||||
{
|
||||
address = 0;
|
||||
@@ -1358,29 +1861,20 @@ public sealed partial class DirectExecutionBackend
|
||||
return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
ulong bridgeHandle = cpuContext[CpuRegister.Rdi];
|
||||
ulong symbolNameAddress = cpuContext[CpuRegister.Rsi];
|
||||
ulong outputAddress = cpuContext[CpuRegister.Rdx];
|
||||
_ = TryReadAsciiZ(symbolNameAddress, 512, out var symbolName);
|
||||
|
||||
OrbisGen2Result result = DispatchKernelDynlibDlsym();
|
||||
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
try
|
||||
{
|
||||
return result;
|
||||
OrbisGen2Result result = DispatchKernelDynlibDlsym();
|
||||
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
if (_logBootstrap)
|
||||
catch
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] bootstrap_dispatch: handle=0x{bridgeHandle:X16} symbol='{symbolName}' out=0x{outputAddress:X16} rax=0x{cpuContext[CpuRegister.Rax]:X16}");
|
||||
return CompleteKernelDynlibDlsymFailure(cpuContext, outputAddress);
|
||||
}
|
||||
|
||||
if (cpuContext[CpuRegister.Rax] == 0uL)
|
||||
{
|
||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] bootstrap_bridge unresolved: handle=0x{bridgeHandle:X} symbol='{symbolName}' out=0x{outputAddress:X16}");
|
||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -1421,6 +1915,7 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
List<byte> list = new List<byte>(Math.Min(maxLength, 256));
|
||||
Span<byte> destination = stackalloc byte[1];
|
||||
for (int i = 0; i < maxLength; i++)
|
||||
|
||||
@@ -0,0 +1,582 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
// Guest entry stubs must not run above CLR-managed frames on a CLR-created thread:
|
||||
// the suspension/stack-walk machinery then has to traverse a frame chain that is
|
||||
// interleaved with guest stubs carrying no CLR unwind info, and any window in which
|
||||
// the thread-mode bookkeeping disagrees with the actual stack (import dispatch, VEH
|
||||
// redirection) fail-fasts the runtime — the audio_output_thread ~7th-cycle
|
||||
// "attempted to call a UnmanagedCallersOnly method from managed code" crash.
|
||||
//
|
||||
// Guest execution therefore runs on dedicated raw OS threads whose run loop is
|
||||
// emitted native code. While guest code executes there is not a single managed
|
||||
// frame on the thread and it sits in preemptive mode, so the GC ignores it
|
||||
// entirely. The only managed activity on the thread is the reverse-P/Invoke import
|
||||
// dispatch and the per-run prologue/epilogue, which the CLR supports on foreign
|
||||
// threads (lazy attach, preemptive between calls). The managed orchestrator
|
||||
// (RunGuestThread / the main execution thread) parks in a preemptive wait for the
|
||||
// duration of the run, so its own frame chain is never walked across guest frames.
|
||||
private static readonly bool NativeGuestWorkersDisabled =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_NATIVE_GUEST_WORKERS"), "1", StringComparison.Ordinal);
|
||||
|
||||
private readonly object _nativeWorkerGate = new();
|
||||
private readonly List<NativeGuestExecutor> _allNativeWorkers = new();
|
||||
private readonly Stack<NativeGuestExecutor> _idleNativeWorkers = new();
|
||||
private bool _nativeWorkersDisposed;
|
||||
private int _nativeWorkerCreationFailedLogged;
|
||||
|
||||
private const uint StackSizeParamIsAReservation = 0x00010000u;
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern nint CreateThread(
|
||||
nint lpThreadAttributes,
|
||||
nuint dwStackSize,
|
||||
nint lpStartAddress,
|
||||
nint lpParameter,
|
||||
uint dwCreationFlags,
|
||||
out uint lpThreadId);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern uint WaitForSingleObject(nint hHandle, uint dwMilliseconds);
|
||||
|
||||
// Runs an emitted guest entry stub. Preferred path is a pooled native worker
|
||||
// thread; falls back to the historical inline calli (guest frames above this
|
||||
// thread's managed frames) when workers are disabled or unavailable.
|
||||
//
|
||||
// Callers set the Active* thread-statics before emitting the stub and read the
|
||||
// yield/forced-exit flags right after this returns, so the worker outcome is
|
||||
// copied back into this thread's statics before returning.
|
||||
private unsafe int RunGuestEntryStub(void* entryStub, ulong hostRspSlot)
|
||||
{
|
||||
var worker = RentNativeGuestExecutor();
|
||||
if (worker is null)
|
||||
{
|
||||
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
||||
return CallNativeEntry(entryStub);
|
||||
}
|
||||
try
|
||||
{
|
||||
var state = _activeGuestThreadState;
|
||||
var nativeReturn = worker.Run(
|
||||
_activeCpuContext!,
|
||||
state,
|
||||
GuestThreadExecution.CurrentGuestThreadHandle,
|
||||
_activeEntryReturnSentinelRip,
|
||||
_activeGuestReturnSlotAddress,
|
||||
(nint)hostRspSlot,
|
||||
(nint)entryStub,
|
||||
state?.AffinityMask ?? 0,
|
||||
out var yieldRequested,
|
||||
out var yieldReason,
|
||||
out var forcedExit);
|
||||
_activeGuestThreadYieldRequested = yieldRequested;
|
||||
_activeGuestThreadYieldReason = yieldReason;
|
||||
_activeForcedGuestExit = forcedExit;
|
||||
return nativeReturn;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ReturnNativeGuestExecutor(worker);
|
||||
}
|
||||
}
|
||||
|
||||
private NativeGuestExecutor? RentNativeGuestExecutor()
|
||||
{
|
||||
if (NativeGuestWorkersDisabled)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
lock (_nativeWorkerGate)
|
||||
{
|
||||
if (_nativeWorkersDisposed)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (_idleNativeWorkers.Count > 0)
|
||||
{
|
||||
return _idleNativeWorkers.Pop();
|
||||
}
|
||||
}
|
||||
var worker = NativeGuestExecutor.TryCreate(this);
|
||||
if (worker is null)
|
||||
{
|
||||
if (Interlocked.Exchange(ref _nativeWorkerCreationFailedLogged, 1) == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Failed to create a native guest worker thread; falling back to inline guest execution.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
lock (_nativeWorkerGate)
|
||||
{
|
||||
if (_nativeWorkersDisposed)
|
||||
{
|
||||
worker.Dispose();
|
||||
return null;
|
||||
}
|
||||
_allNativeWorkers.Add(worker);
|
||||
}
|
||||
return worker;
|
||||
}
|
||||
|
||||
private void ReturnNativeGuestExecutor(NativeGuestExecutor worker)
|
||||
{
|
||||
lock (_nativeWorkerGate)
|
||||
{
|
||||
if (!_nativeWorkersDisposed)
|
||||
{
|
||||
_idleNativeWorkers.Push(worker);
|
||||
return;
|
||||
}
|
||||
}
|
||||
worker.Dispose();
|
||||
}
|
||||
|
||||
private void DisposeNativeGuestExecutors()
|
||||
{
|
||||
NativeGuestExecutor[] workers;
|
||||
lock (_nativeWorkerGate)
|
||||
{
|
||||
if (_nativeWorkersDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_nativeWorkersDisposed = true;
|
||||
workers = _allNativeWorkers.ToArray();
|
||||
_allNativeWorkers.Clear();
|
||||
_idleNativeWorkers.Clear();
|
||||
}
|
||||
foreach (var worker in workers)
|
||||
{
|
||||
worker.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// A pooled raw OS thread that executes guest entry stubs. The run loop is emitted
|
||||
// native code (no CLR unwind info required — nothing ever unwinds through it):
|
||||
//
|
||||
// loop: WaitForSingleObject(work);
|
||||
// if (*stopFlag) { SetEvent(done); ExitThread(0); }
|
||||
// rax = RunPrologue(self); // managed: binds per-run ambient, returns stub
|
||||
// if (rax != 0) eax = rax(); // guest entry stub — zero managed frames below
|
||||
// RunEpilogue(self, eax); // managed: captures outcome, restores ambient
|
||||
// SetEvent(done); goto loop;
|
||||
//
|
||||
// Workers carry no per-guest identity of their own: the prologue rebinds guest TLS,
|
||||
// the host-RSP slot, the Active* thread-statics and the GuestThreadExecution ambient
|
||||
// on every run, so a worker can be reused for any guest thread.
|
||||
private sealed unsafe class NativeGuestExecutor : IDisposable
|
||||
{
|
||||
private const uint LoopStubSize = 512u;
|
||||
private const uint WorkerStackReservation = 4u * 1024u * 1024u;
|
||||
|
||||
private static nint _waitForSingleObjectAddress;
|
||||
private static nint _setEventAddress;
|
||||
private static nint _exitThreadAddress;
|
||||
|
||||
private readonly DirectExecutionBackend _backend;
|
||||
private readonly AutoResetEvent _workAvailable = new(false);
|
||||
private readonly AutoResetEvent _workCompleted = new(false);
|
||||
private GCHandle _selfHandle;
|
||||
private void* _controlBlock;
|
||||
private void* _loopStub;
|
||||
private nint _threadHandle;
|
||||
private uint _nativeThreadId;
|
||||
|
||||
// Single in-flight run; publication is ordered by the work/done event pair.
|
||||
private CpuContext? _runContext;
|
||||
private GuestThreadState? _runState;
|
||||
private ulong _runGuestThreadHandle;
|
||||
private ulong _runSentinelRip;
|
||||
private ulong _runReturnSlotAddress;
|
||||
private nint _runHostRspSlot;
|
||||
private nint _runEntryStub;
|
||||
private ulong _runAffinityMask;
|
||||
private int _runNativeResult;
|
||||
private bool _runYieldRequested;
|
||||
private string? _runYieldReason;
|
||||
private bool _runForcedExit;
|
||||
private bool _runPrologueFailed;
|
||||
|
||||
// Prologue -> epilogue carry, only touched on the worker thread.
|
||||
private DirectExecutionBackend? _prevBackend;
|
||||
private CpuContext? _prevContext;
|
||||
private ulong _prevSentinel;
|
||||
private ulong _prevReturnSlot;
|
||||
private bool _prevForcedExit;
|
||||
private bool _prevYieldRequested;
|
||||
private string? _prevYieldReason;
|
||||
private GuestThreadState? _prevState;
|
||||
private ulong _prevGuestThreadHandle;
|
||||
private nint _prevHostRspSlot;
|
||||
private int _prevHostThreadId;
|
||||
private bool _entered;
|
||||
|
||||
private NativeGuestExecutor(DirectExecutionBackend backend)
|
||||
{
|
||||
_backend = backend;
|
||||
}
|
||||
|
||||
public static NativeGuestExecutor? TryCreate(DirectExecutionBackend backend)
|
||||
{
|
||||
if (!EnsureKernel32Exports())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var executor = new NativeGuestExecutor(backend);
|
||||
if (!executor.Initialize())
|
||||
{
|
||||
executor.Dispose();
|
||||
return null;
|
||||
}
|
||||
return executor;
|
||||
}
|
||||
|
||||
private static bool EnsureKernel32Exports()
|
||||
{
|
||||
if (_exitThreadAddress != 0)
|
||||
{
|
||||
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0;
|
||||
}
|
||||
nint kernel32 = GetModuleHandle("kernel32.dll");
|
||||
if (kernel32 == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
_waitForSingleObjectAddress = GetProcAddress(kernel32, "WaitForSingleObject");
|
||||
_setEventAddress = GetProcAddress(kernel32, "SetEvent");
|
||||
_exitThreadAddress = GetProcAddress(kernel32, "ExitThread");
|
||||
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0 && _exitThreadAddress != 0;
|
||||
}
|
||||
|
||||
private bool Initialize()
|
||||
{
|
||||
_selfHandle = GCHandle.Alloc(this);
|
||||
_controlBlock = VirtualAlloc(null, 4096u, 12288u, 4u);
|
||||
if (_controlBlock == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
_loopStub = VirtualAlloc(null, LoopStubSize, 12288u, 64u);
|
||||
if (_loopStub == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var prologuePtr = (nint)(delegate* unmanaged<nint, nint>)&RunPrologue;
|
||||
var epiloguePtr = (nint)(delegate* unmanaged<nint, int, void>)&RunEpilogue;
|
||||
var executorHandle = GCHandle.ToIntPtr(_selfHandle);
|
||||
var workHandle = _workAvailable.SafeWaitHandle.DangerousGetHandle();
|
||||
var doneHandle = _workCompleted.SafeWaitHandle.DangerousGetHandle();
|
||||
|
||||
byte* code = (byte*)_loopStub;
|
||||
int offset = 0;
|
||||
void Emit(byte value) => code[offset++] = value;
|
||||
void EmitMovRcxImm64(ulong value)
|
||||
{
|
||||
Emit(0x48); Emit(0xB9);
|
||||
*(ulong*)(code + offset) = value;
|
||||
offset += sizeof(ulong);
|
||||
}
|
||||
void EmitMovRaxImm64(ulong value)
|
||||
{
|
||||
Emit(0x48); Emit(0xB8);
|
||||
*(ulong*)(code + offset) = value;
|
||||
offset += sizeof(ulong);
|
||||
}
|
||||
void EmitCallRax()
|
||||
{
|
||||
Emit(0xFF); Emit(0xD0);
|
||||
}
|
||||
void EmitSetDoneEvent()
|
||||
{
|
||||
EmitMovRcxImm64((ulong)doneHandle);
|
||||
EmitMovRaxImm64((ulong)_setEventAddress);
|
||||
EmitCallRax();
|
||||
}
|
||||
|
||||
// Thread entry leaves RSP ≡ 8 (mod 16); after this every call below happens
|
||||
// at RSP ≡ 0 with shadow space available.
|
||||
Emit(0x48); Emit(0x83); Emit(0xEC); Emit(0x28); // sub rsp, 0x28
|
||||
int loopStart = offset;
|
||||
EmitMovRcxImm64((ulong)workHandle);
|
||||
Emit(0xBA); // mov edx, INFINITE
|
||||
*(uint*)(code + offset) = 0xFFFFFFFFu;
|
||||
offset += sizeof(uint);
|
||||
EmitMovRaxImm64((ulong)_waitForSingleObjectAddress);
|
||||
EmitCallRax();
|
||||
EmitMovRaxImm64((ulong)_controlBlock);
|
||||
Emit(0x83); Emit(0x38); Emit(0x00); // cmp dword [rax], 0
|
||||
Emit(0x0F); Emit(0x85); // jne stop
|
||||
int stopJump = offset;
|
||||
offset += sizeof(int);
|
||||
EmitMovRcxImm64((ulong)executorHandle);
|
||||
EmitMovRaxImm64((ulong)prologuePtr);
|
||||
EmitCallRax(); // rax = entry stub, or 0 on failure
|
||||
Emit(0x48); Emit(0x85); Emit(0xC0); // test rax, rax
|
||||
Emit(0x0F); Emit(0x84); // je skipEntry
|
||||
int skipJump = offset;
|
||||
offset += sizeof(int);
|
||||
EmitCallRax(); // guest entry stub -> eax
|
||||
int skipEntryOffset = offset;
|
||||
Emit(0x89); Emit(0xC2); // mov edx, eax
|
||||
EmitMovRcxImm64((ulong)executorHandle);
|
||||
EmitMovRaxImm64((ulong)epiloguePtr);
|
||||
EmitCallRax();
|
||||
EmitSetDoneEvent();
|
||||
Emit(0xE9); // jmp loop
|
||||
*(int*)(code + offset) = loopStart - (offset + sizeof(int));
|
||||
offset += sizeof(int);
|
||||
int stopOffset = offset;
|
||||
// Signal done so a Run() racing teardown cannot park forever; a stopped
|
||||
// worker is never re-rented, so the stale signal is unobservable.
|
||||
EmitSetDoneEvent();
|
||||
Emit(0x31); Emit(0xC9); // xor ecx, ecx
|
||||
EmitMovRaxImm64((ulong)_exitThreadAddress);
|
||||
EmitCallRax();
|
||||
Emit(0xCC); // int3 (ExitThread never returns)
|
||||
*(int*)(code + stopJump) = stopOffset - (stopJump + sizeof(int));
|
||||
*(int*)(code + skipJump) = skipEntryOffset - (skipJump + sizeof(int));
|
||||
|
||||
uint oldProtect = 0;
|
||||
if (!VirtualProtect(_loopStub, LoopStubSize, 32u, &oldProtect))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), _loopStub, LoopStubSize);
|
||||
_threadHandle = CreateThread(
|
||||
0,
|
||||
WorkerStackReservation,
|
||||
(nint)_loopStub,
|
||||
0,
|
||||
StackSizeParamIsAReservation,
|
||||
out _nativeThreadId);
|
||||
if (_threadHandle == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (LogThreadMode)
|
||||
{
|
||||
TraceThreadMode($"worker_created native_tid={_nativeThreadId} loop=0x{(ulong)_loopStub:X16}");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public int Run(
|
||||
CpuContext context,
|
||||
GuestThreadState? state,
|
||||
ulong guestThreadHandle,
|
||||
ulong sentinelRip,
|
||||
ulong returnSlotAddress,
|
||||
nint hostRspSlot,
|
||||
nint entryStub,
|
||||
ulong affinityMask,
|
||||
out bool yieldRequested,
|
||||
out string? yieldReason,
|
||||
out bool forcedExit)
|
||||
{
|
||||
_runContext = context;
|
||||
_runState = state;
|
||||
_runGuestThreadHandle = guestThreadHandle;
|
||||
_runSentinelRip = sentinelRip;
|
||||
_runReturnSlotAddress = returnSlotAddress;
|
||||
_runHostRspSlot = hostRspSlot;
|
||||
_runEntryStub = entryStub;
|
||||
_runAffinityMask = affinityMask;
|
||||
_runPrologueFailed = true;
|
||||
_runYieldRequested = false;
|
||||
_runYieldReason = null;
|
||||
_runForcedExit = false;
|
||||
_workAvailable.Set();
|
||||
_workCompleted.WaitOne();
|
||||
_runContext = null;
|
||||
_runState = null;
|
||||
yieldRequested = _runYieldRequested;
|
||||
yieldReason = _runYieldReason;
|
||||
forcedExit = _runForcedExit;
|
||||
if (_runPrologueFailed)
|
||||
{
|
||||
throw new InvalidOperationException("Native guest worker failed to bind the run ambient (prologue fault)");
|
||||
}
|
||||
return _runNativeResult;
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly]
|
||||
private static nint RunPrologue(nint executorHandle)
|
||||
{
|
||||
try
|
||||
{
|
||||
var executor = (NativeGuestExecutor)GCHandle.FromIntPtr(executorHandle).Target!;
|
||||
return executor.EnterRun();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][ERROR] Native guest worker prologue failed: {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly]
|
||||
private static void RunEpilogue(nint executorHandle, int nativeResult)
|
||||
{
|
||||
try
|
||||
{
|
||||
var executor = (NativeGuestExecutor)GCHandle.FromIntPtr(executorHandle).Target!;
|
||||
executor.ExitRun(nativeResult);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][ERROR] Native guest worker epilogue failed: {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private nint EnterRun()
|
||||
{
|
||||
var backend = _backend;
|
||||
_prevBackend = _activeExecutionBackend;
|
||||
_prevContext = _activeCpuContext;
|
||||
_prevSentinel = _activeEntryReturnSentinelRip;
|
||||
_prevReturnSlot = _activeGuestReturnSlotAddress;
|
||||
_prevForcedExit = _activeForcedGuestExit;
|
||||
_prevYieldRequested = _activeGuestThreadYieldRequested;
|
||||
_prevYieldReason = _activeGuestThreadYieldReason;
|
||||
_prevState = _activeGuestThreadState;
|
||||
_prevHostRspSlot = TlsGetValue(backend._hostRspSlotTlsIndex);
|
||||
_prevGuestThreadHandle = GuestThreadExecution.EnterGuestThread(_runGuestThreadHandle);
|
||||
_entered = true;
|
||||
_activeExecutionBackend = backend;
|
||||
_activeCpuContext = _runContext;
|
||||
_activeEntryReturnSentinelRip = _runSentinelRip;
|
||||
_activeGuestReturnSlotAddress = _runReturnSlotAddress;
|
||||
_activeForcedGuestExit = false;
|
||||
_activeGuestThreadYieldRequested = false;
|
||||
_activeGuestThreadYieldReason = null;
|
||||
_activeGuestThreadState = _runState;
|
||||
backend.BindTlsBase(_runContext!);
|
||||
TlsSetValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
|
||||
if (_runState is { } state)
|
||||
{
|
||||
_prevHostThreadId = Volatile.Read(ref state.HostThreadId);
|
||||
Volatile.Write(ref state.HostThreadId, unchecked((int)GetCurrentThreadId()));
|
||||
}
|
||||
if (_runAffinityMask != 0)
|
||||
{
|
||||
backend.ApplyGuestThreadAffinity(_runAffinityMask);
|
||||
}
|
||||
_runPrologueFailed = false;
|
||||
if (LogThreadMode)
|
||||
{
|
||||
TraceThreadMode(
|
||||
$"worker_enter guest=0x{_runGuestThreadHandle:X16} stub=0x{(ulong)_runEntryStub:X16}");
|
||||
}
|
||||
return _runEntryStub;
|
||||
}
|
||||
|
||||
private void ExitRun(int nativeResult)
|
||||
{
|
||||
_runNativeResult = nativeResult;
|
||||
_runYieldRequested = _activeGuestThreadYieldRequested;
|
||||
_runYieldReason = _activeGuestThreadYieldReason;
|
||||
_runForcedExit = _activeForcedGuestExit;
|
||||
if (!_entered)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_entered = false;
|
||||
if (_runState is { } state)
|
||||
{
|
||||
Volatile.Write(ref state.HostThreadId, _prevHostThreadId);
|
||||
}
|
||||
TlsSetValue(_backend._hostRspSlotTlsIndex, _prevHostRspSlot);
|
||||
GuestThreadExecution.RestoreGuestThread(_prevGuestThreadHandle);
|
||||
_activeExecutionBackend = _prevBackend;
|
||||
_activeCpuContext = _prevContext;
|
||||
_activeEntryReturnSentinelRip = _prevSentinel;
|
||||
_activeGuestReturnSlotAddress = _prevReturnSlot;
|
||||
_activeForcedGuestExit = _prevForcedExit;
|
||||
_activeGuestThreadYieldRequested = _prevYieldRequested;
|
||||
_activeGuestThreadYieldReason = _prevYieldReason;
|
||||
_activeGuestThreadState = _prevState;
|
||||
_prevBackend = null;
|
||||
_prevContext = null;
|
||||
_prevState = null;
|
||||
_prevYieldReason = null;
|
||||
if (LogThreadMode)
|
||||
{
|
||||
TraceThreadMode(
|
||||
$"worker_exit guest=0x{_runGuestThreadHandle:X16} result=0x{nativeResult:X8} yield={_runYieldRequested}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_controlBlock != null)
|
||||
{
|
||||
*(int*)_controlBlock = 1;
|
||||
}
|
||||
try
|
||||
{
|
||||
_workAvailable.Set();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
}
|
||||
var exited = _threadHandle == 0;
|
||||
if (_threadHandle != 0)
|
||||
{
|
||||
exited = WaitForSingleObject(_threadHandle, 1000u) == 0u;
|
||||
CloseHandle(_threadHandle);
|
||||
_threadHandle = 0;
|
||||
}
|
||||
if (!exited)
|
||||
{
|
||||
// The worker is still parked in guest code (teardown should have unwound
|
||||
// it first). Leak the stub, control block, events and GC handle rather
|
||||
// than have the thread execute freed memory.
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Native guest worker tid={_nativeThreadId} did not stop; leaking its run loop.");
|
||||
return;
|
||||
}
|
||||
if (_loopStub != null)
|
||||
{
|
||||
VirtualFree(_loopStub, 0u, 32768u);
|
||||
_loopStub = null;
|
||||
}
|
||||
if (_controlBlock != null)
|
||||
{
|
||||
VirtualFree(_controlBlock, 0u, 32768u);
|
||||
_controlBlock = null;
|
||||
}
|
||||
if (_selfHandle.IsAllocated)
|
||||
{
|
||||
_selfHandle.Free();
|
||||
}
|
||||
_workAvailable.Dispose();
|
||||
_workCompleted.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
@@ -48,6 +49,13 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
ulong Arg1,
|
||||
ulong Arg2);
|
||||
|
||||
private readonly record struct DeferredBootstrapTraceEntry(
|
||||
long DispatchIndex,
|
||||
ulong Op,
|
||||
ulong SymbolPointer,
|
||||
ulong OutputPointer,
|
||||
ulong ReturnRip);
|
||||
|
||||
#pragma warning disable CS0649
|
||||
private struct EXCEPTION_POINTERS
|
||||
{
|
||||
@@ -232,7 +240,31 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
private KeyValuePair<string, ulong>[] _runtimeSymbolsByAddress = Array.Empty<KeyValuePair<string, ulong>>();
|
||||
|
||||
private readonly Dictionary<string, ulong> _runtimeSymbolsByName = new Dictionary<string, ulong>(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, ulong> _runtimeSymbolsByName =
|
||||
new(StringComparer.Ordinal);
|
||||
|
||||
// Keep in sync with SelfLoader import-stub mapping constants.
|
||||
private const ulong ImportStubRegionCanonicalBase = 0x0000_7000_0000_0000UL;
|
||||
|
||||
private const ulong ImportStubRegionAddressStride = 0x0000_0000_0100_0000UL;
|
||||
|
||||
private const ulong LazyImportStubSlotSize = 0x10;
|
||||
|
||||
private const ulong ImportStubRegionPageSize = 0x1000UL;
|
||||
|
||||
private const string KernelDynlibDlsymAerolibNid = "LwG8g3niqwA";
|
||||
|
||||
private readonly object _lazyDlsymStubGate = new();
|
||||
|
||||
private readonly Dictionary<string, ulong> _lazyDlsymStubCache = new(StringComparer.Ordinal);
|
||||
|
||||
private ulong _lazyImportStubPoolBase;
|
||||
|
||||
private ulong _lazyImportStubNextSlot;
|
||||
|
||||
private ulong _lazyImportStubPoolLimit;
|
||||
|
||||
private bool _lazyImportStubPoolMapped;
|
||||
|
||||
private readonly RecentImportTraceEntry[] _recentImportTrace = new RecentImportTraceEntry[64];
|
||||
|
||||
@@ -240,6 +272,14 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
private int _recentImportTraceWriteIndex;
|
||||
|
||||
private readonly DeferredBootstrapTraceEntry[] _deferredBootstrapTrace = new DeferredBootstrapTraceEntry[32];
|
||||
|
||||
private int _deferredBootstrapTraceCount;
|
||||
|
||||
private int _deferredBootstrapTraceWriteIndex;
|
||||
|
||||
private readonly object _deferredBootstrapTraceGate = new();
|
||||
|
||||
private readonly string[] _distinctImportNidHistory = new string[128];
|
||||
|
||||
private int _distinctImportNidHistoryCount;
|
||||
@@ -424,6 +464,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
private void ThreadMain()
|
||||
{
|
||||
var previousGuestThreadHandle = GuestThreadExecution.EnterGuestThread(_guestThreadHandle);
|
||||
if (LogThreadMode)
|
||||
{
|
||||
TraceThreadMode($"runner_start guest=0x{_guestThreadHandle:X16}");
|
||||
}
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
@@ -434,6 +478,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
return;
|
||||
}
|
||||
|
||||
if (LogThreadMode)
|
||||
{
|
||||
_threadModeCycleId = Interlocked.Increment(ref _threadModeCycleCounter);
|
||||
TraceThreadMode($"runner_run guest=0x{_guestThreadHandle:X16}");
|
||||
}
|
||||
try
|
||||
{
|
||||
_work?.Invoke();
|
||||
@@ -446,6 +495,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (LogThreadMode)
|
||||
{
|
||||
TraceThreadMode($"runner_stop guest=0x{_guestThreadHandle:X16}");
|
||||
}
|
||||
GuestThreadExecution.RestoreGuestThread(previousGuestThreadHandle);
|
||||
}
|
||||
}
|
||||
@@ -469,6 +522,14 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
private readonly Queue<GuestThreadState> _readyGuestThreads = new Queue<GuestThreadState>();
|
||||
|
||||
// Once set, guest worker threads are unwound to the host at their next import
|
||||
// dispatch and Pump refuses to start new ones. This must happen before the
|
||||
// runtime frees trampolines or guest memory: workers that keep running native
|
||||
// guest code during teardown execute freed pages (observed as an execute-AV in
|
||||
// a MEM_FREE module region plus a CLR "UnmanagedCallersOnly" fatal from a Pump
|
||||
// thread entering a freed stub).
|
||||
private volatile bool _guestTeardownRequested;
|
||||
|
||||
private int _readyGuestThreadCount;
|
||||
|
||||
private readonly Dictionary<ulong, GuestThreadState> _guestThreads = new Dictionary<ulong, GuestThreadState>();
|
||||
@@ -567,6 +628,33 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
private static int _nestedVehTraceCount;
|
||||
|
||||
// SHARPEMU_LOG_THREAD_MODE=1 — GC thread-mode corruption investigation. Traces
|
||||
// every managed<->guest transition per guest-thread run cycle so the last event
|
||||
// before a ReversePInvokeBadTransition FailFast identifies the corrupting path.
|
||||
private static readonly bool LogThreadMode =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_THREAD_MODE"), "1", StringComparison.Ordinal);
|
||||
|
||||
private static long _threadModeCycleCounter;
|
||||
|
||||
[ThreadStatic]
|
||||
private static long _threadModeCycleId;
|
||||
|
||||
[ThreadStatic]
|
||||
private static int _threadModeGatewayDepth;
|
||||
|
||||
[ThreadStatic]
|
||||
private static long _threadModeGatewayCalls;
|
||||
|
||||
[ThreadStatic]
|
||||
private static bool _threadModeGatewayFirstLogged;
|
||||
|
||||
private static void TraceThreadMode(string message)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[THREADMODE] {message} cycle={_threadModeCycleId} tid={GetCurrentThreadId()} managed={Environment.CurrentManagedThreadId}");
|
||||
Console.Error.Flush();
|
||||
}
|
||||
|
||||
private const uint MEM_COMMIT = 4096u;
|
||||
|
||||
private const uint MEM_RESERVE = 8192u;
|
||||
@@ -625,7 +713,17 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
private unsafe static int CallNativeEntry(void* entry)
|
||||
{
|
||||
var nativeEntry = (delegate* unmanaged[Cdecl]<int>)entry;
|
||||
return nativeEntry();
|
||||
if (!LogThreadMode)
|
||||
{
|
||||
return nativeEntry();
|
||||
}
|
||||
|
||||
_threadModeGatewayFirstLogged = false;
|
||||
var dispatchesBefore = _threadModeGatewayCalls;
|
||||
TraceThreadMode($"native_enter entry=0x{(ulong)entry:X16}");
|
||||
var result = nativeEntry();
|
||||
TraceThreadMode($"native_exit result=0x{result:X8} dispatches={_threadModeGatewayCalls - dispatchesBefore}");
|
||||
return result;
|
||||
}
|
||||
|
||||
private unsafe static void WriteCtxU64(void* contextRecord, int offset, ulong value)
|
||||
@@ -797,8 +895,14 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
result = OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
LastError = null;
|
||||
InitializeRuntimeSymbolIndex(runtimeSymbols);
|
||||
ResetLazyDlsymStubState();
|
||||
_recentImportTraceCount = 0;
|
||||
_recentImportTraceWriteIndex = 0;
|
||||
lock (_deferredBootstrapTraceGate)
|
||||
{
|
||||
_deferredBootstrapTraceCount = 0;
|
||||
_deferredBootstrapTraceWriteIndex = 0;
|
||||
}
|
||||
_distinctImportNidHistoryCount = 0;
|
||||
_distinctImportNidHistoryWriteIndex = 0;
|
||||
_lastDistinctImportNid = string.Empty;
|
||||
@@ -824,6 +928,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
_importLoopGuardSeconds = GetImportLoopGuardSeconds();
|
||||
_entryReturnSentinelRip = 0uL;
|
||||
_forcedGuestExit = false;
|
||||
_guestTeardownRequested = false;
|
||||
_importLoopSignatureCount = 0;
|
||||
_importLoopSignatureWriteIndex = 0;
|
||||
_importLoopPatternHits = 0;
|
||||
@@ -871,6 +976,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
}
|
||||
finally
|
||||
{
|
||||
DrainDeferredBootstrapTraces();
|
||||
GuestThreadExecution.Scheduler = previousGuestThreadScheduler;
|
||||
Console.Error.WriteLine("[LOADER][INFO] === Execute END (LastError: " + (LastError ?? "null") + ") ===");
|
||||
}
|
||||
@@ -1122,21 +1228,72 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
0x75, 0xF5,
|
||||
0xC3,
|
||||
],
|
||||
// memcpy: guarded native copy. The first "rep movsb" intrinsic had no bounds/null
|
||||
// checking and crashed with a read at -1 right after a null-dst memset recovery in
|
||||
// the NGS2 audio streaming code path, so it was temporarily pulled in favor of the
|
||||
// safe C# HLE memcpy. That detour costs a full import dispatch per call - far too
|
||||
// slow for a function this hot - so this stub keeps the native leaf path and adds
|
||||
// the same guards as memset below: it silently returns dst without copying when dst
|
||||
// or src is null/low-page (< 0x10000) or outside canonical user space, or when len
|
||||
// is 0 or absurd (> 512MB).
|
||||
"Q3VBxCXhUHs" =>
|
||||
[
|
||||
0x48, 0x89, 0xF8,
|
||||
0x48, 0x89, 0xD1,
|
||||
0xF3, 0xA4,
|
||||
0xC3,
|
||||
0x48, 0x89, 0xF8, // mov rax, rdi (return dst)
|
||||
0x48, 0x81, 0xFF, 0x00, 0x00, 0x01, 0x00, // cmp rdi, 0x10000
|
||||
0x72, 0x31, // jb done
|
||||
0x49, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, // mov r8, 0x800000000000
|
||||
0x4C, 0x39, 0xC7, // cmp rdi, r8
|
||||
0x73, 0x22, // jae done
|
||||
0x48, 0x81, 0xFE, 0x00, 0x00, 0x01, 0x00, // cmp rsi, 0x10000
|
||||
0x72, 0x19, // jb done
|
||||
0x4C, 0x39, 0xC6, // cmp rsi, r8
|
||||
0x73, 0x14, // jae done
|
||||
0x48, 0x81, 0xFA, 0x00, 0x00, 0x00, 0x20, // cmp rdx, 0x20000000
|
||||
0x77, 0x0B, // ja done
|
||||
0x48, 0x85, 0xD2, // test rdx, rdx
|
||||
0x74, 0x06, // jz done
|
||||
0x48, 0x89, 0xD1, // mov rcx, rdx
|
||||
0xFC, // cld
|
||||
0xF3, 0xA4, // rep movsb
|
||||
0xC3, // done: ret
|
||||
],
|
||||
// memset: guarded native fill. An earlier unguarded version crashed with a write AV
|
||||
// at address 0 (NGS2 audio streaming init memsets a never-populated buffer field),
|
||||
// so this one mirrors the HLE guards and silently returns dst without writing when
|
||||
// dst is null/low-page (< 0x10000), dst is outside canonical user space, or len is
|
||||
// absurd (> 512MB, e.g. the 0x27060035 / sign-extended values NGS2 passes). Routing
|
||||
// memset through the HLE trampoline instead is not viable: parse/streaming loops
|
||||
// issue hundreds of thousands of small memsets back-to-back, which crawls at
|
||||
// dispatch speed and looks like a repeating-import hang to the loop guard.
|
||||
// _sigprocmask: the HLE handler (KernelRuntimeCompatExports.Sigprocmask) is a pure
|
||||
// no-op returning 0 that never writes oldset, so this is behavior-identical. The
|
||||
// game's bundled libc queries the mask (set=NULL) once per iteration in its font/
|
||||
// parse loops - hundreds of thousands of back-to-back calls that both crawl at
|
||||
// dispatch speed and read as a repeating-import hang to the loop guard.
|
||||
"6xVpy0Fdq+I" =>
|
||||
[
|
||||
0x31, 0xC0, // xor eax, eax
|
||||
0xC3, // ret
|
||||
],
|
||||
"8zTFvBIAIN8" =>
|
||||
[
|
||||
0x49, 0x89, 0xF8,
|
||||
0x48, 0x89, 0xF0,
|
||||
0x48, 0x89, 0xD1,
|
||||
0xF3, 0xAA,
|
||||
0x4C, 0x89, 0xC0,
|
||||
0xC3,
|
||||
0x48, 0x89, 0xF8, // mov rax, rdi (return dst)
|
||||
0x48, 0x81, 0xFF, 0x00, 0x00, 0x01, 0x00, // cmp rdi, 0x10000
|
||||
0x72, 0x2B, // jb done
|
||||
0x49, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, // mov r8, 0x800000000000
|
||||
0x4C, 0x39, 0xC7, // cmp rdi, r8
|
||||
0x73, 0x1C, // jae done
|
||||
0x48, 0x81, 0xFA, 0x00, 0x00, 0x00, 0x20, // cmp rdx, 0x20000000
|
||||
0x77, 0x13, // ja done
|
||||
0x48, 0x85, 0xD2, // test rdx, rdx
|
||||
0x74, 0x0E, // jz done
|
||||
0x48, 0x89, 0xD1, // mov rcx, rdx
|
||||
0x49, 0x89, 0xF9, // mov r9, rdi
|
||||
0x89, 0xF0, // mov eax, esi
|
||||
0xFC, // cld
|
||||
0xF3, 0xAA, // rep stosb
|
||||
0x4C, 0x89, 0xC8, // mov rax, r9
|
||||
0xC3, // done: ret
|
||||
],
|
||||
_ => default,
|
||||
};
|
||||
@@ -1355,10 +1512,25 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
{
|
||||
return exportName switch
|
||||
{
|
||||
"memcpy" or
|
||||
"memmove" or
|
||||
"memset" or
|
||||
"memcmp" => true,
|
||||
// memset/memcpy excluded: the raw LLE routines have no null/bounds guard and crash
|
||||
// with an access violation on bad pointers (observed hit during Quake's CL_Init,
|
||||
// where a still-unidentified upstream bug calls memcpy/memset with a null
|
||||
// destination). Both are instead served by the guarded native intrinsics in
|
||||
// TryCreateNativeImportIntrinsic, which fail safely without leaving the leaf path.
|
||||
"memcmp" or
|
||||
// _Getpctype must come from the game's own Dinkumware libc when one is bundled:
|
||||
// it returns a pointer to that CRT's ctype bitmask table, whose bit layout
|
||||
// (_DI=0x20, _SP=0x04, _BB=0x80, ...) differs from the MSVC-style table the HLE
|
||||
// fallback used to serve. Serving the wrong layout made the bundled printf engine
|
||||
// render every Sys_Error message as an empty string (isdigit misfired during
|
||||
// %-directive parsing) and made mcpp drop 'a'-'f' from identifiers ("texture" ->
|
||||
// "txtur", the 0x80 bit reads as _BB/control there). _Getptolower/_Getptoupper
|
||||
// already resolve to the bundled module because no HLE export shadows them; this
|
||||
// keeps _Getpctype consistent with them. It is a pure accessor returning a pointer
|
||||
// to a const table, so it is also the cheapest possible LLE call - important
|
||||
// because parsers hit it once per input character.
|
||||
"_Getpctype" => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
@@ -1871,6 +2043,36 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
byte* code = (byte*)ptr;
|
||||
int offset = 0;
|
||||
// Native pre-filter: these exception codes are raised while the thread can be in
|
||||
// cooperative GC mode (a C# throw is RaiseException(0xE0434352) on the throwing
|
||||
// thread; FailFast/stack-overflow arrive mid-runtime-failure). Entering the managed
|
||||
// handler then trips the CLR's reverse-P/Invoke check and kills the process with
|
||||
// "Invalid Program: attempted to call a UnmanagedCallersOnly method from managed
|
||||
// code" — this is why no managed throw (even one with a catch handler) ever
|
||||
// survived inside the emulator. Continue the handler search without touching
|
||||
// managed code; the CLR's own VEH handles its exceptions. MSVC C++ exceptions
|
||||
// (Vulkan drivers, host CRT) are excluded too: the managed handler only ever
|
||||
// returned CONTINUE_SEARCH for them.
|
||||
ReadOnlySpan<uint> nonManagedExceptionCodes = [0xE0434352u, MSVC_CPP_EXCEPTION, 0xC0000409u, 0xC00000FDu];
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x01); // mov rax, [rcx] (ExceptionRecord*)
|
||||
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x00); // mov eax, [rax] (ExceptionCode)
|
||||
var passJumpOffsets = stackalloc int[nonManagedExceptionCodes.Length];
|
||||
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
|
||||
{
|
||||
EmitByte(code, ref offset, 0x3D); // cmp eax, imm32
|
||||
EmitUInt32(code, ref offset, nonManagedExceptionCodes[i]);
|
||||
EmitByte(code, ref offset, 0x74); // je pass
|
||||
passJumpOffsets[i] = offset;
|
||||
EmitByte(code, ref offset, 0x00);
|
||||
}
|
||||
EmitByte(code, ref offset, 0xEB); EmitByte(code, ref offset, 0x03); // jmp over pass block
|
||||
int passOffset = offset;
|
||||
EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0); // pass: xor eax, eax (EXCEPTION_CONTINUE_SEARCH)
|
||||
EmitByte(code, ref offset, 0xC3); // ret
|
||||
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
|
||||
{
|
||||
code[passJumpOffsets[i]] = checked((byte)(passOffset - (passJumpOffsets[i] + 1)));
|
||||
}
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x54); // push r12
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x55); // push r13
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE4); // mov r12, rsp
|
||||
@@ -2555,6 +2757,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
public void Pump(CpuContext callerContext, string reason)
|
||||
{
|
||||
_ = callerContext;
|
||||
if (_guestTeardownRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var runSynchronously = string.Equals(reason, "entry_return", StringComparison.Ordinal);
|
||||
WakeExpiredBlockedGuestThreads();
|
||||
if (Volatile.Read(ref _readyGuestThreadCount) == 0)
|
||||
@@ -3079,6 +3285,45 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
continuationThread.Join();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parks every guest worker thread before the caller starts freeing executable
|
||||
/// memory. Workers unwind to the host at their next import dispatch (see the
|
||||
/// teardown check in DispatchImport); this waits for their host threads to
|
||||
/// finish within <paramref name="timeoutMs"/>. Returns false when at least one
|
||||
/// worker is still running — the caller must then leak its executable stubs
|
||||
/// rather than free memory a live thread may still execute.
|
||||
/// </summary>
|
||||
private bool RequestGuestThreadTeardown(int timeoutMs)
|
||||
{
|
||||
_guestTeardownRequested = true;
|
||||
Thread[] hostThreads;
|
||||
lock (_guestThreadGate)
|
||||
{
|
||||
_readyGuestThreads.Clear();
|
||||
Interlocked.Exchange(ref _readyGuestThreadCount, 0);
|
||||
hostThreads = _guestThreads.Values
|
||||
.Select(static thread => thread.HostThread)
|
||||
.Where(static host => host is not null && host != Thread.CurrentThread && host.IsAlive)
|
||||
.Cast<Thread>()
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
var deadline = Environment.TickCount64 + timeoutMs;
|
||||
var allStopped = true;
|
||||
foreach (var host in hostThreads)
|
||||
{
|
||||
var remaining = (int)Math.Max(1L, deadline - Environment.TickCount64);
|
||||
if (!host.Join(remaining) && host.IsAlive)
|
||||
{
|
||||
allStopped = false;
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Guest worker host thread '{host.Name}' still running after teardown wait.");
|
||||
}
|
||||
}
|
||||
|
||||
return allStopped;
|
||||
}
|
||||
|
||||
private void ClearGuestThreads()
|
||||
{
|
||||
GuestContinuationRunner[] runners;
|
||||
@@ -3358,6 +3603,13 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
ApplyGuestThreadAffinity(thread.AffinityMask);
|
||||
Volatile.Write(ref thread.HostThreadId, unchecked((int)GetCurrentThreadId()));
|
||||
_activeGuestThreadState = thread;
|
||||
if (LogThreadMode)
|
||||
{
|
||||
_threadModeCycleId = Interlocked.Increment(ref _threadModeCycleCounter);
|
||||
TraceThreadMode(
|
||||
$"cycle_start name='{thread.Name}' guest=0x{thread.ThreadHandle:X16} reason={reason} " +
|
||||
$"rsp_slot=0x{(ulong)TlsGetValue(_hostRspSlotTlsIndex):X}");
|
||||
}
|
||||
try
|
||||
{
|
||||
LastError = null;
|
||||
@@ -3432,6 +3684,13 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (LogThreadMode)
|
||||
{
|
||||
TraceThreadMode(
|
||||
$"cycle_end name='{thread.Name}' state={thread.State} " +
|
||||
$"imports={Interlocked.Read(ref thread.ImportCount)} " +
|
||||
$"rsp_slot=0x{(ulong)TlsGetValue(_hostRspSlotTlsIndex):X}");
|
||||
}
|
||||
_activeGuestThreadState = previousGuestThreadState;
|
||||
Volatile.Write(ref thread.HostThreadId, 0);
|
||||
GuestThreadExecution.RestoreGuestThread(previousGuestThreadHandle);
|
||||
@@ -3506,173 +3765,175 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
var previousYieldRequested = _activeGuestThreadYieldRequested;
|
||||
var previousYieldReason = _activeGuestThreadYieldReason;
|
||||
nint previousHostRspSlotValue = TlsGetValue(_hostRspSlotTlsIndex);
|
||||
if (LogThreadMode)
|
||||
{
|
||||
TraceThreadMode(
|
||||
$"entry_setup name='{name}' entry=0x{entryPoint:X16} stub=0x{(ulong)ptr:X16} " +
|
||||
$"guest_rsp=0x{context[CpuRegister.Rsp]:X16} rsp_slot_prev=0x{(ulong)previousHostRspSlotValue:X}");
|
||||
}
|
||||
try
|
||||
{
|
||||
_activeExecutionBackend = this;
|
||||
_activeCpuContext = context;
|
||||
_activeEntryReturnSentinelRip = 0;
|
||||
_activeGuestReturnSlotAddress = 0;
|
||||
_activeForcedGuestExit = false;
|
||||
_activeGuestThreadYieldRequested = false;
|
||||
_activeGuestThreadYieldReason = null;
|
||||
BindTlsBase(context);
|
||||
byte* ptr2 = (byte*)ptr;
|
||||
ulong hostRspSlot = (ulong)ptr + stubSize - 16uL;
|
||||
int offset = 0;
|
||||
ptr2[offset++] = 83;
|
||||
ptr2[offset++] = 85;
|
||||
ptr2[offset++] = 87;
|
||||
ptr2[offset++] = 86;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 84;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 85;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 86;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 87;
|
||||
EmitHostNonvolatileXmmSave(ptr2, ref offset);
|
||||
ptr2[offset++] = 73;
|
||||
ptr2[offset++] = 186;
|
||||
*(ulong*)(ptr2 + offset) = hostRspSlot;
|
||||
offset += 8;
|
||||
ptr2[offset++] = 73;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 34;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rsp];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 196;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 131;
|
||||
ptr2[offset++] = 236;
|
||||
ptr2[offset++] = 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 189;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rbp];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rdi];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 199;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rsi];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 198;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rdx];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 194;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rcx];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 193;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = entryPoint;
|
||||
offset += 8;
|
||||
ptr2[offset++] = byte.MaxValue;
|
||||
ptr2[offset++] = 208;
|
||||
int sentinelOffset = offset + 4;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 131;
|
||||
ptr2[offset++] = 196;
|
||||
ptr2[offset++] = 8;
|
||||
ptr2[offset++] = 73;
|
||||
ptr2[offset++] = 186;
|
||||
*(ulong*)(ptr2 + offset) = hostRspSlot;
|
||||
offset += 8;
|
||||
ptr2[offset++] = 73;
|
||||
ptr2[offset++] = 139;
|
||||
ptr2[offset++] = 34;
|
||||
EmitHostNonvolatileXmmRestore(ptr2, ref offset);
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 95;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 94;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 93;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 92;
|
||||
ptr2[offset++] = 94;
|
||||
ptr2[offset++] = 95;
|
||||
ptr2[offset++] = 93;
|
||||
ptr2[offset++] = 91;
|
||||
ptr2[offset++] = 195;
|
||||
ulong sentinel = (ulong)ptr + (ulong)sentinelOffset;
|
||||
ActiveEntryReturnSentinelRip = (ulong)_guestReturnStub;
|
||||
_activeGuestReturnSlotAddress = context[CpuRegister.Rsp] - 16uL;
|
||||
if (!context.TryWriteUInt64(context[CpuRegister.Rsp], sentinel))
|
||||
{
|
||||
reason = $"failed to patch guest thread return sentinel at 0x{context[CpuRegister.Rsp]:X16}";
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
uint oldProtect = default(uint);
|
||||
if (!VirtualProtect(ptr, stubSize, 64u, &oldProtect))
|
||||
{
|
||||
reason = $"VirtualProtect failed for guest thread entry stub at 0x{(nint)ptr:X16}";
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
||||
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
||||
ActiveGuestThreadYieldRequested = false;
|
||||
ActiveGuestThreadYieldReason = null;
|
||||
try
|
||||
{
|
||||
var nativeReturn = CallNativeEntry(ptr);
|
||||
if (ActiveGuestThreadYieldRequested)
|
||||
{
|
||||
reason = ActiveGuestThreadYieldReason ?? "guest thread blocked";
|
||||
return GuestNativeCallExitReason.Blocked;
|
||||
}
|
||||
if (ActiveForcedGuestExit)
|
||||
{
|
||||
reason = LastError ?? "guest thread forced exit";
|
||||
return GuestNativeCallExitReason.ForcedExit;
|
||||
}
|
||||
reason = $"returned 0x{nativeReturn:X8}";
|
||||
return GuestNativeCallExitReason.Returned;
|
||||
}
|
||||
catch (AccessViolationException ex)
|
||||
{
|
||||
reason = "access violation: " + ex.Message;
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
reason = ex.GetType().Name + ": " + ex.Message;
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
TlsSetValue(_hostRspSlotTlsIndex, previousHostRspSlotValue);
|
||||
RestoreActiveExecutionThread(
|
||||
previousActiveBackend,
|
||||
previousActiveContext,
|
||||
previousSentinel,
|
||||
previousReturnSlotAddress,
|
||||
previousForcedExit,
|
||||
previousYieldRequested,
|
||||
previousYieldReason);
|
||||
VirtualFree(ptr, 0u, 32768u);
|
||||
}
|
||||
}
|
||||
{
|
||||
_activeExecutionBackend = this;
|
||||
_activeCpuContext = context;
|
||||
_activeEntryReturnSentinelRip = 0;
|
||||
_activeGuestReturnSlotAddress = 0;
|
||||
_activeForcedGuestExit = false;
|
||||
_activeGuestThreadYieldRequested = false;
|
||||
_activeGuestThreadYieldReason = null;
|
||||
BindTlsBase(context);
|
||||
byte* ptr2 = (byte*)ptr;
|
||||
ulong hostRspSlot = (ulong)ptr + stubSize - 16uL;
|
||||
int offset = 0;
|
||||
ptr2[offset++] = 83;
|
||||
ptr2[offset++] = 85;
|
||||
ptr2[offset++] = 87;
|
||||
ptr2[offset++] = 86;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 84;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 85;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 86;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 87;
|
||||
EmitHostNonvolatileXmmSave(ptr2, ref offset);
|
||||
ptr2[offset++] = 73;
|
||||
ptr2[offset++] = 186;
|
||||
*(ulong*)(ptr2 + offset) = hostRspSlot;
|
||||
offset += 8;
|
||||
ptr2[offset++] = 73;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 34;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rsp];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 196;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 131;
|
||||
ptr2[offset++] = 236;
|
||||
ptr2[offset++] = 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 189;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rbp];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rdi];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 199;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rsi];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 198;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rdx];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 194;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rcx];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 193;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = entryPoint;
|
||||
offset += 8;
|
||||
ptr2[offset++] = byte.MaxValue;
|
||||
ptr2[offset++] = 208;
|
||||
int sentinelOffset = offset + 4;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 131;
|
||||
ptr2[offset++] = 196;
|
||||
ptr2[offset++] = 8;
|
||||
ptr2[offset++] = 73;
|
||||
ptr2[offset++] = 186;
|
||||
*(ulong*)(ptr2 + offset) = hostRspSlot;
|
||||
offset += 8;
|
||||
ptr2[offset++] = 73;
|
||||
ptr2[offset++] = 139;
|
||||
ptr2[offset++] = 34;
|
||||
EmitHostNonvolatileXmmRestore(ptr2, ref offset);
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 95;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 94;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 93;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 92;
|
||||
ptr2[offset++] = 94;
|
||||
ptr2[offset++] = 95;
|
||||
ptr2[offset++] = 93;
|
||||
ptr2[offset++] = 91;
|
||||
ptr2[offset++] = 195;
|
||||
ulong sentinel = (ulong)ptr + (ulong)sentinelOffset;
|
||||
ActiveEntryReturnSentinelRip = (ulong)_guestReturnStub;
|
||||
_activeGuestReturnSlotAddress = context[CpuRegister.Rsp] - 16uL;
|
||||
if (!context.TryWriteUInt64(context[CpuRegister.Rsp], sentinel))
|
||||
{
|
||||
reason = $"failed to patch guest thread return sentinel at 0x{context[CpuRegister.Rsp]:X16}";
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
uint oldProtect = default(uint);
|
||||
if (!VirtualProtect(ptr, stubSize, 64u, &oldProtect))
|
||||
{
|
||||
reason = $"VirtualProtect failed for guest thread entry stub at 0x{(nint)ptr:X16}";
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
||||
ActiveGuestThreadYieldRequested = false;
|
||||
ActiveGuestThreadYieldReason = null;
|
||||
var nativeReturn = RunGuestEntryStub(ptr, hostRspSlot);
|
||||
if (ActiveGuestThreadYieldRequested)
|
||||
{
|
||||
reason = ActiveGuestThreadYieldReason ?? "guest thread blocked";
|
||||
return GuestNativeCallExitReason.Blocked;
|
||||
}
|
||||
if (ActiveForcedGuestExit)
|
||||
{
|
||||
reason = LastError ?? "guest thread forced exit";
|
||||
return GuestNativeCallExitReason.ForcedExit;
|
||||
}
|
||||
reason = $"returned 0x{nativeReturn:X8}";
|
||||
return GuestNativeCallExitReason.Returned;
|
||||
}
|
||||
catch (AccessViolationException ex)
|
||||
{
|
||||
reason = "access violation: " + ex.Message;
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
reason = ex.GetType().Name + ": " + ex.Message;
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
finally
|
||||
{
|
||||
TlsSetValue(_hostRspSlotTlsIndex, previousHostRspSlotValue);
|
||||
RestoreActiveExecutionThread(
|
||||
previousActiveBackend,
|
||||
previousActiveContext,
|
||||
previousSentinel,
|
||||
previousReturnSlotAddress,
|
||||
previousForcedExit,
|
||||
previousYieldRequested,
|
||||
previousYieldReason);
|
||||
VirtualFree(ptr, 0u, 32768u);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe GuestNativeCallExitReason ExecuteGuestContinuationEntry(
|
||||
CpuContext context,
|
||||
@@ -3702,6 +3963,12 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
var previousYieldRequested = _activeGuestThreadYieldRequested;
|
||||
var previousYieldReason = _activeGuestThreadYieldReason;
|
||||
nint previousHostRspSlotValue = TlsGetValue(_hostRspSlotTlsIndex);
|
||||
if (LogThreadMode)
|
||||
{
|
||||
TraceThreadMode(
|
||||
$"continuation_setup name='{name}' resume=0x{entryPoint:X16} stub=0x{(ulong)ptr:X16} " +
|
||||
$"guest_rsp=0x{context[CpuRegister.Rsp]:X16} rsp_slot_prev=0x{(ulong)previousHostRspSlotValue:X}");
|
||||
}
|
||||
try
|
||||
{
|
||||
_activeExecutionBackend = this;
|
||||
@@ -3770,12 +4037,12 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
||||
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
||||
ActiveGuestThreadYieldRequested = false;
|
||||
ActiveGuestThreadYieldReason = null;
|
||||
|
||||
try
|
||||
{
|
||||
var nativeReturn = CallNativeEntry(ptr);
|
||||
var nativeReturn = RunGuestEntryStub(ptr, hostRspSlot);
|
||||
if (ActiveGuestThreadYieldRequested)
|
||||
{
|
||||
reason = ActiveGuestThreadYieldReason ?? "guest thread blocked";
|
||||
@@ -4051,7 +4318,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
int num6 = -1;
|
||||
try
|
||||
{
|
||||
num6 = CallNativeEntry(ptr);
|
||||
num6 = RunGuestEntryStub(ptr, num2);
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Guest returned: {num6}");
|
||||
PumpUntilGuestThreadsIdle(context, "entry_return");
|
||||
}
|
||||
@@ -4078,6 +4345,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
LastError = "Detected repeating import loop and forced guest unwind to host.";
|
||||
}
|
||||
Console.Error.WriteLine("[LOADER][ERROR] " + LastError);
|
||||
RequestGuestThreadTeardown(3000);
|
||||
return false;
|
||||
}
|
||||
if (num6 == 0)
|
||||
@@ -4092,6 +4360,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
LastError = $"Guest entry point returned non-zero: {num6}";
|
||||
}
|
||||
Console.Error.WriteLine("[LOADER][ERROR] " + LastError);
|
||||
RequestGuestThreadTeardown(3000);
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
@@ -4234,13 +4503,14 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
ulong rsp = cpuContext[CpuRegister.Rsp];
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] Stall snapshot: rip=0x{cpuContext.Rip:X16} rsp=0x{rsp:X16} rbp=0x{cpuContext[CpuRegister.Rbp]:X16} rax=0x{cpuContext[CpuRegister.Rax]:X16} rbx=0x{cpuContext[CpuRegister.Rbx]:X16} rcx=0x{cpuContext[CpuRegister.Rcx]:X16} rdx=0x{cpuContext[CpuRegister.Rdx]:X16} rsi=0x{cpuContext[CpuRegister.Rsi]:X16} rdi=0x{cpuContext[CpuRegister.Rdi]:X16}");
|
||||
ulong num = cpuContext.Rip & 0xFFFFFFFFFFFFFFF0uL;
|
||||
for (int i = 0; i < _importEntries.Length; i++)
|
||||
var importEntries = _importEntries;
|
||||
for (int i = 0; i < importEntries.Length; i++)
|
||||
{
|
||||
if (_importEntries[i].Address != num)
|
||||
if (importEntries[i].Address != num)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
string text = _importEntries[i].Nid;
|
||||
string text = importEntries[i].Nid;
|
||||
if (_moduleManager.TryGetExport(text, out ExportedFunction export))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] Stall import-stub: rip=0x{num:X16} nid={text} -> {export.LibraryName}:{export.Name}");
|
||||
@@ -4416,9 +4686,22 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
public unsafe void Dispose()
|
||||
{
|
||||
if (!RequestGuestThreadTeardown(2000))
|
||||
{
|
||||
// A guest worker is still executing native code; freeing the trampolines,
|
||||
// exception-handler stubs, or GC handles under it turns process exit into
|
||||
// an execute-AV / CLR fatal. Leak them — the process is going away anyway.
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Skipping executable stub teardown: guest worker threads are still running.");
|
||||
return;
|
||||
}
|
||||
// Native guest workers park idle once every guest thread has unwound; stop
|
||||
// them before any executable stub or TLS index they reference is freed.
|
||||
DisposeNativeGuestExecutors();
|
||||
ClearImportHandlerTrampolines();
|
||||
_importEntries = Array.Empty<ImportStubEntry>();
|
||||
_runtimeSymbolsByName.Clear();
|
||||
ResetLazyDlsymStubState();
|
||||
_importNidHashCache.Clear();
|
||||
StopStallWatchdog();
|
||||
if (_exceptionHandler != 0)
|
||||
|
||||
@@ -140,7 +140,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
LastMilestoneLog = null;
|
||||
KernelModuleRegistry.Reset();
|
||||
var image = LoadImage(normalizedEbootPath);
|
||||
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
|
||||
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version, BuildInfo.CommitSha);
|
||||
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
|
||||
LogAppBundleInfo(normalizedEbootPath, image);
|
||||
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
|
||||
|
||||
@@ -65,7 +65,10 @@
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
},
|
||||
"sharpemu.hle": {
|
||||
"type": "Project"
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"SharpEmu.Logging": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.libs": {
|
||||
"type": "Project",
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using Avalonia;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Templates;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Platform;
|
||||
using Avalonia.Threading;
|
||||
using System.Collections.Specialized;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
public sealed class ConsoleWindow : Window
|
||||
{
|
||||
private readonly AvaloniaList<LogLine> _sourceLines;
|
||||
private readonly AvaloniaList<LogLine> _visibleLines = new();
|
||||
private readonly ListBox _list;
|
||||
private readonly TextBox _searchBox;
|
||||
private readonly CheckBox _autoScrollCheck;
|
||||
|
||||
public ConsoleWindow(
|
||||
AvaloniaList<LogLine> lines,
|
||||
Action clear,
|
||||
bool autoScroll)
|
||||
{
|
||||
_sourceLines = lines;
|
||||
Title = "SharpEmu Console";
|
||||
Width = 980;
|
||||
Height = 620;
|
||||
MinWidth = 520;
|
||||
MinHeight = 320;
|
||||
Background = new SolidColorBrush(Color.Parse("#0D1017"));
|
||||
Icon = new WindowIcon(AssetLoader.Open(new Uri("avares://SharpEmu.GUI/Assets/SharpEmu.ico")));
|
||||
|
||||
_searchBox = new TextBox { Watermark = "Search...", Width = 320, Margin = new Thickness(0, 0, 12, 0) };
|
||||
_autoScrollCheck = new CheckBox
|
||||
{
|
||||
Content = "Auto-scroll",
|
||||
IsChecked = autoScroll,
|
||||
FontSize = 12,
|
||||
Margin = new Thickness(0, 0, 12, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
var copyButton = new Button
|
||||
{
|
||||
Classes = { "ghost" },
|
||||
Content = "Copy",
|
||||
Padding = new Thickness(10, 4),
|
||||
Margin = new Thickness(0, 0, 8, 0),
|
||||
};
|
||||
var clearButton = new Button { Classes = { "ghost" }, Content = "Clear", Padding = new Thickness(10, 4) };
|
||||
copyButton.Click += async (_, _) => await CopyAsync();
|
||||
clearButton.Click += (_, _) => clear();
|
||||
_searchBox.TextChanged += (_, _) => RefreshVisibleLines();
|
||||
|
||||
_list = new ListBox
|
||||
{
|
||||
Classes = { "console" },
|
||||
ItemsSource = _visibleLines,
|
||||
BorderThickness = new Thickness(1),
|
||||
BorderBrush = new SolidColorBrush(Color.Parse("#232B3A")),
|
||||
ItemTemplate = new FuncDataTemplate<LogLine>((_, _) =>
|
||||
{
|
||||
var text = new TextBlock { TextWrapping = TextWrapping.NoWrap };
|
||||
text.Bind(TextBlock.TextProperty, new Binding(nameof(LogLine.Text)));
|
||||
text.Bind(TextBlock.ForegroundProperty, new Binding(nameof(LogLine.Brush)));
|
||||
return text;
|
||||
}),
|
||||
};
|
||||
|
||||
Content = new Grid
|
||||
{
|
||||
Margin = new Thickness(12),
|
||||
RowDefinitions = new RowDefinitions("Auto,*"),
|
||||
Children =
|
||||
{
|
||||
new Grid
|
||||
{
|
||||
Margin = new Thickness(0, 0, 0, 8),
|
||||
ColumnDefinitions = new ColumnDefinitions("*,Auto,Auto,Auto,Auto"),
|
||||
Children =
|
||||
{
|
||||
new TextBlock
|
||||
{
|
||||
Classes = { "sectionTitle" },
|
||||
Text = "CONSOLE",
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
},
|
||||
_searchBox.WithGridColumn(1),
|
||||
_autoScrollCheck.WithGridColumn(2),
|
||||
copyButton.WithGridColumn(3),
|
||||
clearButton.WithGridColumn(4),
|
||||
},
|
||||
},
|
||||
_list.WithGridRow(1),
|
||||
},
|
||||
};
|
||||
|
||||
lines.CollectionChanged += OnLinesChanged;
|
||||
Closed += (_, _) => lines.CollectionChanged -= OnLinesChanged;
|
||||
RefreshVisibleLines();
|
||||
}
|
||||
|
||||
private void OnLinesChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
RefreshVisibleLines();
|
||||
if (_autoScrollCheck.IsChecked == true)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() => (_list.Scroll as ScrollViewer)?.ScrollToEnd());
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshVisibleLines()
|
||||
{
|
||||
var query = _searchBox.Text ?? string.Empty;
|
||||
_visibleLines.Clear();
|
||||
_visibleLines.AddRange(string.IsNullOrWhiteSpace(query)
|
||||
? _sourceLines
|
||||
: _sourceLines.Where(line => line.Text.Contains(query, StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
|
||||
private async Task CopyAsync()
|
||||
{
|
||||
if (_visibleLines.Count == 0 || Clipboard is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await Clipboard.SetTextAsync(string.Join(Environment.NewLine, _visibleLines.Select(line => line.Text)));
|
||||
}
|
||||
}
|
||||
|
||||
file static class GridExtensions
|
||||
{
|
||||
public static T WithGridColumn<T>(this T control, int column) where T : Control
|
||||
{
|
||||
Grid.SetColumn(control, column);
|
||||
return control;
|
||||
}
|
||||
|
||||
public static T WithGridRow<T>(this T control, int row) where T : Control
|
||||
{
|
||||
Grid.SetRow(control, row);
|
||||
return control;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -23,14 +23,35 @@ 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>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");
|
||||
|
||||
@@ -14,7 +14,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
ExtendClientAreaToDecorationsHint="True"
|
||||
ExtendClientAreaChromeHints="PreferSystemChrome"
|
||||
ExtendClientAreaTitleBarHeightHint="44"
|
||||
Icon="avares://SharpEmu.GUI/Assets/SharpEmu.ico">
|
||||
Icon="avares://SharpEmu.GUI/Assets/SharpEmu.ico"
|
||||
KeyDown="OnKeyDown">
|
||||
|
||||
<Grid RowDefinitions="44,*,32">
|
||||
|
||||
@@ -159,13 +160,17 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<Border Grid.Row="2" x:Name="ConsolePanel" Classes="card" Padding="0" Height="240"
|
||||
Margin="0,12,0,0" IsVisible="False">
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto" Margin="16,12,16,8">
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto,Auto,Auto" Margin="16,12,16,8">
|
||||
<TextBlock Classes="sectionTitle" Text="CONSOLE" VerticalAlignment="Center" />
|
||||
<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"
|
||||
<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"
|
||||
@@ -252,10 +257,23 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<TextBlock Classes="fieldLabel" Text="Log to file" />
|
||||
<ToggleSwitch x:Name="LogToFileToggle" OnContent="On" OffContent="Off" />
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<StackPanel Margin="0,0,24,0">
|
||||
<TextBlock Classes="fieldLabel" Text="Log file path" />
|
||||
<Button x:Name="SelectLogFilePathButton" Classes="ghost" Content="Select…"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,24,0">
|
||||
<TextBlock Classes="fieldLabel" Text="Override log file" />
|
||||
<ToggleSwitch x:Name="OverrideLogFileToggle" OnContent="On" OffContent="Off" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,24,0">
|
||||
<TextBlock Classes="fieldLabel" Text="Title music" />
|
||||
<ToggleSwitch x:Name="TitleMusicToggle" OnContent="On" OffContent="Off" IsChecked="True" />
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Classes="fieldLabel" Text="Discord presence" />
|
||||
<ToggleSwitch x:Name="DiscordToggle" OnContent="On" OffContent="Off" />
|
||||
</StackPanel>
|
||||
</WrapPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Diagnostics;
|
||||
using Avalonia;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Platform;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.Threading;
|
||||
using Avalonia.VisualTree;
|
||||
using SharpEmu.Libs.Pad;
|
||||
using SharpEmu.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
@@ -33,17 +35,26 @@ public partial class MainWindow : Window
|
||||
|
||||
private readonly List<GameEntry> _allGames = new();
|
||||
private readonly ObservableCollection<GameEntry> _visibleGames = new();
|
||||
private readonly ObservableCollection<LogLine> _consoleLines = new();
|
||||
private readonly AvaloniaList<LogLine> _consoleLines = new();
|
||||
private readonly List<LogLine> _allConsoleLines = new();
|
||||
private readonly ConcurrentQueue<(string Line, bool IsError)> _pendingLines = new();
|
||||
private readonly DispatcherTimer _consoleFlushTimer;
|
||||
|
||||
private GuiSettings _settings = new();
|
||||
private EmulatorProcess? _emulator;
|
||||
private ConsoleWindow? _consoleWindow;
|
||||
private StreamWriter? _fileLog;
|
||||
private readonly SndPreviewPlayer _sndPreview = new();
|
||||
private string? _emulatorExePath;
|
||||
private bool _isRunning;
|
||||
private int _autoScrollTicks;
|
||||
|
||||
// Discord Rich Presence state.
|
||||
private readonly long _launcherStartUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
private DiscordRichPresence? _discord;
|
||||
private string? _runningGameName;
|
||||
private string? _runningGameTitleId;
|
||||
private long _runningSinceUnixSeconds;
|
||||
private int _detailLoadGeneration;
|
||||
private int _backdropGeneration;
|
||||
|
||||
@@ -77,17 +88,27 @@ public partial class MainWindow : Window
|
||||
GameList.SelectionChanged += (_, _) => UpdateSelectedGame();
|
||||
GameList.DoubleTapped += (_, _) => LaunchSelected();
|
||||
SearchBox.TextChanged += (_, _) => RefreshVisibleGames();
|
||||
ConsoleSearchBox.TextChanged += (_, _) => RefreshVisibleConsoleLines();
|
||||
AddFolderButton.Click += async (_, _) => await AddFolderAsync();
|
||||
EmptyAddFolderButton.Click += async (_, _) => await AddFolderAsync();
|
||||
RescanButton.Click += async (_, _) => await RescanLibraryAsync();
|
||||
OpenFileButton.Click += async (_, _) => await OpenFileAsync();
|
||||
LaunchButton.Click += (_, _) => LaunchSelected();
|
||||
StopButton.Click += (_, _) => _emulator?.Stop();
|
||||
ClearLogButton.Click += (_, _) => { _consoleLines.Clear(); _allConsoleLines.Clear(); };
|
||||
StopButton.Click += (_, _) => StopEmulator();
|
||||
ClearLogButton.Click += (_, _) => _consoleLines.Clear();
|
||||
CopyLogButton.Click += async (_, _) => await CopyConsoleAsync();
|
||||
DetachConsoleButton.Click += (_, _) => ShowConsoleWindow();
|
||||
OptionsToggle.IsCheckedChanged += (_, _) => OptionsPanel.IsVisible = OptionsToggle.IsChecked == true;
|
||||
ConsoleToggle.IsCheckedChanged += (_, _) => ConsolePanel.IsVisible = ConsoleToggle.IsChecked == true;
|
||||
ConsoleToggle.IsCheckedChanged += (_, _) => ConsolePanel.IsVisible = ConsoleToggle.IsChecked == true && _consoleWindow is null;
|
||||
SelectLogFilePathButton.Click += async (_, _) => await SelectFilePathAsync();
|
||||
TitleMusicToggle.IsCheckedChanged += (_, _) => OnTitleMusicToggled();
|
||||
DiscordToggle.IsCheckedChanged += (_, _) =>
|
||||
{
|
||||
_settings.DiscordRichPresence = DiscordToggle.IsChecked == true;
|
||||
UpdateDiscordPresence();
|
||||
};
|
||||
|
||||
GameList.AddHandler(ContextRequestedEvent, OnGameContextRequested, RoutingStrategies.Tunnel);
|
||||
CtxLaunch.Click += (_, _) => LaunchSelected();
|
||||
@@ -164,7 +185,7 @@ public partial class MainWindow : Window
|
||||
|
||||
if ((pressed & 0x2000) != 0) // Circle
|
||||
{
|
||||
_emulator?.Stop();
|
||||
StopEmulator();
|
||||
}
|
||||
|
||||
_previousPadButtons = pad.Buttons;
|
||||
@@ -235,9 +256,73 @@ public partial class MainWindow : Window
|
||||
_settings = GuiSettings.Load();
|
||||
ApplySettingsToControls();
|
||||
LocateEmulator();
|
||||
UpdateDiscordPresence();
|
||||
await RescanLibraryAsync();
|
||||
}
|
||||
|
||||
// ---- Discord Rich Presence ----
|
||||
|
||||
/// <summary>
|
||||
/// Publishes the launcher state to Discord: browsing while idle, the
|
||||
/// running game (with elapsed time) during emulation. No-ops when
|
||||
/// disabled or when no Discord application ID is configured.
|
||||
/// </summary>
|
||||
private void UpdateDiscordPresence()
|
||||
{
|
||||
if (!_settings.DiscordRichPresence || _settings.DiscordClientId.Length == 0)
|
||||
{
|
||||
_discord?.Dispose();
|
||||
_discord = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_discord ??= new DiscordRichPresence(_settings.DiscordClientId);
|
||||
if (_isRunning && _runningGameName is { } gameName)
|
||||
{
|
||||
_discord.SetPresence(
|
||||
$"Playing {gameName}",
|
||||
_runningGameTitleId,
|
||||
_runningSinceUnixSeconds);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Discord does not render activities without timestamps, so the
|
||||
// browsing state carries the launcher's start time.
|
||||
_discord.SetPresence(
|
||||
"Browsing the library",
|
||||
$"{_allGames.Count} game(s)",
|
||||
_launcherStartUnixSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnKeyDown(object sender, KeyEventArgs args)
|
||||
{
|
||||
args.Handled = true;
|
||||
switch (args.Key)
|
||||
{
|
||||
case Key.F11:
|
||||
OnWindowFullScreen(this, new RoutedEventArgs());
|
||||
break;
|
||||
default:
|
||||
args.Handled = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnWindowFullScreen(object sender, RoutedEventArgs args)
|
||||
{
|
||||
if (WindowState == WindowState.FullScreen)
|
||||
{
|
||||
WindowState = WindowState.Normal;
|
||||
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.PreferSystemChrome;
|
||||
}
|
||||
else
|
||||
{
|
||||
WindowState = WindowState.FullScreen;
|
||||
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.NoChrome;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnWindowClosing()
|
||||
{
|
||||
ReadControlsIntoSettings();
|
||||
@@ -245,6 +330,8 @@ public partial class MainWindow : Window
|
||||
_consoleFlushTimer.Stop();
|
||||
_gamepadTimer.Stop();
|
||||
_sndPreview.Stop();
|
||||
_discord?.Dispose();
|
||||
_consoleWindow?.Close();
|
||||
_emulator?.Dispose();
|
||||
DropFileLog();
|
||||
}
|
||||
@@ -274,7 +361,10 @@ public partial class MainWindow : Window
|
||||
TraceImportsBox.Value = Math.Clamp(_settings.ImportTraceLimit, 0, 4096);
|
||||
StrictToggle.IsChecked = _settings.StrictDynlibResolution;
|
||||
LogToFileToggle.IsChecked = _settings.LogToFile;
|
||||
OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile;
|
||||
TitleMusicToggle.IsChecked = _settings.PlayTitleMusic;
|
||||
ToolTip.SetTip(SelectLogFilePathButton, string.IsNullOrWhiteSpace(_settings.LogFilePath) ? "No path selected" : _settings.LogFilePath);
|
||||
DiscordToggle.IsChecked = _settings.DiscordRichPresence;
|
||||
}
|
||||
|
||||
private void ReadControlsIntoSettings()
|
||||
@@ -283,7 +373,9 @@ public partial class MainWindow : Window
|
||||
_settings.ImportTraceLimit = (int)(TraceImportsBox.Value ?? 0);
|
||||
_settings.StrictDynlibResolution = StrictToggle.IsChecked == true;
|
||||
_settings.LogToFile = LogToFileToggle.IsChecked == true;
|
||||
_settings.OverrideLogFile = OverrideLogFileToggle.IsChecked == true;
|
||||
_settings.PlayTitleMusic = TitleMusicToggle.IsChecked == true;
|
||||
_settings.DiscordRichPresence = DiscordToggle.IsChecked == true;
|
||||
}
|
||||
|
||||
private string SelectedLogLevel()
|
||||
@@ -382,6 +474,7 @@ public partial class MainWindow : Window
|
||||
_allGames.AddRange(games);
|
||||
RefreshVisibleGames();
|
||||
LoadGameDetailsInBackground(games);
|
||||
UpdateDiscordPresence();
|
||||
StatusBarRight.Text = folders.Length == 0
|
||||
? "Add a game folder to populate the library."
|
||||
: $"Library scanned: {games.Count} game(s) in {folders.Length} folder(s).";
|
||||
@@ -968,27 +1061,56 @@ public partial class MainWindow : Window
|
||||
arguments.Add($"--trace-imports={_settings.ImportTraceLimit}");
|
||||
}
|
||||
|
||||
arguments.Add(ebootPath);
|
||||
|
||||
_consoleLines.Clear();
|
||||
ConsoleToggle.IsChecked = true;
|
||||
|
||||
// Mirror everything the console pane shows into a log file for the
|
||||
// duration of the run, regardless of the emulator's log level.
|
||||
// Let the CLI mirror stdout/stderr itself; it sees loader/native
|
||||
// diagnostics before the GUI pipe reader can filter or batch them.
|
||||
DropFileLog();
|
||||
if (_settings.LogToFile && BuildLogFilePath(titleId) is { } logFilePath)
|
||||
if (_settings.LogToFile)
|
||||
{
|
||||
try
|
||||
string filePath;
|
||||
if (!string.IsNullOrWhiteSpace(_settings.LogFilePath))
|
||||
{
|
||||
_fileLog = new StreamWriter(logFilePath, append: false);
|
||||
AppendConsoleLine($"Log file: {logFilePath}", DimLineBrush);
|
||||
if (_settings.OverrideLogFile)
|
||||
{
|
||||
filePath = _settings.LogFilePath;
|
||||
}
|
||||
else
|
||||
{
|
||||
string path = _settings.LogFilePath;
|
||||
string id = string.IsNullOrWhiteSpace(titleId) ? "UNKNOWN" : titleId;
|
||||
foreach (var invalidChar in Path.GetInvalidFileNameChars())
|
||||
{
|
||||
id = id.Replace(invalidChar.ToString(), "");
|
||||
}
|
||||
string identifier = $"{id}-{DateTime.Now:yyyyMMdd-HHmmss}";
|
||||
|
||||
string? dir = Path.GetDirectoryName(path);
|
||||
string? fileName = Path.GetFileNameWithoutExtension(path);
|
||||
string? extension = Path.GetExtension(path);
|
||||
|
||||
string newFileName = $"{fileName}-{identifier}{extension}";
|
||||
filePath = string.IsNullOrEmpty(dir)
|
||||
? newFileName
|
||||
: Path.Combine(dir, newFileName);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
else
|
||||
{
|
||||
AppendConsoleLine($"Could not open the log file: {ex.Message}", WarningLineBrush);
|
||||
filePath = BuildLogFilePath(titleId) ?? string.Empty;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
arguments.Add("--log-file");
|
||||
arguments.Add(filePath);
|
||||
AppendConsoleLine($"Log file: {filePath}", DimLineBrush);
|
||||
}
|
||||
}
|
||||
|
||||
arguments.Add(ebootPath);
|
||||
|
||||
AppendConsoleLine($"$ SharpEmu {string.Join(' ', arguments)}", DimLineBrush);
|
||||
|
||||
var emulator = new EmulatorProcess();
|
||||
@@ -1009,10 +1131,38 @@ public partial class MainWindow : Window
|
||||
|
||||
_emulator = emulator;
|
||||
_isRunning = true;
|
||||
_runningGameName = displayName;
|
||||
_runningGameTitleId = _allGames
|
||||
.FirstOrDefault(game => game.Path.Equals(ebootPath, StringComparison.OrdinalIgnoreCase))?
|
||||
.TitleId;
|
||||
_runningSinceUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
StatusDot.Fill = SuccessLineBrush;
|
||||
StatusText.Text = $"Running — {displayName}";
|
||||
StatusBarRight.Text = $"Running {displayName}";
|
||||
UpdateRunButtons();
|
||||
UpdateDiscordPresence();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the running game and updates status/presence immediately. The
|
||||
/// process-exit path still runs when the corpse is collected, but a game
|
||||
/// wedged in a GPU driver call can keep its process alive for a long
|
||||
/// time after termination — the launcher should not look (or tell
|
||||
/// Discord it is) "playing" during that window.
|
||||
/// </summary>
|
||||
private void StopEmulator()
|
||||
{
|
||||
if (!_isRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_emulator?.Stop();
|
||||
_runningGameName = null;
|
||||
_runningGameTitleId = null;
|
||||
StatusText.Text = "Stopping…";
|
||||
StatusBarRight.Text = "Stopping…";
|
||||
UpdateDiscordPresence();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1069,7 +1219,10 @@ public partial class MainWindow : Window
|
||||
StatusDot.Fill = exitCode == 0 ? (IBrush)SuccessLineBrush : ErrorLineBrush;
|
||||
StatusText.Text = $"Exited with code {exitCode} ({meaning})";
|
||||
StatusBarRight.Text = "Idle";
|
||||
_runningGameName = null;
|
||||
_runningGameTitleId = null;
|
||||
UpdateRunButtons();
|
||||
UpdateDiscordPresence();
|
||||
}
|
||||
|
||||
private void UpdateRunButtons()
|
||||
@@ -1079,6 +1232,27 @@ public partial class MainWindow : Window
|
||||
OpenFileButton.IsEnabled = !_isRunning;
|
||||
}
|
||||
|
||||
private async Task SelectFilePathAsync()
|
||||
{
|
||||
SaveFilePickerResult result = await StorageProvider.SaveFilePickerWithResultAsync(new FilePickerSaveOptions
|
||||
{
|
||||
Title = "Select where to save the Log file",
|
||||
SuggestedFileName = "SharpEmuLog",
|
||||
DefaultExtension = "log",
|
||||
FileTypeChoices =
|
||||
[
|
||||
new FilePickerFileType("Plain Text Files") { Patterns = ["*.txt"] },
|
||||
new FilePickerFileType("Log Files") { Patterns = ["*.log"] }
|
||||
]
|
||||
});
|
||||
|
||||
if (result.File is not null)
|
||||
{
|
||||
_settings.LogFilePath = result.File.Path.LocalPath;
|
||||
ToolTip.SetTip(SelectLogFilePathButton, _settings.LogFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Console ----
|
||||
|
||||
private void FlushPendingConsoleLines()
|
||||
@@ -1097,27 +1271,28 @@ public partial class MainWindow : Window
|
||||
|
||||
FlushFileLog();
|
||||
|
||||
if (incoming.Count >= MaxConsoleLines)
|
||||
_allConsoleLines.AddRange(incoming);
|
||||
|
||||
string query = ConsoleSearchBox.Text ?? string.Empty;
|
||||
|
||||
IEnumerable<LogLine> linesToAdd = incoming;
|
||||
if (!string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
// A burst larger than the cap: keep only the newest lines.
|
||||
_consoleLines.Clear();
|
||||
for (var i = incoming.Count - MaxConsoleLines; i < incoming.Count; i++)
|
||||
{
|
||||
_consoleLines.Add(incoming[i]);
|
||||
}
|
||||
linesToAdd = incoming.Where(line =>
|
||||
line.Text != null &&
|
||||
line.Text.Contains(query, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
else
|
||||
_consoleLines.AddRange(linesToAdd);
|
||||
|
||||
var overflow = _consoleLines.Count - MaxConsoleLines;
|
||||
while (_allConsoleLines.Count > MaxConsoleLines)
|
||||
{
|
||||
var overflow = _consoleLines.Count + incoming.Count - MaxConsoleLines;
|
||||
for (var i = 0; i < overflow; i++)
|
||||
var droppedLine = _allConsoleLines[0];
|
||||
_allConsoleLines.RemoveAt(0);
|
||||
if (_consoleLines.Count > 0 && _consoleLines[0] == droppedLine)
|
||||
{
|
||||
_consoleLines.RemoveAt(0);
|
||||
}
|
||||
|
||||
foreach (var line in incoming)
|
||||
{
|
||||
_consoleLines.Add(line);
|
||||
}
|
||||
}
|
||||
|
||||
_autoScrollTicks = 3;
|
||||
@@ -1127,11 +1302,50 @@ public partial class MainWindow : Window
|
||||
{
|
||||
WriteFileLog(text);
|
||||
FlushFileLog();
|
||||
_consoleLines.Add(new LogLine(text, brush));
|
||||
|
||||
var line = new LogLine(text, brush);
|
||||
_allConsoleLines.Add(line);
|
||||
|
||||
string query = ConsoleSearchBox.Text ?? string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(query) || (text != null && text.Contains(query, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
_consoleLines.Add(line);
|
||||
}
|
||||
|
||||
while (_allConsoleLines.Count > MaxConsoleLines)
|
||||
{
|
||||
var droppedLine = _allConsoleLines[0];
|
||||
_allConsoleLines.RemoveAt(0);
|
||||
if (_consoleLines.Count > 0 && _consoleLines[0] == droppedLine)
|
||||
{
|
||||
_consoleLines.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
_autoScrollTicks = 3;
|
||||
MaybeAutoScroll();
|
||||
}
|
||||
|
||||
private void RefreshVisibleConsoleLines()
|
||||
{
|
||||
string query = ConsoleSearchBox.Text ?? string.Empty;
|
||||
|
||||
_consoleLines.Clear();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
_consoleLines.AddRange(_allConsoleLines);
|
||||
}
|
||||
else
|
||||
{
|
||||
var filtered = _allConsoleLines.Where(line =>
|
||||
line.Text != null &&
|
||||
line.Text.Contains(query, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
_consoleLines.AddRange(filtered);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Console-to-file mirroring ----
|
||||
|
||||
private void WriteFileLog(string text)
|
||||
@@ -1253,4 +1467,28 @@ public partial class MainWindow : Window
|
||||
var text = string.Join(Environment.NewLine, _consoleLines.Select(line => line.Text));
|
||||
await Clipboard.SetTextAsync(text);
|
||||
}
|
||||
|
||||
private void ShowConsoleWindow()
|
||||
{
|
||||
if (_consoleWindow is { } window)
|
||||
{
|
||||
window.Activate();
|
||||
return;
|
||||
}
|
||||
|
||||
ConsoleSearchBox.Text = string.Empty;
|
||||
ConsoleToggle.IsChecked = false;
|
||||
ConsolePanel.IsVisible = false;
|
||||
_consoleWindow = new ConsoleWindow(
|
||||
_consoleLines,
|
||||
() => { _consoleLines.Clear(); _allConsoleLines.Clear(); },
|
||||
AutoScrollCheck.IsChecked == true);
|
||||
_consoleWindow.Closed += (_, _) =>
|
||||
{
|
||||
_consoleWindow = null;
|
||||
ConsoleToggle.IsChecked = true;
|
||||
ConsolePanel.IsVisible = true;
|
||||
};
|
||||
_consoleWindow.Show(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,11 +3,14 @@
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
using SharpEmu.Logging;
|
||||
|
||||
namespace SharpEmu.HLE;
|
||||
|
||||
public sealed class ModuleManager : IModuleManager
|
||||
{
|
||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("HLE");
|
||||
|
||||
private readonly ConcurrentDictionary<string, Delegate> _dispatchTable = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, ExportedFunction> _exportTable = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, ExportedFunction> _exportNameTable = new(StringComparer.Ordinal);
|
||||
@@ -47,7 +50,7 @@ public sealed class ModuleManager : IModuleManager
|
||||
var handler = CreateHandler(type, method, instances);
|
||||
if (!_dispatchTable.TryAdd(exportInfo.Value.Nid, handler))
|
||||
{
|
||||
Console.Error.WriteLine($"[HLE] Duplicate NID '{exportInfo.Value.Nid}' ({exportInfo.Value.ExportName}) — already registered, skipping.");
|
||||
Log.Warning($"Duplicate NID '{exportInfo.Value.Nid}' ({exportInfo.Value.ExportName}) — already registered, skipping.");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -106,7 +109,7 @@ public sealed class ModuleManager : IModuleManager
|
||||
|
||||
if (!_dispatchTable.TryGetValue(nid, out var function) || !_exportTable.TryGetValue(nid, out var export))
|
||||
{
|
||||
Console.Error.WriteLine($"[HLE] NID '{nid}' not found in dispatch table.");
|
||||
Log.Warning($"NID '{nid}' not found in dispatch table.");
|
||||
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
return false;
|
||||
@@ -114,7 +117,7 @@ public sealed class ModuleManager : IModuleManager
|
||||
|
||||
if ((export.Target & context.TargetGeneration) == 0)
|
||||
{
|
||||
Console.Error.WriteLine($"[HLE] NID '{nid}' ({export.Name}) found but not implemented for generation {context.TargetGeneration} (targets: {export.Target}).");
|
||||
Log.Warning($"NID '{nid}' ({export.Name}) found but not implemented for generation {context.TargetGeneration} (targets: {export.Target}).");
|
||||
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
|
||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED;
|
||||
return false;
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+329
-107
@@ -179,7 +179,6 @@ public static class AgcExports
|
||||
private static long _createShaderTraceCount;
|
||||
private static long _packetPayloadTraceCount;
|
||||
private static bool _tracedMissingPixelShaderBindings;
|
||||
private static long _unsatisfiedWaitTraceCount;
|
||||
private static long _shaderTranslationMissTraceCount;
|
||||
private static long _translatedDrawTraceCount;
|
||||
private static long _standardDmaTraceCount;
|
||||
@@ -2001,6 +2000,57 @@ public static class AgcExports
|
||||
lock (gpuState.Gate)
|
||||
{
|
||||
ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets);
|
||||
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// ABI (reversed from Quake): rdi = array of DCB base addresses (u64 each),
|
||||
// rsi = array of DCB sizes in dwords (u32 each), rdx = buffer count.
|
||||
[SysAbiExport(
|
||||
Nid = "6UzEidRZwkg",
|
||||
ExportName = "sceAgcDriverSubmitMultiDcbs",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAgcDriver")]
|
||||
public static int DriverSubmitMultiDcbs(CpuContext ctx)
|
||||
{
|
||||
var addressArray = ctx[CpuRegister.Rdi];
|
||||
var sizeArray = ctx[CpuRegister.Rsi];
|
||||
var bufferCount = (uint)ctx[CpuRegister.Rdx];
|
||||
if (addressArray == 0 || sizeArray == 0 || bufferCount == 0 || bufferCount > 4096)
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var tracePackets = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"), "1", StringComparison.Ordinal);
|
||||
|
||||
var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
||||
lock (gpuState.Gate)
|
||||
{
|
||||
for (uint i = 0; i < bufferCount; i++)
|
||||
{
|
||||
if (!ctx.TryReadUInt64(addressArray + i * 8, out var commandAddress) ||
|
||||
commandAddress == 0 ||
|
||||
!ctx.TryReadUInt32(sizeArray + i * 4, out var dwordCount) ||
|
||||
dwordCount == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tracePackets)
|
||||
{
|
||||
TraceAgc(
|
||||
$"agc.driver_submit_multi_dcbs index={i}/{bufferCount} " +
|
||||
$"addr=0x{commandAddress:X16} dwords={dwordCount}");
|
||||
}
|
||||
|
||||
ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets);
|
||||
}
|
||||
|
||||
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
@@ -2049,6 +2099,7 @@ public static class AgcExports
|
||||
}
|
||||
|
||||
ParseSubmittedDcb(ctx, gpuState, queueState, commandAddress, dwordCount, tracePackets);
|
||||
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
@@ -2170,6 +2221,63 @@ public static class AgcExports
|
||||
return ReturnPointer(ctx, commandAddress);
|
||||
}
|
||||
|
||||
// WAIT_REG_MEM packets whose condition is not met suspend their DCB into
|
||||
// GpuWaitRegistry. Each submit re-checks every suspended DCB against current guest
|
||||
// memory (labels are advanced by ReleaseMem/WriteData/DmaData packets or by direct
|
||||
// CPU writes) and resumes the ones whose condition is now satisfied. A resumed DCB
|
||||
// can itself write labels that unblock others, so loop until a fixed point.
|
||||
private static void DrainResumableDcbs(
|
||||
CpuContext ctx,
|
||||
SubmittedGpuState gpuState,
|
||||
bool tracePackets)
|
||||
{
|
||||
for (var pass = 0; pass < 256; pass++)
|
||||
{
|
||||
var woken = GpuWaitRegistry.CollectSatisfied((address, is64Bit) =>
|
||||
{
|
||||
if (is64Bit)
|
||||
{
|
||||
return ctx.TryReadUInt64(address, out var value64)
|
||||
? value64
|
||||
: (ulong?)null;
|
||||
}
|
||||
|
||||
return ctx.TryReadUInt32(address, out var value32)
|
||||
? value32
|
||||
: (ulong?)null;
|
||||
});
|
||||
if (woken is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var waiter in woken)
|
||||
{
|
||||
var remainingDwords = waiter.TotalDwords - waiter.ResumeOffset;
|
||||
if (remainingDwords == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tracePackets)
|
||||
{
|
||||
TraceAgc(
|
||||
$"agc.dcb.resumed addr=0x{waiter.WaitAddress:X16} " +
|
||||
$"resume=0x{waiter.ResumeAddress:X16} dwords={remainingDwords}");
|
||||
}
|
||||
|
||||
var state = waiter.State as SubmittedDcbState ?? gpuState.Graphics;
|
||||
ParseSubmittedDcb(
|
||||
ctx,
|
||||
gpuState,
|
||||
state,
|
||||
waiter.ResumeAddress,
|
||||
remainingDwords,
|
||||
tracePackets);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ParseSubmittedDcb(
|
||||
CpuContext ctx,
|
||||
SubmittedGpuState gpuState,
|
||||
@@ -2201,7 +2309,6 @@ public static class AgcExports
|
||||
$"agc.dcb.packet dw={offset} addr=0x{currentAddress:X16} " +
|
||||
$"header=0x{header:X8} len=1 type=2");
|
||||
}
|
||||
|
||||
offset++;
|
||||
continue;
|
||||
}
|
||||
@@ -2311,16 +2418,91 @@ public static class AgcExports
|
||||
state.InstanceCount = Math.Max(instanceCount, 1);
|
||||
}
|
||||
|
||||
if (op == ItNop &&
|
||||
register is RWaitMem32 or RWaitMem64 &&
|
||||
// WAIT_REG_MEM (AGC NOP-encapsulated 32/64-bit variants): when the condition is
|
||||
// not yet satisfied, suspend this DCB; DrainResumableDcbs resumes it once the
|
||||
// watched memory advances.
|
||||
if (op == ItNop && register is RWaitMem32 or RWaitMem64 &&
|
||||
length >= (register == RWaitMem32 ? 6u : 9u))
|
||||
{
|
||||
ObserveSubmittedWaitRegMem(ctx, currentAddress, register == RWaitMem64, tracePackets);
|
||||
if (TryParseWaitRegMem(ctx, currentAddress, register == RWaitMem64,
|
||||
out var waitAddr, out var refVal, out var waitMask, out var cmpFunc))
|
||||
{
|
||||
ulong curVal = 0;
|
||||
bool hasCurVal;
|
||||
if (register == RWaitMem64)
|
||||
{
|
||||
hasCurVal = ctx.TryReadUInt64(waitAddr, out curVal);
|
||||
}
|
||||
else if (ctx.TryReadUInt32(waitAddr, out var curVal32))
|
||||
{
|
||||
curVal = curVal32;
|
||||
hasCurVal = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
hasCurVal = false;
|
||||
}
|
||||
|
||||
var waiter = new GpuWaitRegistry.WaitingDcb
|
||||
{
|
||||
CommandBufferAddress = commandAddress,
|
||||
ResumeAddress = currentAddress + ((ulong)length * sizeof(uint)),
|
||||
TotalDwords = dwordCount,
|
||||
ResumeOffset = offset + length,
|
||||
ReferenceValue = refVal,
|
||||
Mask = waitMask,
|
||||
CompareFunction = cmpFunc,
|
||||
Is64Bit = register == RWaitMem64,
|
||||
State = state,
|
||||
};
|
||||
if (hasCurVal && !GpuWaitRegistry.Compare(waiter, curVal))
|
||||
{
|
||||
GpuWaitRegistry.Register(waitAddr, waiter);
|
||||
|
||||
if (tracePackets)
|
||||
{
|
||||
TraceAgc(
|
||||
$"agc.dcb.suspended addr=0x{waitAddr:X16} ref=0x{refVal:X16} " +
|
||||
$"mask=0x{waitMask:X16} cur=0x{curVal:X16} cmp={cmpFunc}");
|
||||
}
|
||||
|
||||
return; // suspend parsing of this DCB
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (op == ItWaitRegMem && length >= 7)
|
||||
{
|
||||
ObserveSubmittedStandardWaitRegMem(ctx, currentAddress, tracePackets);
|
||||
if (TryParseStandardWaitRegMem(ctx, currentAddress,
|
||||
out var waitAddr, out var refVal, out var waitMask, out var cmpFunc) &&
|
||||
ctx.TryReadUInt32(waitAddr, out var curVal))
|
||||
{
|
||||
var waiter = new GpuWaitRegistry.WaitingDcb
|
||||
{
|
||||
CommandBufferAddress = commandAddress,
|
||||
ResumeAddress = currentAddress + ((ulong)length * sizeof(uint)),
|
||||
TotalDwords = dwordCount,
|
||||
ResumeOffset = offset + length,
|
||||
ReferenceValue = refVal,
|
||||
Mask = waitMask,
|
||||
CompareFunction = cmpFunc,
|
||||
Is64Bit = false,
|
||||
State = state,
|
||||
};
|
||||
if (!GpuWaitRegistry.Compare(waiter, curVal))
|
||||
{
|
||||
GpuWaitRegistry.Register(waitAddr, waiter);
|
||||
|
||||
if (tracePackets)
|
||||
{
|
||||
TraceAgc(
|
||||
$"agc.dcb.suspended_std addr=0x{waitAddr:X16} ref=0x{refVal:X16} " +
|
||||
$"mask=0x{waitMask:X16} cur=0x{curVal:X16} cmp={cmpFunc}");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (TryReadSubmittedDrawCount(
|
||||
@@ -2641,104 +2823,6 @@ public static class AgcExports
|
||||
}
|
||||
}
|
||||
|
||||
private static void ObserveSubmittedWaitRegMem(
|
||||
CpuContext ctx,
|
||||
ulong packetAddress,
|
||||
bool is64Bit,
|
||||
bool tracePacket)
|
||||
{
|
||||
if (!ctx.TryReadUInt64(packetAddress + 4, out var address) ||
|
||||
!ctx.TryReadUInt32(packetAddress + (is64Bit ? 28u : 16u), out var control))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ulong mask;
|
||||
ulong reference;
|
||||
ulong value;
|
||||
if (is64Bit)
|
||||
{
|
||||
if (!ctx.TryReadUInt64(packetAddress + 12, out mask) ||
|
||||
!ctx.TryReadUInt64(packetAddress + 20, out reference) ||
|
||||
!ctx.TryReadUInt64(address, out value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!ctx.TryReadUInt32(packetAddress + 12, out var mask32) ||
|
||||
!ctx.TryReadUInt32(packetAddress + 20, out var reference32) ||
|
||||
!ctx.TryReadUInt32(address, out var value32))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
mask = mask32;
|
||||
reference = reference32;
|
||||
value = value32;
|
||||
}
|
||||
|
||||
var compareFunction = control & 0xFFu;
|
||||
TraceSubmittedWait(
|
||||
address,
|
||||
value,
|
||||
mask,
|
||||
reference,
|
||||
compareFunction,
|
||||
is64Bit ? 64 : 32,
|
||||
tracePacket);
|
||||
}
|
||||
|
||||
private static void ObserveSubmittedStandardWaitRegMem(
|
||||
CpuContext ctx,
|
||||
ulong packetAddress,
|
||||
bool tracePacket)
|
||||
{
|
||||
if (!ctx.TryReadUInt32(packetAddress + 4, out var control) ||
|
||||
!ctx.TryReadUInt64(packetAddress + 8, out var address) ||
|
||||
!ctx.TryReadUInt32(packetAddress + 16, out var reference) ||
|
||||
!ctx.TryReadUInt32(packetAddress + 20, out var mask) ||
|
||||
!ctx.TryReadUInt32(address, out var value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TraceSubmittedWait(address, value, mask, reference, control & 0x7u, 32, tracePacket);
|
||||
}
|
||||
|
||||
private static void TraceSubmittedWait(
|
||||
ulong address,
|
||||
ulong value,
|
||||
ulong mask,
|
||||
ulong reference,
|
||||
uint compareFunction,
|
||||
int bits,
|
||||
bool tracePacket)
|
||||
{
|
||||
var maskedValue = value & mask;
|
||||
var satisfied = compareFunction switch
|
||||
{
|
||||
0 => false,
|
||||
1 => maskedValue < reference,
|
||||
2 => maskedValue <= reference,
|
||||
3 => maskedValue == reference,
|
||||
4 => maskedValue != reference,
|
||||
5 => maskedValue >= reference,
|
||||
6 => maskedValue > reference,
|
||||
_ => true,
|
||||
};
|
||||
if (!tracePacket && (satisfied || !ShouldTraceHotPath(ref _unsatisfiedWaitTraceCount)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TraceAgc(
|
||||
$"agc.dcb.wait_reg_mem bits={bits} addr=0x{address:X16} " +
|
||||
$"value=0x{value:X16} mask=0x{mask:X16} ref=0x{reference:X16} " +
|
||||
$"compare={compareFunction} satisfied={satisfied}");
|
||||
}
|
||||
|
||||
private static void ApplySubmittedReleaseMem(
|
||||
CpuContext ctx,
|
||||
ulong packetAddress,
|
||||
@@ -2756,10 +2840,11 @@ public static class AgcExports
|
||||
var dataSelection = (control >> 16) & 0xFFu;
|
||||
var destinationAddress = ((ulong)destinationHi << 32) | destinationLo;
|
||||
var data = ((ulong)dataHi << 32) | dataLo;
|
||||
|
||||
var wroteData = dataSelection switch
|
||||
{
|
||||
1 or 2 => ctx.TryWriteUInt32(destinationAddress, dataLo),
|
||||
3 => ctx.TryWriteUInt64(destinationAddress, data),
|
||||
1 => ctx.TryWriteUInt32(destinationAddress, dataLo),
|
||||
2 or 3 => ctx.TryWriteUInt64(destinationAddress, data),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
@@ -2855,6 +2940,11 @@ public static class AgcExports
|
||||
case ItDrawIndex2 when packetLength >= 6:
|
||||
state.DrawIndexOffset = 0;
|
||||
return ctx.TryReadUInt32(packetAddress + 16, out drawCount);
|
||||
// 5-dword form emitted by DcbDrawIndex (count at +4, ItIndexBase/
|
||||
// ItIndexBufferSize carried by separate preceding packets).
|
||||
case ItDrawIndex2 when packetLength >= 5:
|
||||
state.DrawIndexOffset = 0;
|
||||
return ctx.TryReadUInt32(packetAddress + 4, out drawCount);
|
||||
case ItDrawIndexOffset2 when packetLength >= 5:
|
||||
if (!ctx.TryReadUInt32(packetAddress + 8, out var indexOffset))
|
||||
{
|
||||
@@ -3206,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,
|
||||
@@ -5635,4 +5775,86 @@ public static class AgcExports
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] agc.create_shader dst=0x{destinationAddress:X16} header=0x{headerAddress:X16} code=0x{codeAddress:X16} {detail}");
|
||||
}
|
||||
|
||||
// Packet layouts mirror what DcbWaitRegMem emits.
|
||||
// 32-bit (ItNop/RWaitMem32): +4 addrLo, +8 addrHi, +12 mask, +16 cmp|op<<8, +20 ref.
|
||||
// 64-bit (ItNop/RWaitMem64): +4 addrLo, +8 addrHi, +12 maskLo, +16 maskHi,
|
||||
// +20 refLo, +24 refHi, +28 cmp|op<<8, +32 poll.
|
||||
private static bool TryParseWaitRegMem(
|
||||
CpuContext ctx,
|
||||
ulong addr,
|
||||
bool is64,
|
||||
out ulong waitAddr,
|
||||
out ulong refVal,
|
||||
out ulong mask,
|
||||
out uint cmpFunc)
|
||||
{
|
||||
waitAddr = refVal = mask = 0;
|
||||
cmpFunc = 0;
|
||||
|
||||
if (!ctx.TryReadUInt32(addr + 4, out var lo) ||
|
||||
!ctx.TryReadUInt32(addr + 8, out var hi))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
waitAddr = ((ulong)hi << 32) | lo;
|
||||
if (!is64)
|
||||
{
|
||||
if (!ctx.TryReadUInt32(addr + 12, out var mask32) ||
|
||||
!ctx.TryReadUInt32(addr + 16, out var cmpRaw) ||
|
||||
!ctx.TryReadUInt32(addr + 20, out var refVal32))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
mask = mask32;
|
||||
refVal = refVal32;
|
||||
cmpFunc = cmpRaw & 0x7;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt32(addr + 12, out var maskLo) ||
|
||||
!ctx.TryReadUInt32(addr + 16, out var maskHi) ||
|
||||
!ctx.TryReadUInt32(addr + 20, out var refLo) ||
|
||||
!ctx.TryReadUInt32(addr + 24, out var refHi) ||
|
||||
!ctx.TryReadUInt32(addr + 28, out var cmpRaw64))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
mask = ((ulong)maskHi << 32) | maskLo;
|
||||
refVal = ((ulong)refHi << 32) | refLo;
|
||||
cmpFunc = cmpRaw64 & 0x7;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Standard ItWaitRegMem (7 dwords, 32-bit compare):
|
||||
// +4 cmp|(op&1)<<8, +8 addrLo, +12 addrHi, +16 ref, +20 mask, +24 poll.
|
||||
private static bool TryParseStandardWaitRegMem(
|
||||
CpuContext ctx,
|
||||
ulong addr,
|
||||
out ulong waitAddr,
|
||||
out ulong refVal,
|
||||
out ulong mask,
|
||||
out uint cmpFunc)
|
||||
{
|
||||
waitAddr = refVal = mask = 0;
|
||||
cmpFunc = 0;
|
||||
|
||||
if (!ctx.TryReadUInt32(addr + 4, out var cmpRaw) ||
|
||||
!ctx.TryReadUInt32(addr + 8, out var lo) ||
|
||||
!ctx.TryReadUInt32(addr + 12, out var hi) ||
|
||||
!ctx.TryReadUInt32(addr + 16, out var reference) ||
|
||||
!ctx.TryReadUInt32(addr + 20, out var mask32))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
cmpFunc = cmpRaw & 0x7;
|
||||
waitAddr = ((ulong)hi << 32) | lo;
|
||||
refVal = reference;
|
||||
mask = mask32;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
|
||||
// Holds DCBs whose parsing was suspended on an unsatisfied WAIT_REG_MEM condition.
|
||||
// AgcExports re-checks every waiter against guest memory on each submit and resumes
|
||||
// the ones whose condition became true (labels are advanced by ReleaseMem/WriteData/
|
||||
// DmaData packets or by direct CPU writes).
|
||||
internal static class GpuWaitRegistry
|
||||
{
|
||||
public struct WaitingDcb
|
||||
{
|
||||
public ulong CommandBufferAddress;
|
||||
public ulong ResumeAddress;
|
||||
public uint TotalDwords;
|
||||
public uint ResumeOffset;
|
||||
public ulong WaitAddress;
|
||||
public ulong ReferenceValue;
|
||||
public ulong Mask;
|
||||
public uint CompareFunction;
|
||||
public bool Is64Bit;
|
||||
public object? State;
|
||||
}
|
||||
|
||||
private static readonly object _gate = new();
|
||||
private static readonly Dictionary<ulong, List<WaitingDcb>> _waiters = new();
|
||||
|
||||
public static void Register(ulong address, WaitingDcb waiter)
|
||||
{
|
||||
waiter.WaitAddress = address;
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_waiters.TryGetValue(address, out var list))
|
||||
{
|
||||
list = new List<WaitingDcb>();
|
||||
_waiters.Add(address, list);
|
||||
}
|
||||
|
||||
list.Add(waiter);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-evaluates every registered waiter. readValue receives (address, is64Bit) and
|
||||
// returns null when the memory is unreadable; such waiters are kept registered.
|
||||
public static List<WaitingDcb>? CollectSatisfied(Func<ulong, bool, ulong?> readValue)
|
||||
{
|
||||
List<WaitingDcb>? woken = null;
|
||||
lock (_gate)
|
||||
{
|
||||
List<ulong>? emptied = null;
|
||||
foreach (var (address, list) in _waiters)
|
||||
{
|
||||
for (var i = list.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var value = readValue(address, list[i].Is64Bit);
|
||||
if (value is null || !Compare(list[i], value.Value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
woken ??= new List<WaitingDcb>();
|
||||
woken.Add(list[i]);
|
||||
list.RemoveAt(i);
|
||||
}
|
||||
|
||||
if (list.Count == 0)
|
||||
{
|
||||
emptied ??= new List<ulong>();
|
||||
emptied.Add(address);
|
||||
}
|
||||
}
|
||||
|
||||
if (emptied != null)
|
||||
{
|
||||
foreach (var address in emptied)
|
||||
{
|
||||
_waiters.Remove(address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return woken;
|
||||
}
|
||||
|
||||
public static bool Compare(in WaitingDcb waiter, ulong value)
|
||||
{
|
||||
var masked = value & waiter.Mask;
|
||||
return waiter.CompareFunction switch
|
||||
{
|
||||
1 => masked < waiter.ReferenceValue,
|
||||
2 => masked <= waiter.ReferenceValue,
|
||||
3 => masked == waiter.ReferenceValue,
|
||||
4 => masked != waiter.ReferenceValue,
|
||||
5 => masked >= waiter.ReferenceValue,
|
||||
6 => masked > waiter.ReferenceValue,
|
||||
// 0 is "always" in the PM4 encoding and 7 is reserved; treating both as
|
||||
// satisfied keeps a malformed packet from suspending its DCB forever.
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -267,19 +267,53 @@ public static class AmprExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath))
|
||||
ulong bytesRead = 0;
|
||||
|
||||
// Unregistered/missing files are zero-filled instead of failing: games queue
|
||||
// speculative reads and only consume the bytes on success paths.
|
||||
if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath) || !File.Exists(hostPath))
|
||||
{
|
||||
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead: 0, hostPath, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
if (destination != 0 && size > 0)
|
||||
{
|
||||
int chunkSize = (int)Math.Min(size, 4096);
|
||||
Span<byte> zeros = stackalloc byte[chunkSize];
|
||||
zeros.Clear();
|
||||
while (bytesRead < size)
|
||||
{
|
||||
int currentChunk = (int)Math.Min((ulong)chunkSize, size - bytesRead);
|
||||
if (!ctx.Memory.TryWrite(destination + bytesRead, zeros[..currentChunk]))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
bytesRead += (ulong)currentChunk;
|
||||
}
|
||||
}
|
||||
|
||||
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, "(missing)", (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
var result = TryReadFileToGuestMemory(ctx, hostPath, fileOffset, destination, size, out var bytesRead);
|
||||
// Offset -1 means "continue after the previous read of this file id".
|
||||
if (fileOffset == unchecked((ulong)(long)-1))
|
||||
{
|
||||
fileOffset = PakDirectoryTracker.ResolveSequentialOffset(fileId, size);
|
||||
}
|
||||
else if (fileOffset > long.MaxValue)
|
||||
{
|
||||
fileOffset = 0;
|
||||
}
|
||||
|
||||
var result = TryReadFileToGuestMemory(ctx, hostPath, fileOffset, destination, size, out bytesRead);
|
||||
if (result != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, hostPath, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination, fileOffset, bytesRead);
|
||||
|
||||
if (!AppendReadFileRecord(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Ampr;
|
||||
|
||||
/// <summary>
|
||||
/// sceAmprAprCommandBufferReadFile carries -1 instead of an absolute offset whenever the guest
|
||||
/// wants "the next sequential chunk" of a file. For an id Software PACK archive that sequence is
|
||||
/// always header -> directory table -> individual archived files, and the guest never tells us
|
||||
/// which archived file it wants next - only the byte count it expects, taken from the very
|
||||
/// directory entry it already parsed on its side. This tracks a per-fileId read cursor, and once
|
||||
/// the directory table has streamed past, parses it so later reads can be matched back to their
|
||||
/// real absolute offset by the size the guest asks for.
|
||||
/// </summary>
|
||||
internal static class PakDirectoryTracker
|
||||
{
|
||||
private const int DirectoryEntrySize = 64;
|
||||
private const int DirectoryEntryNameLength = 56;
|
||||
|
||||
private static readonly bool _trace =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal) ||
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR_READS"), "1", StringComparison.Ordinal);
|
||||
|
||||
private sealed class DirectoryEntry(string name, uint filePos, uint fileLen)
|
||||
{
|
||||
public string Name { get; } = name;
|
||||
public uint FilePos { get; } = filePos;
|
||||
public uint FileLen { get; } = fileLen;
|
||||
public bool Consumed { get; set; }
|
||||
}
|
||||
|
||||
private sealed class FileState
|
||||
{
|
||||
public ulong NextOffset;
|
||||
public bool ExpectingDirectory;
|
||||
public List<DirectoryEntry>? Directory;
|
||||
}
|
||||
|
||||
private static readonly ConcurrentDictionary<uint, FileState> _state = new();
|
||||
|
||||
/// <summary>
|
||||
/// Resolves what "read the next sequential chunk of size <paramref name="requestedSize"/>"
|
||||
/// really means for this fileId: an unconsumed directory entry whose length matches, if the
|
||||
/// directory has already been parsed, otherwise the tracked linear cursor.
|
||||
/// </summary>
|
||||
public static ulong ResolveSequentialOffset(uint fileId, ulong requestedSize)
|
||||
{
|
||||
var state = _state.GetOrAdd(fileId, static _ => new FileState());
|
||||
|
||||
if (state.Directory is { } entries)
|
||||
{
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (!entry.Consumed && entry.FileLen == requestedSize)
|
||||
{
|
||||
entry.Consumed = true;
|
||||
if (_trace)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] pak.directory_match: id=0x{fileId:X8} name='{entry.Name}' " +
|
||||
$"filepos=0x{entry.FilePos:X8} filelen=0x{entry.FileLen:X8}");
|
||||
}
|
||||
|
||||
return entry.FilePos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return state.NextOffset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Feeds back a completed read so the tracker can advance its cursor, recognize a PACK
|
||||
/// header and jump straight to its directory table, or parse the directory table itself
|
||||
/// once it streams past.
|
||||
/// </summary>
|
||||
public static void OnReadCompleted(CpuContext ctx, uint fileId, ulong destination, ulong fileOffset, ulong bytesRead)
|
||||
{
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var state = _state.GetOrAdd(fileId, static _ => new FileState());
|
||||
|
||||
if (state.ExpectingDirectory && fileOffset == state.NextOffset)
|
||||
{
|
||||
state.Directory = TryParseDirectory(ctx, destination, bytesRead);
|
||||
state.ExpectingDirectory = false;
|
||||
state.NextOffset = fileOffset + bytesRead;
|
||||
if (_trace)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] pak.directory_parsed: id=0x{fileId:X8} entries={state.Directory?.Count ?? 0}");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (fileOffset == 0 && bytesRead >= 12 && TryReadPackHeader(ctx, destination, out var dirOffset))
|
||||
{
|
||||
state.NextOffset = dirOffset;
|
||||
state.ExpectingDirectory = true;
|
||||
return;
|
||||
}
|
||||
|
||||
state.NextOffset = fileOffset + bytesRead;
|
||||
}
|
||||
|
||||
private static bool TryReadPackHeader(CpuContext ctx, ulong destination, out ulong dirOffset)
|
||||
{
|
||||
dirOffset = 0;
|
||||
Span<byte> header = stackalloc byte[12];
|
||||
if (!ctx.Memory.TryRead(destination, header) ||
|
||||
header[0] != (byte)'P' || header[1] != (byte)'A' || header[2] != (byte)'C' || header[3] != (byte)'K')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
dirOffset = BinaryPrimitives.ReadUInt32LittleEndian(header[4..8]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static List<DirectoryEntry>? TryParseDirectory(CpuContext ctx, ulong destination, ulong length)
|
||||
{
|
||||
var count = (int)(length / DirectoryEntrySize);
|
||||
if (count <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var entries = new List<DirectoryEntry>(count);
|
||||
Span<byte> record = stackalloc byte[DirectoryEntrySize];
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
if (!ctx.Memory.TryRead(destination + (ulong)(i * DirectoryEntrySize), record))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var nameBytes = record[..DirectoryEntryNameLength];
|
||||
var nullIndex = nameBytes.IndexOf((byte)0);
|
||||
var name = Encoding.ASCII.GetString(nullIndex >= 0 ? nameBytes[..nullIndex] : nameBytes);
|
||||
var filePos = BinaryPrimitives.ReadUInt32LittleEndian(record.Slice(56, 4));
|
||||
var fileLen = BinaryPrimitives.ReadUInt32LittleEndian(record.Slice(60, 4));
|
||||
entries.Add(new DirectoryEntry(name, filePos, fileLen));
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,42 @@ public static class AvPlayerExports
|
||||
return unchecked((int)handle);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "JdksQu8pNdQ",
|
||||
ExportName = "sceAvPlayerGetVideoDataEx",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAvPlayer")]
|
||||
public static int AvPlayerGetVideoDataEx(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// "UbQoYawOsfY" is sceAvPlayerIsActive, not sceAvPlayerGetVideoDataEx (the previous NID here
|
||||
// was wrong - verified by hashing both names against scripts/ps5_names.txt). No player is
|
||||
// ever actually active in this HLE implementation, so report false/inactive.
|
||||
[SysAbiExport(
|
||||
Nid = "UbQoYawOsfY",
|
||||
ExportName = "sceAvPlayerIsActive",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAvPlayer")]
|
||||
public static int AvPlayerIsActive(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Wnp1OVcrZgk",
|
||||
ExportName = "sceAvPlayerGetAudioData",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAvPlayer")]
|
||||
public static int AvPlayerGetAudioData(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "HD1YKVU26-M",
|
||||
ExportName = "sceAvPlayerPostInit",
|
||||
|
||||
@@ -8,7 +8,6 @@ namespace SharpEmu.Libs.CommonDialog;
|
||||
|
||||
public static class CommonDialogExports
|
||||
{
|
||||
private const int AlreadySystemInitialized = unchecked((int)0x80B80002);
|
||||
private static int _initialized;
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -18,11 +17,12 @@ public static class CommonDialogExports
|
||||
LibraryName = "libSceCommonDialog")]
|
||||
public static int CommonDialogInitialize(CpuContext ctx)
|
||||
{
|
||||
var result = Interlocked.Exchange(ref _initialized, 1) == 0
|
||||
? 0
|
||||
: AlreadySystemInitialized;
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
// Games can initialize the common-dialog service defensively from more than one
|
||||
// subsystem. Keeping this HLE initialization idempotent avoids turning an error
|
||||
// reporting path into a second, unrelated failure.
|
||||
Interlocked.Exchange(ref _initialized, 1);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
|
||||
@@ -8,8 +8,13 @@ namespace SharpEmu.Libs.CommonDialog;
|
||||
|
||||
public static class MsgDialogExports
|
||||
{
|
||||
private const int AlreadyInitialized = unchecked((int)0x80B80002);
|
||||
private const int StatusNone = 0;
|
||||
private const int StatusInitialized = 1;
|
||||
private const int StatusFinished = 3;
|
||||
private const int ResultSize = 0x20;
|
||||
|
||||
private static int _initialized;
|
||||
private static int _status;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "lDqxaY1UbEo",
|
||||
@@ -18,9 +23,162 @@ public static class MsgDialogExports
|
||||
LibraryName = "libSceMsgDialog")]
|
||||
public static int MsgDialogInitialize(CpuContext ctx)
|
||||
{
|
||||
var result = Interlocked.Exchange(ref _initialized, 1) == 0
|
||||
? 0
|
||||
: AlreadyInitialized;
|
||||
// Treat repeated initialization as success. The dialog service is process-global in
|
||||
// this HLE implementation and has no per-call resources to recreate.
|
||||
Interlocked.Exchange(ref _initialized, 1);
|
||||
Interlocked.Exchange(ref _status, StatusInitialized);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "ePw-kqZmelo",
|
||||
ExportName = "sceMsgDialogTerminate",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceMsgDialog")]
|
||||
public static int MsgDialogTerminate(CpuContext ctx)
|
||||
{
|
||||
Interlocked.Exchange(ref _initialized, 0);
|
||||
Interlocked.Exchange(ref _status, StatusNone);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "b06Hh0DPEaE",
|
||||
ExportName = "sceMsgDialogOpen",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceMsgDialog")]
|
||||
public static int MsgDialogOpen(CpuContext ctx)
|
||||
{
|
||||
LogDialogMessage(ctx, ctx[CpuRegister.Rdi]);
|
||||
|
||||
// There is no host popup to actually show. Complete immediately with "finished" so a
|
||||
// guest polling loop (GetStatus/UpdateStatus -> GetResult -> Close) sees a dismissed
|
||||
// dialog on its very first poll instead of spinning forever waiting for user input.
|
||||
Interlocked.Exchange(ref _status, StatusFinished);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
// Best-effort extraction of the dialog text so fatal-error popups are visible in the
|
||||
// log even though no host dialog is shown. The PS5 SceMsgDialogParam layout is not
|
||||
// fully known, so chase every qword in the struct that points at readable text - one
|
||||
// level deep, then a second level for nested sub-param structs.
|
||||
private static void LogDialogMessage(CpuContext ctx, ulong paramAddress)
|
||||
{
|
||||
if (paramAddress == 0)
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][INFO] sceMsgDialogOpen: param=NULL");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[LOADER][INFO] sceMsgDialogOpen: param=0x{paramAddress:X12}");
|
||||
|
||||
Span<byte> head = stackalloc byte[0xA0];
|
||||
if (ctx.Memory.TryRead(paramAddress, head))
|
||||
{
|
||||
DumpPointerStrings(ctx, paramAddress, head, "param", chaseNested: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DumpPointerStrings(CpuContext ctx, ulong baseAddress, ReadOnlySpan<byte> bytes, string label, bool chaseNested)
|
||||
{
|
||||
Span<byte> nested = stackalloc byte[0x40];
|
||||
for (var offset = 0; offset + 8 <= bytes.Length; offset += 8)
|
||||
{
|
||||
var value = System.Buffers.Binary.BinaryPrimitives.ReadUInt64LittleEndian(bytes[offset..]);
|
||||
if (value < 0x10000 || value == baseAddress)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var text = TryReadPrintableText(ctx, value);
|
||||
if (text is not null)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][INFO] {label}+0x{offset:X2} -> 0x{value:X12} text=\"{text}\"");
|
||||
}
|
||||
else if (chaseNested && ctx.Memory.TryRead(value, nested))
|
||||
{
|
||||
DumpPointerStrings(ctx, value, nested, $"{label}+0x{offset:X2}", chaseNested: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string? TryReadPrintableText(CpuContext ctx, ulong address)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[256];
|
||||
if (!ctx.Memory.TryRead(address, bytes))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var nullIndex = bytes.IndexOf((byte)0);
|
||||
if (nullIndex < 2)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var candidate = bytes[..nullIndex];
|
||||
foreach (var b in candidate)
|
||||
{
|
||||
if (b is < 0x20 or > 0x7E && b != (byte)'\n' && b != (byte)'\r' && b != (byte)'\t')
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return System.Text.Encoding.ASCII.GetString(candidate).Replace("\n", "\\n").Replace("\r", "\\r");
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "CWVW78Qc3fI",
|
||||
ExportName = "sceMsgDialogGetStatus",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceMsgDialog")]
|
||||
public static int MsgDialogGetStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "6fIC3XKt2k0",
|
||||
ExportName = "sceMsgDialogUpdateStatus",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceMsgDialog")]
|
||||
public static int MsgDialogUpdateStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Lr8ovHH9l6A",
|
||||
ExportName = "sceMsgDialogGetResult",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceMsgDialog")]
|
||||
public static int MsgDialogGetResult(CpuContext ctx)
|
||||
{
|
||||
var resultAddress = ctx[CpuRegister.Rdi];
|
||||
if (resultAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> result = stackalloc byte[ResultSize];
|
||||
result.Clear();
|
||||
if (!ctx.Memory.TryWrite(resultAddress, result))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "HTrcDKlFKuM",
|
||||
ExportName = "sceMsgDialogClose",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceMsgDialog")]
|
||||
public static int MsgDialogClose(CpuContext ctx)
|
||||
{
|
||||
Interlocked.Exchange(ref _status, StatusFinished);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ public static class CxaGuardExports
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "",
|
||||
Nid = "9rAeANT2tyE",
|
||||
ExportName = "__cxa_guard_release",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
@@ -142,7 +142,7 @@ public static class CxaGuardExports
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "",
|
||||
Nid = "2emaaluWzUw",
|
||||
ExportName = "__cxa_guard_abort",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
|
||||
@@ -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)];
|
||||
|
||||
@@ -73,6 +73,21 @@ public static class KernelExports
|
||||
LibraryName = "libKernel")]
|
||||
public static int Exit(CpuContext ctx) => RequestProcessExit(ctx, "exit");
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "L1SBTkC+Cvw",
|
||||
ExportName = "abort",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Abort(CpuContext ctx)
|
||||
{
|
||||
// Route through the same graceful guest-entry-exit path as exit(): letting the call
|
||||
// fall through to the host's native abort() does not unwind the guest thread cleanly.
|
||||
Console.Error.WriteLine("[LOADER][INFO] abort() called by guest - terminating");
|
||||
GuestThreadExecution.RequestCurrentEntryExit("abort", -1);
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "XKRegsFpEpk",
|
||||
ExportName = "catchReturnFromMain",
|
||||
@@ -263,20 +278,14 @@ public static class KernelExports
|
||||
ExportName = "pthread_create",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixPthreadCreate(CpuContext ctx)
|
||||
{
|
||||
return PthreadCreate(ctx);
|
||||
}
|
||||
public static int PosixPthreadCreate(CpuContext ctx) => PthreadCreate(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Jmi+9w9u0E4",
|
||||
ExportName = "pthread_create_name_np",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixPthreadCreateNameNp(CpuContext ctx)
|
||||
{
|
||||
return PthreadCreate(ctx);
|
||||
}
|
||||
public static int PosixPthreadCreateNameNp(CpuContext ctx) => PthreadCreate(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "3kg7rT0NQIs",
|
||||
@@ -353,10 +362,7 @@ public static class KernelExports
|
||||
ExportName = "pthread_join",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixPthreadJoin(CpuContext ctx)
|
||||
{
|
||||
return PthreadJoin(ctx);
|
||||
}
|
||||
public static int PosixPthreadJoin(CpuContext ctx) => PthreadJoin(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "wuCroIGjt2g",
|
||||
@@ -454,4 +460,67 @@ public static class KernelExports
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)status);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// Unidentified kernel NIDs observed at runtime; stubbed to return 0 until they are
|
||||
// resolved against scripts/ps5_names.txt.
|
||||
[SysAbiExport(
|
||||
Nid = "9T2pDF2Ryqg",
|
||||
ExportName = "sceKernelUnknown9T2p",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelUnknown9T2p(CpuContext ctx) => SetZeroReturn(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "TU-d9PfIHPM",
|
||||
ExportName = "sceKernelUnknownTU",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelUnknownTU(CpuContext ctx) => SetZeroReturn(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "iWQWrwiSt8A",
|
||||
ExportName = "sceKernelUnknowniWQW",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelUnknowniWQW(CpuContext ctx) => SetZeroReturn(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "KuOmgKoqCdY",
|
||||
ExportName = "sceKernelUnknownKuOmg",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelUnknownKuOmg(CpuContext ctx) => SetZeroReturn(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "RenI1lL1WFk",
|
||||
ExportName = "sceKernelUnknownRenI",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelUnknownRenI(CpuContext ctx) => SetZeroReturn(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "pQGpHYopAIY",
|
||||
ExportName = "sceKernelUnknownpQGp",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelUnknownpQGp(CpuContext ctx) => SetZeroReturn(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Rbvt+5Y2iEw",
|
||||
ExportName = "sceKernelUnknownRbvt",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelUnknownRbvt(CpuContext ctx) => SetZeroReturn(ctx);
|
||||
|
||||
// NOTE: "SHtAad20YYM"/"DIxvoy7Ngvk" are sce::Json::Value::getType/getInteger, and
|
||||
// "3GPpjQdAMTw"/"9rAeANT2tyE"/"tsvEmnenz48" are __cxa_guard_acquire/__cxa_guard_release/
|
||||
// __cxa_atexit (verified by hashing against scripts/ps5_names.txt). Do not register them
|
||||
// here as kernel mutex functions: the cxa guards are implemented in CxxAbiExports.cs and
|
||||
// shadowing them breaks every C++ static-init guard in the game.
|
||||
|
||||
private static int SetZeroReturn(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,8 @@ public static class KernelMemoryCompatExports
|
||||
private const int OrbisVirtualQueryInfoSize = 72;
|
||||
private const int OrbisKernelMaximumNameLength = 32;
|
||||
private const uint MemCommit = 0x1000;
|
||||
private const uint MemReserve = 0x2000;
|
||||
private const uint MemRelease = 0x8000;
|
||||
private const uint HostPageNoAccess = 0x01;
|
||||
private const uint HostPageReadOnly = 0x02;
|
||||
private const uint HostPageReadWrite = 0x04;
|
||||
@@ -121,6 +123,7 @@ public static class KernelMemoryCompatExports
|
||||
private static int _hostMemoryWriteFallbackCount;
|
||||
private static int _hostMemoryReadFallbackCount;
|
||||
private static int _nullWcscpyRecoveryCount;
|
||||
private static int _nullStrcasecmpRecoveryCount;
|
||||
private static string? _cachedApp0Root;
|
||||
private static string? _cachedDownload0Root;
|
||||
|
||||
@@ -143,6 +146,13 @@ public static class KernelMemoryCompatExports
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool VirtualProtect(nint lpAddress, nuint dwSize, uint flNewProtect, out uint lpflOldProtect);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern nint VirtualAlloc(nint lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool VirtualFree(nint lpAddress, nuint dwSize, uint dwFreeType);
|
||||
|
||||
private sealed class OpenDirectory
|
||||
{
|
||||
public required string Path { get; init; }
|
||||
@@ -151,7 +161,7 @@ public static class KernelMemoryCompatExports
|
||||
}
|
||||
|
||||
private readonly record struct DirectAllocation(ulong Start, ulong Length, int MemoryType);
|
||||
private readonly record struct LibcHeapAllocation(nint BaseAddress, nuint Size, nuint Alignment);
|
||||
private readonly record struct LibcHeapAllocation(nint BaseAddress, nuint Size, nuint Alignment, bool IsGuarded);
|
||||
private readonly record struct MappedRegion(ulong Address, ulong Length, int Protection, bool IsFlexible, bool IsDirect, ulong DirectStart);
|
||||
private readonly record struct BatchMapEntry(ulong Start, ulong Offset, ulong Length, byte Protection, byte Type, int Operation);
|
||||
|
||||
@@ -186,6 +196,14 @@ public static class KernelMemoryCompatExports
|
||||
ulong length,
|
||||
ulong alignment,
|
||||
out ulong address)
|
||||
=> TryAllocateHleData(ctx, length, alignment, OrbisProtCpuReadWrite, out address);
|
||||
|
||||
internal static bool TryAllocateHleData(
|
||||
CpuContext ctx,
|
||||
ulong length,
|
||||
ulong alignment,
|
||||
int protection,
|
||||
out ulong address)
|
||||
{
|
||||
address = 0;
|
||||
if (length == 0 || length > int.MaxValue)
|
||||
@@ -200,7 +218,7 @@ public static class KernelMemoryCompatExports
|
||||
var desiredAddress = AlignUp(
|
||||
_nextVirtualAddress == 0 ? 0x1_0000_0000UL : _nextVirtualAddress,
|
||||
effectiveAlignment);
|
||||
if (!TryReserveGuestVirtualRange(ctx, desiredAddress, mappedLength, OrbisProtCpuReadWrite, effectiveAlignment, out address) ||
|
||||
if (!TryReserveGuestVirtualRange(ctx, desiredAddress, mappedLength, protection, effectiveAlignment, out address) ||
|
||||
address == 0)
|
||||
{
|
||||
return false;
|
||||
@@ -210,7 +228,7 @@ public static class KernelMemoryCompatExports
|
||||
_mappedRegions[address] = new MappedRegion(
|
||||
address,
|
||||
mappedLength,
|
||||
OrbisProtCpuReadWrite,
|
||||
protection,
|
||||
IsFlexible: false,
|
||||
IsDirect: false,
|
||||
DirectStart: 0);
|
||||
@@ -231,6 +249,69 @@ public static class KernelMemoryCompatExports
|
||||
return true;
|
||||
}
|
||||
|
||||
private static ulong _dummyVtableAddress;
|
||||
private const int DummyVtableSlotCount = 64;
|
||||
|
||||
// Some KexEngine objects (mutex wrappers, NGS2 system/rack/voice handles, etc.) are treated
|
||||
// by guest code as C++ instances with a vtable pointer at offset 0. When an HLE constructor
|
||||
// leaves that slot zeroed, the game's own virtual dispatch (call [ [obj]+N ]) reads a null
|
||||
// vtable and jumps through address N, crashing (e.g. "CALL qword ptr [RAX+0x58]" with RAX=0).
|
||||
// This allocates one shared, executable no-op stub plus a vtable of pointers to it, so any
|
||||
// HLE object constructor can make virtual calls on its output land safely.
|
||||
internal static bool TryWriteDummyVtable(CpuContext ctx, ulong objectAddress)
|
||||
{
|
||||
if (objectAddress == 0 || !TryEnsureDummyVtable(ctx, out var vtableAddress))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return ctx.TryWriteUInt64(objectAddress, vtableAddress);
|
||||
}
|
||||
|
||||
private static bool TryEnsureDummyVtable(CpuContext ctx, out ulong vtableAddress)
|
||||
{
|
||||
lock (_memoryGate)
|
||||
{
|
||||
if (_dummyVtableAddress != 0)
|
||||
{
|
||||
vtableAddress = _dummyVtableAddress;
|
||||
return true;
|
||||
}
|
||||
|
||||
const int executableReadWrite = OrbisProtCpuRead | OrbisProtCpuWrite | OrbisProtCpuExec;
|
||||
if (!TryAllocateHleData(ctx, 0x1000, 0x1000, executableReadWrite, out var block))
|
||||
{
|
||||
vtableAddress = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// xor eax, eax; ret - every dummy virtual method just returns 0 and does nothing else.
|
||||
if (!ctx.Memory.TryWrite(block, new byte[] { 0x31, 0xC0, 0xC3 }))
|
||||
{
|
||||
vtableAddress = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
var table = new byte[DummyVtableSlotCount * sizeof(ulong)];
|
||||
for (var i = 0; i < DummyVtableSlotCount; i++)
|
||||
{
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(table.AsSpan(i * sizeof(ulong), sizeof(ulong)), block);
|
||||
}
|
||||
|
||||
var tableAddress = block + 0x100;
|
||||
if (!ctx.Memory.TryWrite(tableAddress, table))
|
||||
{
|
||||
vtableAddress = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Dummy vtable ready: stub=0x{block:X16} vtable=0x{tableAddress:X16} slots={DummyVtableSlotCount}");
|
||||
_dummyVtableAddress = tableAddress;
|
||||
vtableAddress = tableAddress;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
internal static void RegisterReservedVirtualRange(ulong address, ulong length)
|
||||
{
|
||||
if (address == 0 || length == 0)
|
||||
@@ -281,6 +362,18 @@ public static class KernelMemoryCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// Longer null-dst memsets are unrecoverable, but RAX must still be set - leaving it
|
||||
// stale here previously let callers that do `buf = memset(...)` carry on with a
|
||||
// garbage "buffer" pointer instead of a clean NULL, causing a *different*,
|
||||
// confusingly-located crash further downstream.
|
||||
var largeRecoveryIndex = Interlocked.Increment(ref _nullMemsetRecoveryCount);
|
||||
if (largeRecoveryIndex <= 8)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARNING] memset null-dst (len>0x20) recovery#{largeRecoveryIndex}: rip=0x{ctx.Rip:X16} len=0x{length:X} val=0x{value:X2}");
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
@@ -304,6 +397,7 @@ public static class KernelMemoryCompatExports
|
||||
Console.WriteLine("!!! CRITICAL: Bad Memset Call !!!");
|
||||
Console.WriteLine($"Called from RIP: 0x{ctx.Rip:X}");
|
||||
Console.WriteLine($"dst=0x{destination:X} val=0x{value:X2} len=0x{length:X}");
|
||||
ctx[CpuRegister.Rax] = destination;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
@@ -536,6 +630,80 @@ public static class KernelMemoryCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "AV6ipCNa4Rw",
|
||||
ExportName = "strcasecmp",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Strcasecmp(CpuContext ctx)
|
||||
{
|
||||
var left = ctx[CpuRegister.Rdi];
|
||||
var right = ctx[CpuRegister.Rsi];
|
||||
if (left == 0 || right == 0)
|
||||
{
|
||||
var recoveryIndex = Interlocked.Increment(ref _nullStrcasecmpRecoveryCount);
|
||||
if (recoveryIndex <= 16)
|
||||
{
|
||||
var otherAddress = left == 0 ? right : left;
|
||||
var otherText = otherAddress != 0 && TryReadNullTerminatedUtf8(ctx, otherAddress, 256, out var text)
|
||||
? text
|
||||
: "<unreadable>";
|
||||
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out var returnRip);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARNING] strcasecmp null-arg recovery#{recoveryIndex}: ret=0x{returnRip:X16} left=0x{left:X16} right=0x{right:X16} other=\"{otherText}\"");
|
||||
}
|
||||
|
||||
// Real strcasecmp(NULL, x) is undefined behaviour and previously crashed inside the
|
||||
// LLE-routed implementation. Treat it as "not equal" instead so callers doing
|
||||
// `if (strcasecmp(a, b) == 0)` degrade gracefully rather than taking down the guest.
|
||||
ctx[CpuRegister.Rax] = left == right ? 0uL : 1uL;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (!TryCompareStringsCaseInsensitive(ctx, left, right, limit: ulong.MaxValue, out var compare))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 1;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)compare);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static bool TryCompareStringsCaseInsensitive(CpuContext ctx, ulong left, ulong right, ulong limit, out int compare)
|
||||
{
|
||||
compare = 0;
|
||||
if (left == 0 || right == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var max = limit == ulong.MaxValue ? 1_048_576UL : Math.Min(limit, 1_048_576UL);
|
||||
Span<byte> leftByte = stackalloc byte[1];
|
||||
Span<byte> rightByte = stackalloc byte[1];
|
||||
for (ulong i = 0; i < max; i++)
|
||||
{
|
||||
if (!TryReadCompat(ctx, left + i, leftByte) ||
|
||||
!TryReadCompat(ctx, right + i, rightByte))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var leftLower = ToAsciiLower(leftByte[0]);
|
||||
var rightLower = ToAsciiLower(rightByte[0]);
|
||||
compare = leftLower - rightLower;
|
||||
if (compare != 0 || leftByte[0] == 0 || rightByte[0] == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
compare = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static byte ToAsciiLower(byte value) => value is >= (byte)'A' and <= (byte)'Z' ? (byte)(value + 32) : value;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "0nV21JjYCH8",
|
||||
ExportName = "wcsncpy",
|
||||
@@ -587,6 +755,54 @@ public static class KernelMemoryCompatExports
|
||||
return VsnprintfCore(ctx);
|
||||
}
|
||||
|
||||
// sprintf/vsprintf are served from the same HLE formatting engine as snprintf/vsnprintf
|
||||
// instead of falling through to the game's bundled libc.
|
||||
[SysAbiExport(
|
||||
Nid = "tcVi5SivF7Q",
|
||||
ExportName = "sprintf",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Sprintf(CpuContext ctx)
|
||||
{
|
||||
var destination = ctx[CpuRegister.Rdi];
|
||||
var formatAddress = ctx[CpuRegister.Rsi];
|
||||
|
||||
if (!TryReadCString(ctx, formatAddress, 1_048_576, out var formatBytes))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
var format = Encoding.UTF8.GetString(formatBytes);
|
||||
var rendered = FormatStringFromVarArgs(ctx, format, firstGpArgIndex: 2);
|
||||
return WriteSnprintfOutput(ctx, destination, ulong.MaxValue, rendered);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "jbz9I9vkqkk",
|
||||
ExportName = "vsprintf",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Vsprintf(CpuContext ctx)
|
||||
{
|
||||
var destination = ctx[CpuRegister.Rdi];
|
||||
var formatAddress = ctx[CpuRegister.Rsi];
|
||||
var vaListAddress = ctx[CpuRegister.Rdx];
|
||||
|
||||
if (!TryReadCString(ctx, formatAddress, 1_048_576, out var formatBytes))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
var format = Encoding.UTF8.GetString(formatBytes);
|
||||
if (!TryCreateVaListCursor(ctx, vaListAddress, out var vaCursor))
|
||||
{
|
||||
return WriteSnprintfOutput(ctx, destination, ulong.MaxValue, formatBytes);
|
||||
}
|
||||
|
||||
var rendered = FormatString(ctx, format, ref vaCursor);
|
||||
return WriteSnprintfOutput(ctx, destination, ulong.MaxValue, rendered);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "nJz16JE1txM",
|
||||
ExportName = "swprintf",
|
||||
@@ -945,15 +1161,27 @@ public static class KernelMemoryCompatExports
|
||||
{
|
||||
var destination = ctx[CpuRegister.Rdi];
|
||||
var source = ctx[CpuRegister.Rsi];
|
||||
var count = (int)Math.Min(ctx[CpuRegister.Rdx], int.MaxValue);
|
||||
if (count < 0)
|
||||
var rawCount = ctx[CpuRegister.Rdx];
|
||||
|
||||
// A garbage/absurd count (observed as e.g. 0xA7560035 from the same still-unidentified
|
||||
// upstream bug that also feeds bad lengths to memset) must not reach
|
||||
// GC.AllocateUninitializedArray: attempting a multi-GB allocation from a guest-thread
|
||||
// call context corrupted the CLR outright ("Invalid Program: attempted to call a
|
||||
// UnmanagedCallersOnly method from managed code") instead of throwing a normal
|
||||
// exception. Reject anything above a sane bound before allocating.
|
||||
const ulong maxSaneCount = 512UL * 1024 * 1024;
|
||||
if (rawCount > maxSaneCount)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][WARNING] memcpy oversized count rejected: dst=0x{destination:X16} src=0x{source:X16} count=0x{rawCount:X}");
|
||||
ctx[CpuRegister.Rax] = destination;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var count = (int)rawCount;
|
||||
var payload = GC.AllocateUninitializedArray<byte>(count);
|
||||
if (count > 0 && (!TryReadCompat(ctx, source, payload) || !TryWriteCompat(ctx, destination, payload)))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = destination;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
@@ -1196,6 +1424,24 @@ public static class KernelMemoryCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// Was unresolved (returning the 0x80020002 sentinel, then crashing when the guest
|
||||
// dereferenced it) - the game's own heap instrumentation calls this hook when it
|
||||
// detects a corrupted/invalid block, not the emulator's allocator, so this is purely
|
||||
// a diagnostic sink: log what was reported and return success so the caller continues.
|
||||
[SysAbiExport(
|
||||
Nid = "al3JzFI9MQ0",
|
||||
ExportName = "sceLibcInternalHeapErrorReportForGame",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceLibcInternal")]
|
||||
public static int LibcInternalHeapErrorReportForGame(CpuContext ctx)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] sceLibcInternalHeapErrorReportForGame: rdi=0x{ctx[CpuRegister.Rdi]:X16} " +
|
||||
$"rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} rcx=0x{ctx[CpuRegister.Rcx]:X16}");
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "DfivPArhucg",
|
||||
ExportName = "memcmp",
|
||||
@@ -1443,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);
|
||||
@@ -1466,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;
|
||||
}
|
||||
@@ -4151,7 +4421,7 @@ public static class KernelMemoryCompatExports
|
||||
return FileMode.Open;
|
||||
}
|
||||
|
||||
private static string ResolveGuestPath(string guestPath)
|
||||
public static string ResolveGuestPath(string guestPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(guestPath))
|
||||
{
|
||||
@@ -4475,7 +4745,7 @@ public static class KernelMemoryCompatExports
|
||||
private static bool IsMutatingOpen(int flags) =>
|
||||
(flags & (O_WRONLY | O_RDWR | O_CREAT | O_TRUNC | O_APPEND)) != 0;
|
||||
|
||||
private static bool IsReadOnlyGuestMutationPath(string guestPath)
|
||||
public static bool IsReadOnlyGuestMutationPath(string guestPath)
|
||||
{
|
||||
var normalized = NormalizeGuestStatCachePath(guestPath);
|
||||
return normalized is not null &&
|
||||
@@ -4754,7 +5024,7 @@ public static class KernelMemoryCompatExports
|
||||
return destination == 0 || destinationCount == 0 || TryWriteWideTerminator(ctx, destination);
|
||||
}
|
||||
|
||||
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
|
||||
public static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
|
||||
{
|
||||
value = string.Empty;
|
||||
if (address == 0 || maxLength <= 0)
|
||||
@@ -5543,6 +5813,26 @@ public static class KernelMemoryCompatExports
|
||||
alignment = NormalizeLibcAlignment(alignment);
|
||||
var actualSize = requestedSize == 0 ? 1u : requestedSize;
|
||||
|
||||
// This intentionally uses host guard pages for allocations made through the libc HLE.
|
||||
// It turns an overrun that reaches the next page into an immediate access violation at
|
||||
// the offending guest instruction, rather than allowing it to poison a later import.
|
||||
var guardHeap = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_GUARD_HEAP"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
|
||||
if (guardHeap && TryAllocateGuardedLibcHeap(actualSize, alignment, zeroFill, out address))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Do not silently fall back to an unguarded allocation when guard heap was explicitly
|
||||
// requested: a failed setup must remain visible to the caller/reproducer.
|
||||
if (guardHeap)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
nuint totalSize;
|
||||
try
|
||||
{
|
||||
@@ -5578,7 +5868,7 @@ public static class KernelMemoryCompatExports
|
||||
var alignedAddress = AlignUp(unchecked((ulong)baseAddress) + (ulong)IntPtr.Size, (ulong)alignment);
|
||||
lock (_libcAllocGate)
|
||||
{
|
||||
_libcAllocations[alignedAddress] = new LibcHeapAllocation(baseAddress, actualSize, alignment);
|
||||
_libcAllocations[alignedAddress] = new LibcHeapAllocation(baseAddress, actualSize, alignment, IsGuarded: false);
|
||||
}
|
||||
|
||||
try
|
||||
@@ -5598,6 +5888,51 @@ public static class KernelMemoryCompatExports
|
||||
return true;
|
||||
}
|
||||
|
||||
private static unsafe bool TryAllocateGuardedLibcHeap(nuint actualSize, nuint alignment, bool zeroFill, out ulong address)
|
||||
{
|
||||
address = 0;
|
||||
const nuint pageSize = 0x1000;
|
||||
|
||||
try
|
||||
{
|
||||
var effectiveAlignment = Math.Max(alignment, pageSize);
|
||||
var usableSize = checked((nuint)AlignUp((ulong)actualSize, (ulong)pageSize));
|
||||
var reservationSize = checked(pageSize + effectiveAlignment - 1 + usableSize + pageSize);
|
||||
var baseAddress = VirtualAlloc(0, reservationSize, MemCommit | MemReserve, HostPageReadWrite);
|
||||
if (baseAddress == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var alignedAddress = AlignUp(unchecked((ulong)baseAddress) + (ulong)pageSize, (ulong)effectiveAlignment);
|
||||
var guardAddress = alignedAddress + (ulong)usableSize;
|
||||
if (!VirtualProtect((nint)guardAddress, pageSize, HostPageNoAccess, out _))
|
||||
{
|
||||
_ = VirtualFree(baseAddress, 0, MemRelease);
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (_libcAllocGate)
|
||||
{
|
||||
_libcAllocations[alignedAddress] = new LibcHeapAllocation(baseAddress, actualSize, alignment, IsGuarded: true);
|
||||
}
|
||||
|
||||
if (zeroFill)
|
||||
{
|
||||
NativeMemory.Clear((void*)alignedAddress, actualSize);
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] guard-heap alloc: ptr=0x{alignedAddress:X16} size=0x{actualSize:X} guard=0x{guardAddress:X16}");
|
||||
address = alignedAddress;
|
||||
return true;
|
||||
}
|
||||
catch (OverflowException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe bool TryReallocateLibcHeap(ulong existingAddress, ulong requestedSize, out ulong resizedAddress)
|
||||
{
|
||||
resizedAddress = 0;
|
||||
@@ -5706,7 +6041,14 @@ public static class KernelMemoryCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
Marshal.FreeHGlobal(allocation.BaseAddress);
|
||||
if (allocation.IsGuarded)
|
||||
{
|
||||
_ = VirtualFree(allocation.BaseAddress, 0, MemRelease);
|
||||
}
|
||||
else
|
||||
{
|
||||
Marshal.FreeHGlobal(allocation.BaseAddress);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryMultiplyAllocationSize(ulong left, ulong right, out nuint size)
|
||||
@@ -6032,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);
|
||||
@@ -6063,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;
|
||||
}
|
||||
@@ -6312,4 +6661,15 @@ public static class KernelMemoryCompatExports
|
||||
sum = left + right;
|
||||
return sum >= left;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "KMcEa+rHsIo",
|
||||
ExportName = "sceKernelMapMemory",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelMapMemory(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,11 +19,17 @@ 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;
|
||||
private const ulong TlsStackChkGuardBaseOffset = 0x800;
|
||||
private const ulong StackChkGuardFieldOffset = 0x10;
|
||||
@@ -74,6 +80,77 @@ public static class KernelRuntimeCompatExports
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate ulong RdtscDelegate();
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "QvsZxomvUHs",
|
||||
ExportName = "sceKernelNanosleep",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelNanosleep(CpuContext ctx)
|
||||
{
|
||||
var requestAddress = ctx[CpuRegister.Rdi];
|
||||
var remainAddress = ctx[CpuRegister.Rsi];
|
||||
|
||||
if (requestAddress == 0)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = Einval;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
Span<byte> timespecBuffer = stackalloc byte[16];
|
||||
if (!ctx.Memory.TryRead(requestAddress, timespecBuffer))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = Efault;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
var tvSec = BinaryPrimitives.ReadInt64LittleEndian(timespecBuffer);
|
||||
var tvNsec = BinaryPrimitives.ReadInt64LittleEndian(timespecBuffer[sizeof(long)..]);
|
||||
|
||||
if (tvSec < 0 || tvNsec < 0 || tvNsec >= 1_000_000_000L)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = Einval;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (tvSec == 0 && tvNsec == 0)
|
||||
{
|
||||
WriteRemainingTime(ctx, remainAddress, 0, 0);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
GuestThreadExecution.Scheduler?.Pump(ctx, "sceKernelNanosleep");
|
||||
|
||||
// TimeSpan resolution is 100 ns ticks, so sub-100 ns requests round up to
|
||||
// a single tick rather than collapsing to a zero-length (no-op) sleep.
|
||||
var totalTicks = tvSec * TimeSpan.TicksPerSecond + Math.Max(tvNsec / 100L, 1L);
|
||||
try
|
||||
{
|
||||
Thread.Sleep(TimeSpan.FromTicks(totalTicks));
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
Thread.Sleep(TimeSpan.FromMilliseconds(int.MaxValue));
|
||||
}
|
||||
|
||||
WriteRemainingTime(ctx, remainAddress, 0, 0);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static void WriteRemainingTime(CpuContext ctx, ulong remainAddress, long seconds, long nanoseconds)
|
||||
{
|
||||
if (remainAddress == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Span<byte> remainBuffer = stackalloc byte[16];
|
||||
BinaryPrimitives.WriteInt64LittleEndian(remainBuffer, seconds);
|
||||
BinaryPrimitives.WriteInt64LittleEndian(remainBuffer[sizeof(long)..], nanoseconds);
|
||||
ctx.Memory.TryWrite(remainAddress, remainBuffer);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "1jfXLRVzisc",
|
||||
ExportName = "sceKernelUsleep",
|
||||
@@ -651,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,467 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
|
||||
namespace SharpEmu.Libs.LibcStdio;
|
||||
|
||||
public static class LibcStdioExports
|
||||
{
|
||||
private const int MaxPathLength = 4096;
|
||||
private const int MaxModeLength = 16;
|
||||
private const int ReadChunkSize = 1024 * 1024;
|
||||
|
||||
private static readonly ConcurrentDictionary<ulong, FileStream> _fileHandles = new();
|
||||
private static long _nextHandle = 0x1000 - 8;
|
||||
|
||||
private static readonly bool _traceStdio =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_STDIO"), "1", StringComparison.Ordinal);
|
||||
|
||||
private const int CtypeTableLowerBound = -128;
|
||||
private const int CtypeTableUpperBound = 255;
|
||||
private const int CtypeTableEntryCount = CtypeTableUpperBound - CtypeTableLowerBound + 1; // 384 entries * 2 bytes = 768 bytes
|
||||
|
||||
// Mirrors the Dinkumware ctype bitmask layout (the CRT Sony's libc is based on;
|
||||
// _Getpctype/_Getptolower/_Getptoupper are Dinkumware accessor names). This is NOT the
|
||||
// MSVC/UCRT layout: e.g. Dinkumware puts digit at 0x20 where UCRT puts control, so
|
||||
// serving a UCRT-shaped table made the game's bundled printf engine misparse every
|
||||
// %-directive (fatal-error messages rendered empty) and made its mcpp preprocessor
|
||||
// treat 'a'-'f' as control characters (UCRT _HEX=0x80 reads as Dinkumware _BB) and
|
||||
// drop them from identifiers ("texture" -> "txtur"). Titles that bundle their own
|
||||
// libc bypass this table entirely (see IsSafeLleLibcExport in DirectExecutionBackend);
|
||||
// this fallback only serves titles that import _Getpctype without shipping one.
|
||||
private const ushort CtypeXDigit = 0x001; // _XD '0'-'9', 'A'-'F', 'a'-'f'
|
||||
private const ushort CtypeUpper = 0x002; // _UP 'A'-'Z'
|
||||
private const ushort CtypeSpace = 0x004; // _SP ' ' (isspace = _CN|_SP|_XS)
|
||||
private const ushort CtypePunct = 0x008; // _PU punctuation
|
||||
private const ushort CtypeLower = 0x010; // _LO 'a'-'z'
|
||||
private const ushort CtypeDigit = 0x020; // _DI '0'-'9'
|
||||
private const ushort CtypeControlSpace = 0x040; // _CN '\t','\n','\v','\f','\r'
|
||||
private const ushort CtypeControl = 0x080; // _BB other control characters
|
||||
private const ushort CtypeBlank = 0x400; // _XB ' ' and '\t'
|
||||
|
||||
private static readonly object _ctypeTableGate = new();
|
||||
private static nint _ctypeTableBase;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "xeYO4u7uyJ0",
|
||||
ExportName = "fopen",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Fopen(CpuContext ctx)
|
||||
{
|
||||
var pathAddress = ctx[CpuRegister.Rdi];
|
||||
var modeAddress = ctx[CpuRegister.Rsi];
|
||||
|
||||
if (pathAddress == 0 || modeAddress == 0 ||
|
||||
!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, pathAddress, MaxPathLength, out var guestPath) ||
|
||||
!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, modeAddress, MaxModeLength, out var mode) ||
|
||||
!TryParseFopenMode(mode, out var fileMode, out var fileAccess))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var hostPath = KernelMemoryCompatExports.ResolveGuestPath(guestPath);
|
||||
if (fileAccess != FileAccess.Read && KernelMemoryCompatExports.IsReadOnlyGuestMutationPath(guestPath))
|
||||
{
|
||||
if (_traceStdio)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] fopen: guest='{guestPath}' host='{hostPath}' mode='{mode}' -> PERMISSION_DENIED (read-only path)");
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (fileAccess != FileAccess.Read)
|
||||
{
|
||||
var parentDirectory = Path.GetDirectoryName(hostPath);
|
||||
if (!string.IsNullOrWhiteSpace(parentDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(parentDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
var stream = new FileStream(hostPath, fileMode, fileAccess, FileShare.ReadWrite);
|
||||
if (mode.StartsWith('a') && fileAccess == FileAccess.ReadWrite)
|
||||
{
|
||||
stream.Seek(0, SeekOrigin.End);
|
||||
}
|
||||
|
||||
var handle = unchecked((ulong)Interlocked.Add(ref _nextHandle, 8));
|
||||
_fileHandles[handle] = stream;
|
||||
|
||||
if (_traceStdio)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] fopen: guest='{guestPath}' host='{hostPath}' mode='{mode}' -> OK handle=0x{handle:X} length={stream.Length}");
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = handle;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
if (_traceStdio)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] fopen: guest='{guestPath}' host='{hostPath}' mode='{mode}' -> FAILED {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return ex is UnauthorizedAccessException
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "rQFVBXp-Cxg",
|
||||
ExportName = "fseek",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Fseek(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
var offset = unchecked((long)ctx[CpuRegister.Rsi]);
|
||||
var whence = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||
|
||||
if (!_fileHandles.TryGetValue(handle, out var stream) || !TryGetSeekOrigin(whence, out var origin))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)-1);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
stream.Seek(offset, origin);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)-1);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Qazy8LmXTvw",
|
||||
ExportName = "ftell",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Ftell(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
|
||||
if (!_fileHandles.TryGetValue(handle, out var stream))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(long)-1);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)stream.Position);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(long)-1);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "uodLYyUip20",
|
||||
ExportName = "fclose",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Fclose(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
|
||||
if (!_fileHandles.TryRemove(handle, out var stream))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)-1);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
stream.Dispose();
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)-1);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "lbB+UlZqVG0",
|
||||
ExportName = "fread",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Fread(CpuContext ctx)
|
||||
{
|
||||
var destination = ctx[CpuRegister.Rdi];
|
||||
var elementSize = ctx[CpuRegister.Rsi];
|
||||
var elementCount = ctx[CpuRegister.Rdx];
|
||||
var handle = ctx[CpuRegister.Rcx];
|
||||
|
||||
if (elementSize == 0 || elementCount == 0)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (destination == 0 || !_fileHandles.TryGetValue(handle, out var stream))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var totalRequested = elementSize * elementCount;
|
||||
var buffer = ArrayPool<byte>.Shared.Rent((int)Math.Min((ulong)ReadChunkSize, totalRequested));
|
||||
ulong totalRead = 0;
|
||||
|
||||
try
|
||||
{
|
||||
while (totalRead < totalRequested)
|
||||
{
|
||||
var request = (int)Math.Min((ulong)buffer.Length, totalRequested - totalRead);
|
||||
var read = stream.Read(buffer, 0, request);
|
||||
if (read <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (!ctx.Memory.TryWrite(destination + totalRead, buffer.AsSpan(0, read)))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = totalRead / elementSize;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
totalRead += (ulong)read;
|
||||
}
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = totalRead / elementSize;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
|
||||
if (_traceStdio)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] fread: handle=0x{handle:X} requested={totalRequested} read={totalRead} pos={stream.Position}");
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = totalRead / elementSize;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "KdP-nULpuGw",
|
||||
ExportName = "fgets",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Fgets(CpuContext ctx)
|
||||
{
|
||||
var destination = ctx[CpuRegister.Rdi];
|
||||
var maxCount = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
var handle = ctx[CpuRegister.Rdx];
|
||||
|
||||
if (destination == 0 || maxCount <= 0 || !_fileHandles.TryGetValue(handle, out var stream))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var buffer = ArrayPool<byte>.Shared.Rent(maxCount - 1);
|
||||
var count = 0;
|
||||
|
||||
try
|
||||
{
|
||||
while (count < maxCount - 1)
|
||||
{
|
||||
var b = stream.ReadByte();
|
||||
if (b < 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
buffer[count++] = (byte)b;
|
||||
if (b == '\n')
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
Span<byte> withNul = stackalloc byte[count + 1];
|
||||
buffer.AsSpan(0, count).CopyTo(withNul);
|
||||
withNul[count] = 0;
|
||||
|
||||
if (!ctx.Memory.TryWrite(destination, withNul))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = destination;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "sUP1hBaouOw",
|
||||
ExportName = "_Getpctype",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int GetPctype(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)EnsureCtypeTable());
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static unsafe nint EnsureCtypeTable()
|
||||
{
|
||||
lock (_ctypeTableGate)
|
||||
{
|
||||
if (_ctypeTableBase != 0)
|
||||
{
|
||||
return _ctypeTableBase;
|
||||
}
|
||||
|
||||
var storage = Marshal.AllocHGlobal(CtypeTableEntryCount * sizeof(ushort));
|
||||
var entries = new Span<ushort>((void*)storage, CtypeTableEntryCount);
|
||||
for (var i = 0; i < CtypeTableEntryCount; i++)
|
||||
{
|
||||
var c = i + CtypeTableLowerBound;
|
||||
entries[i] = c is >= 0 and <= 0x7F ? ComputeCtypeFlags(c) : (ushort)0;
|
||||
}
|
||||
|
||||
// Table is indexed as base[c] for c in [-128, 255], so the pointer handed to the
|
||||
// guest must point at the c == 0 entry, not the start of the allocation.
|
||||
_ctypeTableBase = storage - (CtypeTableLowerBound * sizeof(ushort));
|
||||
return _ctypeTableBase;
|
||||
}
|
||||
}
|
||||
|
||||
private static ushort ComputeCtypeFlags(int c)
|
||||
{
|
||||
var isUpper = c is >= 'A' and <= 'Z';
|
||||
var isLower = c is >= 'a' and <= 'z';
|
||||
var isDigit = c is >= '0' and <= '9';
|
||||
var isHex = isDigit || (c is >= 'A' and <= 'F') || (c is >= 'a' and <= 'f');
|
||||
var isAlnum = isUpper || isLower || isDigit;
|
||||
// In the Dinkumware table '\t'..'\r' carry _CN (isspace and iscntrl both match via
|
||||
// _CN) while plain ' ' carries _SP; keeping them distinct is what keeps isprint(' ')
|
||||
// true and isprint('\t') false.
|
||||
var isControlSpace = c is >= 0x09 and <= 0x0D;
|
||||
var isControl = (c is >= 0x00 and <= 0x08) || (c is >= 0x0E and <= 0x1F) || c == 0x7F;
|
||||
var isPrintable = c is >= 0x20 and <= 0x7E;
|
||||
var isPunct = isPrintable && !isAlnum && c != ' ';
|
||||
|
||||
ushort flags = 0;
|
||||
if (isUpper) flags |= CtypeUpper;
|
||||
if (isLower) flags |= CtypeLower;
|
||||
if (isDigit) flags |= CtypeDigit;
|
||||
if (isHex) flags |= CtypeXDigit;
|
||||
if (c == ' ') flags |= CtypeSpace | CtypeBlank;
|
||||
if (c == '\t') flags |= CtypeBlank;
|
||||
if (isControlSpace) flags |= CtypeControlSpace;
|
||||
if (isPunct) flags |= CtypePunct;
|
||||
if (isControl) flags |= CtypeControl;
|
||||
return flags;
|
||||
}
|
||||
|
||||
private static bool TryParseFopenMode(string mode, out FileMode fileMode, out FileAccess fileAccess)
|
||||
{
|
||||
fileMode = FileMode.Open;
|
||||
fileAccess = FileAccess.Read;
|
||||
|
||||
if (string.IsNullOrEmpty(mode))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var plus = mode.Contains('+');
|
||||
switch (mode[0])
|
||||
{
|
||||
case 'r':
|
||||
fileMode = FileMode.Open;
|
||||
fileAccess = plus ? FileAccess.ReadWrite : FileAccess.Read;
|
||||
return true;
|
||||
|
||||
case 'w':
|
||||
fileMode = FileMode.Create;
|
||||
fileAccess = plus ? FileAccess.ReadWrite : FileAccess.Write;
|
||||
return true;
|
||||
|
||||
case 'a':
|
||||
if (plus)
|
||||
{
|
||||
fileMode = FileMode.OpenOrCreate;
|
||||
fileAccess = FileAccess.ReadWrite;
|
||||
}
|
||||
else
|
||||
{
|
||||
fileMode = FileMode.Append;
|
||||
fileAccess = FileAccess.Write;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetSeekOrigin(int whence, out SeekOrigin origin)
|
||||
{
|
||||
switch (whence)
|
||||
{
|
||||
case 0:
|
||||
origin = SeekOrigin.Begin;
|
||||
return true;
|
||||
|
||||
case 1:
|
||||
origin = SeekOrigin.Current;
|
||||
return true;
|
||||
|
||||
case 2:
|
||||
origin = SeekOrigin.End;
|
||||
return true;
|
||||
|
||||
default:
|
||||
origin = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ public static class Ngs2Exports
|
||||
private const int OrbisNgs2ErrorInvalidSystemHandle = unchecked((int)0x804A0230);
|
||||
private const int OrbisNgs2ErrorInvalidRackHandle = unchecked((int)0x804A0261);
|
||||
private const int OrbisNgs2ErrorInvalidVoiceHandle = unchecked((int)0x804A0300);
|
||||
private const ulong HandleStorageSize = 0x20;
|
||||
private const ulong HandleStorageSize = 0x1000;
|
||||
private const int RenderBufferInfoSize = 0x18;
|
||||
private const ulong MaximumRenderBufferSize = 16 * 1024 * 1024;
|
||||
|
||||
@@ -197,12 +197,18 @@ public static class Ngs2Exports
|
||||
lock (StateGate)
|
||||
{
|
||||
return ctx.SetReturn(
|
||||
Voices.ContainsKey(ctx[CpuRegister.Rdi])
|
||||
? 0
|
||||
Voices.ContainsKey(ctx[CpuRegister.Rdi])
|
||||
? 0
|
||||
: OrbisNgs2ErrorInvalidVoiceHandle);
|
||||
}
|
||||
}
|
||||
|
||||
// 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",
|
||||
@@ -210,6 +216,27 @@ public static class Ngs2Exports
|
||||
LibraryName = "libSceNgs2")]
|
||||
public static int Ngs2VoiceRunCommands(CpuContext ctx) => Ngs2VoiceControl(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "-TOuuAQ-buE",
|
||||
ExportName = "sceNgs2VoiceGetState",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNgs2")]
|
||||
public static int Ngs2VoiceGetState(CpuContext ctx) => ctx.SetReturn(0);
|
||||
|
||||
[SysAbiExport(
|
||||
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",
|
||||
@@ -263,7 +290,6 @@ public static class Ngs2Exports
|
||||
|
||||
private static bool TryCreateHandle(CpuContext ctx, uint type, ulong ownerHandle, out ulong handle)
|
||||
{
|
||||
handle = 0;
|
||||
if (!KernelMemoryCompatExports.TryAllocateHleData(ctx, HandleStorageSize, 16, out handle))
|
||||
{
|
||||
return false;
|
||||
@@ -271,11 +297,18 @@ public static class Ngs2Exports
|
||||
|
||||
Span<byte> data = stackalloc byte[(int)HandleStorageSize];
|
||||
data.Clear();
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(data[0..8], handle);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(data[8..16], ownerHandle);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(data[16..20], 1);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(data[24..28], type);
|
||||
return ctx.Memory.TryWrite(handle, data);
|
||||
if (!ctx.Memory.TryWrite(handle, data))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Offset 0 doubles as a vtable slot for guest code that treats this handle as a C++
|
||||
// object; stamp it with the shared dummy vtable instead of a self-referential value.
|
||||
KernelMemoryCompatExports.TryWriteDummyVtable(ctx, handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryClearGuestBuffer(CpuContext ctx, ulong address, ulong length)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
|
||||
namespace SharpEmu.Libs.Np;
|
||||
|
||||
public static class NpTrophy2Exports
|
||||
{
|
||||
private static int _nextContext = 1;
|
||||
private static int _nextHandle = 1;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Bagshr7OQ6Q",
|
||||
ExportName = "sceNpTrophy2CreateContext",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpTrophy2")]
|
||||
public static int NpTrophy2CreateContext(CpuContext ctx)
|
||||
{
|
||||
return WriteIdAndReturn(ctx, ctx[CpuRegister.Rdi], ref _nextContext);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Gz1rmUZpROM",
|
||||
ExportName = "sceNpTrophy2CreateHandle",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpTrophy2")]
|
||||
public static int NpTrophy2CreateHandle(CpuContext ctx)
|
||||
{
|
||||
return WriteIdAndReturn(ctx, ctx[CpuRegister.Rdi], ref _nextHandle);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "sysY2FHYff4",
|
||||
ExportName = "sceNpTrophy2DestroyContext",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpTrophy2")]
|
||||
public static int NpTrophy2DestroyContext(CpuContext ctx) => ReturnOk(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "d8P11CI40KE",
|
||||
ExportName = "sceNpTrophy2DestroyHandle",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpTrophy2")]
|
||||
public static int NpTrophy2DestroyHandle(CpuContext ctx) => ReturnOk(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "fYapWA9xVmA",
|
||||
ExportName = "sceNpTrophy2AbortHandle",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpTrophy2")]
|
||||
public static int NpTrophy2AbortHandle(CpuContext ctx) => ReturnOk(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "bIDov3wBu5Q",
|
||||
ExportName = "sceNpTrophy2RegisterContext",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpTrophy2")]
|
||||
public static int NpTrophy2RegisterContext(CpuContext ctx) => ReturnOk(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "sUXGfNMalIo",
|
||||
ExportName = "sceNpTrophy2RegisterUnlockCallback",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpTrophy2")]
|
||||
public static int NpTrophy2RegisterUnlockCallback(CpuContext ctx) => ReturnOk(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "wVqxM58sIKs",
|
||||
ExportName = "sceNpTrophy2UnregisterUnlockCallback",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpTrophy2")]
|
||||
public static int NpTrophy2UnregisterUnlockCallback(CpuContext ctx) => ReturnOk(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "EHQEDVXZ0TI",
|
||||
ExportName = "sceNpTrophy2ShowTrophyList",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpTrophy2")]
|
||||
public static int NpTrophy2ShowTrophyList(CpuContext ctx) => ReturnOk(ctx);
|
||||
|
||||
private static int WriteIdAndReturn(CpuContext ctx, ulong outAddress, ref int nextId)
|
||||
{
|
||||
if (outAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> idBytes = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(idBytes, nextId);
|
||||
if (!ctx.Memory.TryWrite(outAddress, idBytes))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
nextId++;
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static int ReturnOk(CpuContext ctx) => SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
|
||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||
return (int)result;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,4 +72,26 @@ public static class SystemGestureExports
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "1MMK0W-kMgA",
|
||||
ExportName = "sceSystemGestureAppendTouchRecognizer",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSystemGesture")]
|
||||
public static int SystemGestureAppendTouchRecognizer(CpuContext ctx)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,4 +96,11 @@ public static class SystemServiceExports
|
||||
VulkanVideoPresenter.HideSplashScreen();
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "3s8cHiCBKBE",
|
||||
ExportName = "sceSystemServiceReportAbnormalTermination",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSystemService")]
|
||||
public static int SystemServiceReportAbnormalTermination(CpuContext ctx) => ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,9 @@ public static class UserServiceExports
|
||||
private const string PrimaryUserName = "SharpEmu";
|
||||
private static int _loginEventDelivered;
|
||||
|
||||
private static readonly bool _traceUserService =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_USER_SERVICE"), "1", StringComparison.Ordinal);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "j3YMu1MVNNo",
|
||||
ExportName = "sceUserServiceInitialize",
|
||||
@@ -42,9 +45,11 @@ public static class UserServiceExports
|
||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
||||
}
|
||||
|
||||
return ctx.TryWriteInt32(userIdAddress, PrimaryUserId)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
var result = ctx.TryWriteInt32(userIdAddress, PrimaryUserId)
|
||||
? 0
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
TraceUserService($"get_initial_user out=0x{userIdAddress:X16} value={PrimaryUserId} result=0x{result:X8}");
|
||||
return ctx.SetReturn(result);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -124,9 +129,12 @@ public static class UserServiceExports
|
||||
|
||||
Span<byte> output = stackalloc byte[nameBytes.Length + 1];
|
||||
nameBytes.CopyTo(output);
|
||||
return ctx.Memory.TryWrite(nameAddress, output)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
var result = ctx.Memory.TryWrite(nameAddress, output)
|
||||
? 0
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
TraceUserService(
|
||||
$"get_user_name user={userId} out=0x{nameAddress:X16} capacity=0x{capacity:X} value='{PrimaryUserName}' result=0x{result:X8}");
|
||||
return ctx.SetReturn(result);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -152,4 +160,12 @@ public static class UserServiceExports
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
private static void TraceUserService(string message)
|
||||
{
|
||||
if (_traceUserService)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] user_service.{message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ public static class VideoOutExports
|
||||
private const ulong SceVideoOutPixelFormatA2R10G10B10 = 0x88060000;
|
||||
private const ulong SceVideoOutPixelFormatA2R10G10B10Srgb = 0x88000000;
|
||||
private const ulong SceVideoOutPixelFormatA2R10G10B10Bt2020Pq = 0x88740000;
|
||||
private const ulong SceVideoOutInternalEventVblank = 0x5;
|
||||
private const ulong SceVideoOutInternalEventFlip = 0x6;
|
||||
private const short OrbisKernelEventFilterVideoOut = -13;
|
||||
|
||||
@@ -53,11 +54,17 @@ public static class VideoOutExports
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT_FPS"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
private static readonly bool _logVideoOutSync = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT_SYNC"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
private static long _frameRateWindowStart = Stopwatch.GetTimestamp();
|
||||
private static long _submittedFrameCount;
|
||||
private static long _presentedFrameCount;
|
||||
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))
|
||||
@@ -72,9 +79,10 @@ public static class VideoOutExports
|
||||
|
||||
var application = parts.Count == 0 ? "VideoOut" : string.Join(' ', parts);
|
||||
var versionSuffix = string.IsNullOrWhiteSpace(version) ? string.Empty : $" v{version.Trim()}";
|
||||
var commitSuffix = string.IsNullOrWhiteSpace(emulatorCommitSha) ? string.Empty : $" \u00b7 {emulatorCommitSha.Trim()}";
|
||||
lock (_stateGate)
|
||||
{
|
||||
_windowTitle = $"SharpEmu - {application}{versionSuffix}";
|
||||
_windowTitle = $"SharpEmu{commitSuffix} - {application}{versionSuffix}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +107,7 @@ public static class VideoOutExports
|
||||
public float Gamma { get; set; } = 1.0f;
|
||||
public VideoOutBufferGroup?[] Groups { get; } = new VideoOutBufferGroup?[MaxDisplayBufferGroups];
|
||||
public VideoOutBufferSlot[] BufferSlots { get; } = CreateBufferSlots();
|
||||
public List<FlipEventRegistration> VblankEvents { get; } = new();
|
||||
public List<FlipEventRegistration> FlipEvents { get; } = new();
|
||||
}
|
||||
|
||||
@@ -312,11 +321,48 @@ public static class VideoOutExports
|
||||
}
|
||||
|
||||
Thread.Sleep(1);
|
||||
lock (_stateGate)
|
||||
SignalVblank(port);
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Xru92wHJRmg",
|
||||
ExportName = "sceVideoOutAddVblankEvent",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceVideoOut")]
|
||||
public static int VideoOutAddVblankEvent(CpuContext ctx)
|
||||
{
|
||||
var equeue = ctx[CpuRegister.Rdi];
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
if (!TryGetPort(handle, out var port))
|
||||
{
|
||||
port.VblankCount++;
|
||||
return OrbisVideoOutErrorInvalidHandle;
|
||||
}
|
||||
|
||||
if (!KernelEventQueueCompatExports.IsValidEqueue(equeue))
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidEventQueue;
|
||||
}
|
||||
|
||||
var userData = ctx[CpuRegister.Rdx];
|
||||
lock (_stateGate)
|
||||
{
|
||||
var existingIndex = port.VblankEvents.FindIndex(registration => registration.Equeue == equeue);
|
||||
if (existingIndex >= 0)
|
||||
{
|
||||
port.VblankEvents[existingIndex] = new FlipEventRegistration(equeue, userData);
|
||||
}
|
||||
else
|
||||
{
|
||||
port.VblankEvents.Add(new FlipEventRegistration(equeue, userData));
|
||||
}
|
||||
}
|
||||
|
||||
// Some engines wait on this queue before issuing their first flip. Provide a first
|
||||
// edge now; later calls to WaitVblank advance the same notification sequence.
|
||||
SignalVblank(port);
|
||||
TraceVideoOut($"videoout.add_vblank_event eq=0x{equeue:X16} handle={handle} udata=0x{userData:X16}");
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -371,6 +417,53 @@ public static class VideoOutExports
|
||||
return SubmitFlip(ctx, handle, bufferIndex, flipMode, flipArg, submitGpuImage: true);
|
||||
}
|
||||
|
||||
// Struct layout matches the classic SceVideoOutFlipStatus (40 bytes):
|
||||
// count, processTime, tsc, flipArg, currentBuffer, flipPendingNum.
|
||||
[SysAbiExport(
|
||||
Nid = "SbU3dwp80lQ",
|
||||
ExportName = "sceVideoOutGetFlipStatus",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceVideoOut")]
|
||||
public static int VideoOutGetFlipStatus(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var statusAddress = ctx[CpuRegister.Rsi];
|
||||
if (statusAddress == 0)
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidAddress;
|
||||
}
|
||||
|
||||
VideoOutPortState? port;
|
||||
lock (_stateGate)
|
||||
{
|
||||
_ports.TryGetValue(handle, out port);
|
||||
}
|
||||
|
||||
if (port is null)
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidHandle;
|
||||
}
|
||||
|
||||
ulong count;
|
||||
long flipArg;
|
||||
uint currentBuffer;
|
||||
lock (_stateGate)
|
||||
{
|
||||
count = port.FlipCount;
|
||||
flipArg = 0;
|
||||
currentBuffer = unchecked((uint)port.CurrentBuffer);
|
||||
}
|
||||
|
||||
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x00, count);
|
||||
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x08, 0);
|
||||
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x10, 0);
|
||||
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x18, unchecked((ulong)flipArg));
|
||||
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x20, currentBuffer);
|
||||
|
||||
TraceVideoOut($"videoout.get_flip_status handle={handle} count={count} currentBuffer={currentBuffer}");
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "zgXifHT9ErY",
|
||||
ExportName = "sceVideoOutIsFlipPending",
|
||||
@@ -407,7 +500,8 @@ public static class VideoOutExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (filter != OrbisKernelEventFilterVideoOut || ident != SceVideoOutInternalEventFlip)
|
||||
if (filter != OrbisKernelEventFilterVideoOut ||
|
||||
ident is not (SceVideoOutInternalEventVblank or SceVideoOutInternalEventFlip))
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidEvent;
|
||||
}
|
||||
@@ -436,7 +530,8 @@ public static class VideoOutExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (filter != OrbisKernelEventFilterVideoOut || ident != SceVideoOutInternalEventFlip)
|
||||
if (filter != OrbisKernelEventFilterVideoOut ||
|
||||
ident is not (SceVideoOutInternalEventVblank or SceVideoOutInternalEventFlip))
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidEvent;
|
||||
}
|
||||
@@ -756,6 +851,38 @@ public static class VideoOutExports
|
||||
return groupIndex < 0 ? groupIndex : setIndex;
|
||||
}
|
||||
|
||||
private static void SignalVblank(VideoOutPortState port)
|
||||
{
|
||||
List<FlipEventRegistration> vblankEvents;
|
||||
ulong eventHint;
|
||||
lock (_stateGate)
|
||||
{
|
||||
port.VblankCount++;
|
||||
eventHint = SceVideoOutInternalEventVblank |
|
||||
((port.VblankCount & 0x0000_FFFF_FFFF_FFFFUL) << 16);
|
||||
vblankEvents = new List<FlipEventRegistration>(port.VblankEvents);
|
||||
}
|
||||
|
||||
var signalCount = Interlocked.Increment(ref _vblankSignalCount);
|
||||
|
||||
foreach (var vblankEvent in vblankEvents)
|
||||
{
|
||||
_ = KernelEventQueueCompatExports.TriggerDisplayEvent(
|
||||
vblankEvent.Equeue,
|
||||
SceVideoOutInternalEventVblank,
|
||||
OrbisKernelEventFilterVideoOut,
|
||||
eventHint,
|
||||
vblankEvent.UserData);
|
||||
}
|
||||
|
||||
if (_logVideoOutSync && (signalCount <= 8 || signalCount % 60 == 0))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][SYNC] vblank#{signalCount} handle={port.Handle} count={port.VblankCount} " +
|
||||
$"queues={vblankEvents.Count} hint=0x{eventHint:X16}");
|
||||
}
|
||||
}
|
||||
|
||||
private static int SubmitFlip(
|
||||
CpuContext ctx,
|
||||
int handle,
|
||||
@@ -790,11 +917,14 @@ public static class VideoOutExports
|
||||
flipEvents = new List<FlipEventRegistration>(port.FlipEvents);
|
||||
}
|
||||
|
||||
var guestImageSubmitted = false;
|
||||
ulong guestImageAddress = 0;
|
||||
if (submitGpuImage &&
|
||||
bufferIndex >= 0 &&
|
||||
TryGetDisplayBufferInfo(handle, bufferIndex, out var displayBuffer))
|
||||
{
|
||||
_ = VulkanVideoPresenter.TrySubmitGuestImage(
|
||||
guestImageAddress = displayBuffer.Address;
|
||||
guestImageSubmitted = VulkanVideoPresenter.TrySubmitGuestImage(
|
||||
displayBuffer.Address,
|
||||
displayBuffer.Width,
|
||||
displayBuffer.Height,
|
||||
@@ -819,6 +949,15 @@ public static class VideoOutExports
|
||||
flipEvent.UserData);
|
||||
}
|
||||
|
||||
var flipCount = Interlocked.Increment(ref _flipSubmitCount);
|
||||
if (_logVideoOutSync && (flipCount <= 8 || flipCount % 60 == 0))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][SYNC] flip#{flipCount} handle={handle} buffer={bufferIndex} " +
|
||||
$"addr=0x{guestImageAddress:X16} submitted={guestImageSubmitted} " +
|
||||
$"flipQueues={flipEvents.Count}");
|
||||
}
|
||||
|
||||
TraceVideoOut($"videoout.submit_flip handle={handle} index={bufferIndex} mode={flipMode} arg={flipArg} events={flipEvents.Count}");
|
||||
ReportFrameRate(presented: false);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
@@ -902,6 +1041,16 @@ public static class VideoOutExports
|
||||
TraceVideoOut(
|
||||
$"videoout.register_buffers handle={port.Handle} group={groupIndex} start={startIndex} count={addresses.Length} fmt=0x{attribute.PixelFormat:X} tile={attribute.TilingMode} {attribute.Width}x{attribute.Height} pitch={attribute.PitchInPixel}");
|
||||
VulkanVideoPresenter.EnsureStarted(attribute.Width, attribute.Height);
|
||||
|
||||
var guestFormat = MapPixelFormatToGuestTextureFormat(attribute.PixelFormat);
|
||||
if (guestFormat != 0)
|
||||
{
|
||||
foreach (var address in addresses)
|
||||
{
|
||||
VulkanVideoPresenter.RegisterKnownDisplayBuffer(address, guestFormat);
|
||||
}
|
||||
}
|
||||
|
||||
return groupIndex;
|
||||
}
|
||||
}
|
||||
@@ -1123,6 +1272,22 @@ public static class VideoOutExports
|
||||
? 4u
|
||||
: 0u;
|
||||
|
||||
// Maps the PS5 VideoOut pixel format space to the AGC "guest texture format" tags
|
||||
// VulkanVideoPresenter._availableGuestImages keys on (see VulkanVideoPresenter.
|
||||
// GetGuestTextureFormat: format=10 => 56 for 8-bit RGBA variants, format=9 => 9 for 10-bit).
|
||||
private static uint MapPixelFormatToGuestTextureFormat(ulong pixelFormat) =>
|
||||
NormalizePixelFormat(pixelFormat) switch
|
||||
{
|
||||
SceVideoOutPixelFormatA8R8G8B8Srgb or
|
||||
SceVideoOutPixelFormatA8B8G8R8Srgb or
|
||||
SceVideoOutPixelFormatB8G8R8A8Unorm or
|
||||
SceVideoOutPixelFormatR8G8B8A8Unorm => 56u,
|
||||
SceVideoOutPixelFormatA2R10G10B10 or
|
||||
SceVideoOutPixelFormatA2R10G10B10Srgb or
|
||||
SceVideoOutPixelFormatA2R10G10B10Bt2020Pq => 9u,
|
||||
_ => 0u,
|
||||
};
|
||||
|
||||
private static ulong NormalizePixelFormat(ulong pixelFormat)
|
||||
{
|
||||
if (GetBytesPerPixel(pixelFormat) != 0)
|
||||
|
||||
@@ -182,6 +182,13 @@ internal static unsafe class VulkanVideoPresenter
|
||||
private static long _enqueuedGuestWorkSequence;
|
||||
private static long _completedGuestWorkSequence;
|
||||
|
||||
private static bool ShouldTracePresentedGuestImageContentsForDiagnostics()
|
||||
{
|
||||
var mode = Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGES");
|
||||
return string.Equals(mode, "1", StringComparison.Ordinal) ||
|
||||
string.Equals(mode, "present", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static void EnsureStarted(uint width, uint height)
|
||||
{
|
||||
if (width == 0 || height == 0)
|
||||
@@ -279,6 +286,11 @@ internal static unsafe class VulkanVideoPresenter
|
||||
return;
|
||||
}
|
||||
|
||||
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] vk.submit_call kind=Submit {width}x{height}");
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
if (_closed)
|
||||
@@ -319,6 +331,11 @@ internal static unsafe class VulkanVideoPresenter
|
||||
return;
|
||||
}
|
||||
|
||||
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] vk.submit_call kind=SubmitGuestDraw({drawKind}) {width}x{height}");
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
if (_closed ||
|
||||
@@ -376,6 +393,12 @@ internal static unsafe class VulkanVideoPresenter
|
||||
return;
|
||||
}
|
||||
|
||||
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] vk.submit_call kind=SubmitTranslatedDraw {width}x{height} textures={textures.Count}");
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
if (_closed)
|
||||
@@ -442,6 +465,13 @@ internal static unsafe class VulkanVideoPresenter
|
||||
return;
|
||||
}
|
||||
|
||||
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] vk.submit_call kind=SubmitOffscreenTranslatedDraw " +
|
||||
$"target=0x{target.Address:X16} {target.Width}x{target.Height} textures={textures.Count}");
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
if (_closed)
|
||||
@@ -569,9 +599,31 @@ internal static unsafe class VulkanVideoPresenter
|
||||
var traceSubmission = false;
|
||||
lock (_gate)
|
||||
{
|
||||
if (_closed ||
|
||||
!_availableGuestImages.ContainsKey(address))
|
||||
var known = _availableGuestImages.ContainsKey(address);
|
||||
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] vk.submit_call kind=TrySubmitGuestImage addr=0x{address:X16} " +
|
||||
$"{width}x{height} known={known}");
|
||||
}
|
||||
|
||||
if (_closed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// The caller (VideoOutExports.SubmitFlip) reports the flip as successful either
|
||||
// way, so an unregistered address means the frame is dropped silently; warn once
|
||||
// per address so that shows up in the log.
|
||||
if (!known)
|
||||
{
|
||||
if (_tracedGuestImageSubmissions.Add((address, width, height)))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] vk.submit_guest_image_unknown addr=0x{address:X16} " +
|
||||
$"{width}x{height} - flip target was never registered as a render output");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -588,6 +640,19 @@ internal static unsafe class VulkanVideoPresenter
|
||||
RequiredGuestWorkSequence: 0,
|
||||
IsSplash: false,
|
||||
GuestImageAddress: address);
|
||||
if (_thread is not null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
_windowWidth = width;
|
||||
_windowHeight = height;
|
||||
_thread = new Thread(Run)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "SharpEmu Vulkan VideoOut",
|
||||
};
|
||||
_thread.Start();
|
||||
}
|
||||
|
||||
if (traceSubmission)
|
||||
@@ -601,6 +666,21 @@ internal static unsafe class VulkanVideoPresenter
|
||||
return true;
|
||||
}
|
||||
|
||||
// Display buffers registered through sceVideoOutRegisterBuffers are valid flip targets
|
||||
// even when no AGC render-target write to them was ever observed.
|
||||
internal static void RegisterKnownDisplayBuffer(ulong address, uint guestFormat)
|
||||
{
|
||||
if (address == 0 || guestFormat == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
_availableGuestImages[address] = guestFormat;
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool IsGpuGuestImageAvailable(
|
||||
ulong address,
|
||||
uint format,
|
||||
@@ -5033,14 +5113,19 @@ internal static unsafe class VulkanVideoPresenter
|
||||
_renderPass,
|
||||
_extent);
|
||||
if (ShouldTracePresentedGuestImageContentsForDiagnostics() &&
|
||||
!_firstGuestDrawPresented &&
|
||||
translatedResources.Textures is
|
||||
[
|
||||
{ GuestImage: { } guestImage },
|
||||
] &&
|
||||
_tracedGuestImageContents.Add(guestImage.Address))
|
||||
!_firstGuestDrawPresented)
|
||||
{
|
||||
TraceGuestImageContents(guestImage);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] vk.translated_draw kind={presentation.DrawKind} " +
|
||||
$"textures={translatedResources.Textures.Length}");
|
||||
foreach (var boundTexture in translatedResources.Textures)
|
||||
{
|
||||
if (boundTexture.GuestImage is { } guestImage &&
|
||||
_tracedGuestImageContents.Add(guestImage.Address))
|
||||
{
|
||||
TraceGuestImageContents(guestImage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
@@ -5810,13 +5895,6 @@ internal static unsafe class VulkanVideoPresenter
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool ShouldTracePresentedGuestImageContentsForDiagnostics()
|
||||
{
|
||||
var mode = Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGES");
|
||||
return string.Equals(mode, "1", StringComparison.Ordinal) ||
|
||||
string.Equals(mode, "present", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool ShouldTraceVulkanResources() =>
|
||||
string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VK_RESOURCES"),
|
||||
|
||||
@@ -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