Compare commits

..

1 Commits

Author SHA1 Message Date
ParantezTech 8c09b170b6 [shader-decoder] Fix address calculation for SW linear textures 2026-07-11 05:07:29 +03:00
130 changed files with 1236 additions and 25023 deletions
+3 -18
View File
@@ -16,11 +16,6 @@ body:
Example title:
Demons Souls (PPSA01341)
**Before reporting:**
- Test the game on a **fresh build from `main`**, not an old release.
- Copy the **exact commit SHA** shown at the top of the log.
- `DEBUG` and `TRACE` lines are diagnostic context, not errors by themselves; report the last milestone or the first `WARNING`, `ERROR`, or `CRITICAL` line connected to the stop.
- type: input
id: game_name
attributes:
@@ -78,20 +73,11 @@ body:
id: logs
attributes:
label: Log File
description: Attach the **full** log file (`.log`, `.txt`, or `.zip`). Do not paste only a fragment — the maintainer needs the complete log from launch to crash.
description: Drag and drop your log file here, or click to browse
validations:
required: true
required: false
accept: ".log,.txt,.zip"
- type: input
id: last_milestone_first_error
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: ..."
validations:
required: true
- type: dropdown
id: operating_system
attributes:
@@ -133,8 +119,7 @@ body:
id: emulator_version
attributes:
label: Emulator Version / Commit
description: Copy the **exact short SHA** from the top of the log. Do not enter just `0.0.1` without a commit hash.
placeholder: e.g. a1b2c3d
placeholder: e.g. v0.0.1 / a1b2c3d
validations:
required: true
Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 142 KiB

-1
View File
@@ -39,4 +39,3 @@ Desktop.ini
ehthumbs.db
.vs/
.idea/
-70
View File
@@ -1,70 +0,0 @@
<!--
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.
-14
View File
@@ -32,18 +32,4 @@ SPDX-License-Identifier: GPL-2.0-or-later
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
</PropertyGroup>
<!-- Build provenance surfaced by SharpEmu.Logging.BuildInfo as a banner at the
top of the log. Populated from GitHub Actions environment variables when
building in CI; empty locally. -->
<ItemGroup>
<AssemblyMetadata Include="SharpEmu.BuildConfiguration" Value="$(Configuration)" />
<AssemblyMetadata Condition="'$(GITHUB_SHA)' != ''" Include="SharpEmu.BuildSha" Value="$(GITHUB_SHA)" />
<AssemblyMetadata Condition="'$(GITHUB_REF_NAME)' != ''" Include="SharpEmu.BuildBranch" Value="$(GITHUB_REF_NAME)" />
<AssemblyMetadata Condition="'$(GITHUB_REF)' != ''" Include="SharpEmu.BuildRef" Value="$(GITHUB_REF)" />
<AssemblyMetadata Condition="'$(GITHUB_EVENT_NAME)' != ''" Include="SharpEmu.BuildEventName" Value="$(GITHUB_EVENT_NAME)" />
<AssemblyMetadata Condition="'$(GITHUB_REPOSITORY)' != ''" Include="SharpEmu.BuildRepository" Value="$(GITHUB_REPOSITORY)" />
<AssemblyMetadata Condition="'$(GITHUB_SERVER_URL)' != ''" Include="SharpEmu.BuildServerUrl" Value="$(GITHUB_SERVER_URL)" />
<AssemblyMetadata Condition="'$(GITHUB_RUN_ID)' != ''" Include="SharpEmu.BuildRunId" Value="$(GITHUB_RUN_ID)" />
</ItemGroup>
</Project>
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2018 Alex Barney
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
BIN
View File
Binary file not shown.
-7
View File
@@ -8,7 +8,6 @@ path = [
"**/packages.lock.json",
"scripts/ps5_names.txt",
"src/SharpEmu.HLE/Aerolib/aerolib.bin",
"src/SharpEmu.GUI/Languages/**",
"_logs/**",
".github/images/**",
"assets/images/**"
@@ -16,9 +15,3 @@ path = [
precedence = "aggregate"
SPDX-FileCopyrightText = "SharpEmu Emulator Project"
SPDX-License-Identifier = "GPL-2.0-or-later"
[[annotations]]
path = "src/SharpEmu.GUI/Atrac9/**"
precedence = "aggregate"
SPDX-FileCopyrightText = "2018 Alex Barney"
SPDX-License-Identifier = "MIT"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "10.0.103",
"rollForward": "disable"
"rollForward": "latestFeature"
}
}
+6 -7
View File
@@ -3,8 +3,8 @@
#!/usr/bin/env python3
import hashlib
import struct
import hashlib
from base64 import b64encode as base64enc
from binascii import unhexlify as uhx
from pathlib import Path
@@ -21,7 +21,7 @@ def name2nid(name):
def generate():
names_path = Path(NAMES)
output_path = Path(OUTPUT)
entries = []
with open(names_path, 'r', encoding='utf-8') as f:
for line in f:
@@ -29,12 +29,12 @@ def generate():
if name:
nid = name2nid(name)
entries.append((nid, name))
print(f"Found {len(entries)} entries")
data = bytearray()
data.extend(struct.pack('<I', len(entries)))
for nid, name in entries:
nid_bytes = nid.encode('utf-8')
name_bytes = name.encode('utf-8')
@@ -42,11 +42,10 @@ def generate():
data.extend(nid_bytes)
data.extend(struct.pack('<H', len(name_bytes)))
data.extend(name_bytes)
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, 'wb') as f:
f.write(data)
print(f"Generated: {output_path} ({len(data):,} bytes)")
print(f"Total entries: {len(entries)}")
File diff suppressed because it is too large Load Diff
+14 -431
View File
@@ -8,15 +8,12 @@ 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;
@@ -24,15 +21,11 @@ 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,19 +36,6 @@ internal static partial class Program
[STAThread]
private static int Main(string[] args)
{
try
{
return Run(args);
}
finally
{
DropConsoleFileMirror();
SharpEmuLog.Shutdown();
}
}
private static int Run(string[] args)
{
args = NormalizeInternalArguments(args, out var isMitigatedChild);
if (args.Length == 0 && !isMitigatedChild)
@@ -68,11 +48,6 @@ internal static partial class Program
// The executable uses the GUI subsystem, so CLI mode has to connect
// 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");
@@ -81,25 +56,17 @@ internal static partial class Program
return childExitCode;
}
if (!TryParseArguments(args, out var ebootPath, out var runtimeOptions, out var logLevel, out var logFilePath))
if (!TryParseArguments(args, out var ebootPath, out var runtimeOptions, out var logLevel))
{
PrintUsage();
return 1;
}
if (!isMitigatedChild && !string.IsNullOrWhiteSpace(logFilePath))
{
TryEnableConsoleFileMirror(logFilePath);
}
SharpEmuLog.MinimumLevel = logLevel;
Log.Info(BuildInfo.Banner);
Log.Info(HostSystemInfo.Summary);
ebootPath = Path.GetFullPath(ebootPath);
Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}");
if (!File.Exists(ebootPath))
{
Log.Error($"EBOOT file was not found: {ebootPath}");
@@ -181,48 +148,6 @@ internal static partial class Program
RebindStdHandleToConsole(STD_ERROR_HANDLE);
}
/// <summary>
/// Makes console writes UTF-8 so the GUI's pipe reader (and any modern
/// terminal) decodes non-ASCII text correctly. Without this, redirected
/// output falls back to the OS ANSI code page and characters like "—"
/// arrive mangled.
/// </summary>
private static void UseUtf8ConsoleOutput()
{
try
{
// Also recreates the redirected Console.Out/Error writers with
// the new encoding.
Console.OutputEncoding = Encoding.UTF8;
return;
}
catch (Exception)
{
// No attached console (GUI-subsystem child with piped output):
// wrap the raw handles instead.
}
if (Console.IsOutputRedirected)
{
Console.SetOut(new StreamWriter(
Console.OpenStandardOutput(),
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false))
{
AutoFlush = true,
});
}
if (Console.IsErrorRedirected)
{
Console.SetError(new StreamWriter(
Console.OpenStandardError(),
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false))
{
AutoFlush = true,
});
}
}
private static void RebindStdHandleToConsole(int stdHandle)
{
if (IsHandleValid(GetStdHandle(stdHandle)) || GetConsoleWindow() == 0)
@@ -252,10 +177,6 @@ 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;
@@ -266,7 +187,7 @@ internal static partial class Program
{
if (string.Equals(arg, MitigatedChildFlag, StringComparison.Ordinal))
{
isMitigatedChild = trustedMitigatedChild;
isMitigatedChild = true;
continue;
}
@@ -305,11 +226,9 @@ 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;
@@ -317,9 +236,7 @@ internal static partial class Program
attributeList = Marshal.AllocHGlobal((nint)attributeListSize);
if (!InitializeProcThreadAttributeList(attributeList, 1, 0, ref attributeListSize))
{
childExitCode = 5;
Console.Error.WriteLine($"[ERROR] Failed to initialize mitigation attributes: {Marshal.GetLastWin32Error()}");
return true;
return false;
}
startupInfoEx.lpAttributeList = attributeList;
@@ -327,7 +244,8 @@ 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_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF |
PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF;
mitigationPolicies = Marshal.AllocHGlobal(sizeof(ulong) * 2);
Marshal.WriteInt64(mitigationPolicies, unchecked((long)policy1));
@@ -342,31 +260,24 @@ internal static partial class Program
0,
0))
{
childExitCode = 5;
Console.Error.WriteLine($"[ERROR] Failed to apply mitigation attributes: {Marshal.GetLastWin32Error()}");
return true;
return false;
}
var cmdLineBuilder = new StringBuilder(commandLine);
nint jobHandle = 0;
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, "1");
var created = CreateProcessW(
if (!CreateProcessW(
processPath,
cmdLineBuilder,
0,
0,
true,
false,
EXTENDED_STARTUPINFO_PRESENT,
0,
Environment.CurrentDirectory,
ref startupInfoEx,
out var processInfo);
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, previousChildEnvironment);
if (!created)
out var processInfo))
{
childExitCode = 5;
Console.Error.WriteLine($"[ERROR] Failed to launch mitigated child process: {Marshal.GetLastWin32Error()}");
return true;
return false;
}
try
@@ -420,8 +331,6 @@ internal static partial class Program
}
finally
{
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, previousChildEnvironment);
if (attributeList != 0)
{
DeleteProcThreadAttributeList(attributeList);
@@ -435,238 +344,6 @@ 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();
@@ -769,30 +446,27 @@ internal static partial class Program
private static void PrintUsage()
{
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""");
Log.Info("Usage: SharpEmu.CLI [--strict] [--trace-imports[=N]] [--cpu-engine=<native>] [--log-level=<level>] <path-to-eboot.bin>");
Log.Info(@"Example: SharpEmu.CLI --cpu-engine=native --trace-imports=64 --log-level=debug ""E:\Games\...\eboot.bin""");
}
private static bool TryParseArguments(
string[] args,
out string ebootPath,
out SharpEmuRuntimeOptions runtimeOptions,
out LogLevel logLevel,
out string? logFilePath)
out LogLevel logLevel)
{
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++)
@@ -822,7 +496,6 @@ internal static partial class Program
{
ebootPath = string.Empty;
runtimeOptions = default;
logFilePath = null;
return false;
}
@@ -836,7 +509,6 @@ internal static partial class Program
{
ebootPath = string.Empty;
runtimeOptions = default;
logFilePath = null;
return false;
}
@@ -844,23 +516,6 @@ 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))
{
@@ -884,7 +539,6 @@ internal static partial class Program
ebootPath = string.Empty;
runtimeOptions = default;
logLevel = SharpEmuLog.MinimumLevel;
logFilePath = null;
return false;
}
@@ -907,27 +561,11 @@ 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;
}
@@ -939,7 +577,6 @@ internal static partial class Program
ebootPath = string.Empty;
runtimeOptions = default;
logLevel = SharpEmuLog.MinimumLevel;
logFilePath = null;
return false;
}
@@ -1041,56 +678,6 @@ 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(
@@ -1175,10 +762,6 @@ 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,
-8
View File
@@ -24,9 +24,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Version>0.0.1</Version>
<ServerGarbageCollection>true</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
<TieredPGO>true</TieredPGO>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
@@ -51,11 +48,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
<TargetPath>LICENSE.txt</TargetPath>
<Visible>False</Visible>
</Content>
<Content Include="..\SharpEmu.GUI\Languages\*.json" Condition="'$(Configuration)' != 'Release'">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<TargetPath>Languages\%(Filename)%(Extension)</TargetPath>
<Visible>False</Visible>
</Content>
</ItemGroup>
<!-- Keep glfw as a loose file next to the executable; every other native
+4 -8
View File
@@ -4,9 +4,9 @@
"net10.0": {
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "0B6nZyCHWXnvmlB559oduOspVdNOnpNXPjhpWVMovLPAsDVG7A4jJR9rzECf67JUzxP8/ee/wA8clwIzJcWNFA=="
"requested": "[10.0.9, )",
"resolved": "10.0.9",
"contentHash": "4Iw41e2h7I4t70SJcX2GCmbyKJIlA273Cfm9RJMM050/3VBejGAG1KcthP5Z2L6SQcbfbf6BhNWO26+ZG+GzMg=="
},
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
@@ -211,15 +211,11 @@
"Avalonia.Desktop": "[11.3.18, )",
"Avalonia.Fonts.Inter": "[11.3.18, )",
"Avalonia.Themes.Fluent": "[11.3.18, )",
"SharpEmu.Logging": "[1.0.0, )",
"Tmds.DBus.Protocol": "[0.21.3, )"
}
},
"sharpemu.hle": {
"type": "Project",
"dependencies": {
"SharpEmu.Logging": "[1.0.0, )"
}
"type": "Project"
},
"sharpemu.libs": {
"type": "Project",
+10 -11
View File
@@ -7,14 +7,11 @@ using SharpEmu.Core.Cpu.Native;
using SharpEmu.Core.Loader;
using SharpEmu.Core.Memory;
using SharpEmu.HLE;
using SharpEmu.Logging;
namespace SharpEmu.Core.Cpu;
public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
{
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Dispatcher");
private enum EntryFrameKind
{
ProcessEntry,
@@ -83,8 +80,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
string processImageName = "eboot.bin",
CpuExecutionOptions executionOptions = default)
{
Log.Debug("=== DispatchEntry START ===");
Log.Debug($"entryPoint=0x{entryPoint:X16}, generation={generation}");
Console.Error.WriteLine("[DISPATCHER] === DispatchEntry START ===");
Console.Error.WriteLine($"[DISPATCHER] entryPoint=0x{entryPoint:X16}, generation={generation}");
try
{
@@ -92,7 +89,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
}
catch (Exception ex)
{
Log.Critical($"FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}", ex);
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}");
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
throw;
}
}
@@ -105,8 +103,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
string moduleName = "module",
CpuExecutionOptions executionOptions = default)
{
Log.Debug("=== DispatchModuleInitializer START ===");
Log.Debug($"moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}");
Console.Error.WriteLine("[DISPATCHER] === DispatchModuleInitializer START ===");
Console.Error.WriteLine($"[DISPATCHER] moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}");
try
{
@@ -121,7 +119,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
}
catch (Exception ex)
{
Log.Critical($"FATAL EXCEPTION in DispatchModuleInitializer: {ex.GetType().Name}: {ex.Message}", ex);
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchModuleInitializer: {ex.GetType().Name}: {ex.Message}");
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
throw;
}
}
@@ -135,7 +134,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
CpuExecutionOptions executionOptions = default,
EntryFrameKind frameKind = EntryFrameKind.ProcessEntry)
{
Log.Debug("DispatchEntryCore STARTING...");
Console.Error.WriteLine("[DISPATCHER] DispatchEntryCore STARTING...");
LastEntryPoint = entryPoint;
LastTrapInfo = null;
@@ -307,7 +306,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
LastMilestoneLog,
Environment.NewLine,
$"CpuEngine native-only failed: {backendError}");
Log.Error($"Native backend FAILED: {backendError}");
Console.Error.WriteLine($"[DISPATCHER] Native backend FAILED: {backendError}");
return FailEarly(
OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED,
CpuExitReason.NativeBackendUnavailable);
@@ -8,7 +8,6 @@ using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.HLE;
using SharpEmu.Logging;
namespace SharpEmu.Core.Cpu.Native;
@@ -38,78 +37,13 @@ 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)
{
return;
}
Log.Info($" Recent import calls ({_recentImportTraceCount}):");
Console.Error.WriteLine($"[LOADER][INFO] Recent import calls ({_recentImportTraceCount}):");
int num = (_recentImportTraceWriteIndex - _recentImportTraceCount + _recentImportTrace.Length) % _recentImportTrace.Length;
for (int i = 0; i < _recentImportTraceCount; i++)
{
@@ -117,8 +51,8 @@ public sealed partial class DirectExecutionBackend
var entry = _recentImportTrace[num2];
if (!string.IsNullOrEmpty(entry.Nid))
{
Log.Info(
$" #{entry.DispatchIndex} nid={entry.Nid} ret=0x{entry.ReturnRip:X16} " +
Console.Error.WriteLine(
$"[LOADER][INFO] #{entry.DispatchIndex} nid={entry.Nid} ret=0x{entry.ReturnRip:X16} " +
$"rdi=0x{entry.Arg0:X16} rsi=0x{entry.Arg1:X16} rdx=0x{entry.Arg2:X16}");
}
}
@@ -157,12 +91,12 @@ public sealed partial class DirectExecutionBackend
list.Add(num7);
if (num2 < 32)
{
Log.Info($"Suspicious unresolved pointer: slot=0x{num7:X16} value=0x{value2:X16}");
Console.Error.WriteLine($"[LOADER][INFO] Suspicious unresolved pointer: slot=0x{num7:X16} value=0x{value2:X16}");
num2++;
}
if (num >= 16384)
{
Log.Warning($"Suspicious unresolved pointer scan reached cap ({16384}); truncating.");
Console.Error.WriteLine($"[LOADER][WARNING] Suspicious unresolved pointer scan reached cap ({16384}); truncating.");
return list;
}
}
@@ -172,7 +106,7 @@ public sealed partial class DirectExecutionBackend
}
if (num != 0)
{
Log.Warning($"Suspicious unresolved pointer hits: {num}");
Console.Error.WriteLine($"[LOADER][WARNING] Suspicious unresolved pointer hits: {num}");
}
return list;
}
@@ -187,18 +121,18 @@ public sealed partial class DirectExecutionBackend
Span<byte> destination = stackalloc byte[128];
if (!cpuContext.Memory.TryRead(returnRip, destination))
{
Log.Debug($"Import#{dispatchIndex} return-rip probe: unreadable @0x{returnRip:X16}");
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip probe: unreadable @0x{returnRip:X16}");
return;
}
string value = BitConverter.ToString(destination.ToArray()).Replace("-", " ");
Log.Debug($"Import#{dispatchIndex} return-rip bytes @0x{returnRip:X16}: {value}");
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip bytes @0x{returnRip:X16}: {value}");
if (destination[0] == byte.MaxValue && (destination[1] == 21 || destination[1] == 37))
{
int num = BitConverter.ToInt32(destination.Slice(2, 4));
ulong num2 = returnRip + 6 + (ulong)num;
if (cpuContext.TryReadUInt64(num2, out var value2))
{
Log.Debug($"Import#{dispatchIndex} return-rip slot: [0x{num2:X16}] = 0x{value2:X16}");
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip slot: [0x{num2:X16}] = 0x{value2:X16}");
}
}
if (destination[0] == 72 && destination[1] == 139 && destination[2] == 5)
@@ -207,7 +141,7 @@ public sealed partial class DirectExecutionBackend
ulong num4 = returnRip + 7 + (ulong)num3;
if (cpuContext.TryReadUInt64(num4, out var value3))
{
Log.Debug($"Import#{dispatchIndex} return-rip mov-slot: [0x{num4:X16}] = 0x{value3:X16}");
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip mov-slot: [0x{num4:X16}] = 0x{value3:X16}");
}
}
for (int i = 0; i + 6 <= destination.Length; i++)
@@ -219,7 +153,7 @@ public sealed partial class DirectExecutionBackend
ulong num7 = num6 + 6 + (ulong)num5;
if (cpuContext.TryReadUInt64(num7, out var value4))
{
Log.Debug($"Import#{dispatchIndex} near-indirect @{num6:X16}: slot=0x{num7:X16} val=0x{value4:X16}");
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} near-indirect @{num6:X16}: slot=0x{num7:X16} val=0x{value4:X16}");
}
}
}
@@ -234,33 +168,32 @@ public sealed partial class DirectExecutionBackend
int rel32 = BitConverter.ToInt32(destination.Slice(i + 1, 4));
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}");
var importEntries = _importEntries;
for (int importIndex = 0; importIndex < importEntries.Length; importIndex++)
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} near-call @{callRip:X16}: target=0x{target:X16}");
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(
$"Import#{dispatchIndex} near-call import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
Console.Error.WriteLine(
$"[LOADER][TRACE] Import#{dispatchIndex} near-call import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
}
else
{
Log.Debug(
$"Import#{dispatchIndex} near-call import: index={importIndex} nid={nid}");
Console.Error.WriteLine(
$"[LOADER][TRACE] Import#{dispatchIndex} near-call import: index={importIndex} nid={nid}");
}
break;
}
if (cpuContext.Memory.TryRead(target, targetBytes))
{
Log.Debug(
$"Import#{dispatchIndex} near-call target bytes @0x{target:X16}: " +
Console.Error.WriteLine(
$"[LOADER][TRACE] Import#{dispatchIndex} near-call target bytes @0x{target:X16}: " +
BitConverter.ToString(targetBytes.ToArray()).Replace("-", " "));
if (targetBytes[0] == 0xFF && targetBytes[1] == 0x25)
{
@@ -268,25 +201,25 @@ public sealed partial class DirectExecutionBackend
ulong slot = unchecked((ulong)((long)(target + 6) + slotRel32));
if (cpuContext.TryReadUInt64(slot, out var slotTarget))
{
Log.Debug(
$"Import#{dispatchIndex} near-call PLT slot: [0x{slot:X16}] = 0x{slotTarget:X16}");
for (int importIndex = 0; importIndex < importEntries.Length; importIndex++)
Console.Error.WriteLine(
$"[LOADER][TRACE] Import#{dispatchIndex} near-call PLT slot: [0x{slot:X16}] = 0x{slotTarget:X16}");
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(
$"Import#{dispatchIndex} near-call PLT import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
Console.Error.WriteLine(
$"[LOADER][TRACE] Import#{dispatchIndex} near-call PLT import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
}
else
{
Log.Debug(
$"Import#{dispatchIndex} near-call PLT import: index={importIndex} nid={nid}");
Console.Error.WriteLine(
$"[LOADER][TRACE] Import#{dispatchIndex} near-call PLT import: index={importIndex} nid={nid}");
}
break;
}
@@ -103,15 +103,6 @@ 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,7 +8,6 @@ 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;
@@ -20,16 +19,6 @@ 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))
@@ -47,13 +36,6 @@ 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)
@@ -87,12 +69,6 @@ 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;
@@ -111,25 +87,18 @@ public sealed partial class DirectExecutionBackend
LastError = "Import dispatch called without active CPU context";
return 18446744071562199298uL;
}
var importEntries = _importEntries;
if ((uint)importIndex >= (uint)importEntries.Length)
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)
{
Console.Error.WriteLine($"[LOADER][TRACE] Raw sentinel recoveries: {num2} (last import index={importIndex})");
_lastReportedRawSentinelRecoveries = num2;
}
// Leaf imports take a scalar-only fast path that reads its own operands and
// intentionally bypasses the SysV variadic XMM marshalling below. This is safe
// only while the leaf set contains no float-variadic or float-returning function.
// The constraint and the audited list live on IsLeafImport — do not add a
// function that consumes xmm0-7 args or returns in xmm0 to the leaf set;
// such functions must stay on the full gateway path below.
if (IsLeafImport(importStubEntry.Nid) &&
TryDispatchLeafImport(cpuContext, importStubEntry, argPackPtr, num, out var leafResult))
{
@@ -149,26 +118,11 @@ public sealed partial class DirectExecutionBackend
cpuContext[CpuRegister.R13] = *(ulong*)(argPackPtr + 72);
cpuContext[CpuRegister.R14] = *(ulong*)(argPackPtr + 80);
cpuContext[CpuRegister.R15] = *(ulong*)(argPackPtr + 88);
// The trampoline spills the SysV variadic XMM save area (xmm0..xmm7) into the
// 0x80 bytes immediately below the GP argpack, so variadic float args (printf
// %f, and powf/logf inputs) reach the handler. xmm{i} is at argPackPtr-0x80+i*16.
for (var xmmIndex = 0; xmmIndex < 8; xmmIndex++)
{
var xmmSlot = argPackPtr - 0x80 + (xmmIndex * 16);
cpuContext.SetXmmRegister(
xmmIndex,
*(ulong*)xmmSlot,
*(ulong*)(xmmSlot + 8));
}
cpuContext.SetXmmRegister(
0,
*(ulong*)(argPackPtr - 16),
*(ulong*)(argPackPtr - 8));
cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL;
if (importStubEntry.Kind == ImportStubKind.BootstrapBridge)
{
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];
@@ -219,6 +173,16 @@ 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) &&
@@ -227,17 +191,6 @@ 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;
@@ -245,13 +198,12 @@ public sealed partial class DirectExecutionBackend
bool flag4 = !string.IsNullOrWhiteSpace(_importFilter);
bool flag5 = false;
ExportedFunction? matchedExport = importStubEntry.Export;
var traceFlags = importStubEntry.TraceFlags;
bool periodicTrace = num <= 128 ||
(num >= 240 && num <= 400) ||
(num >= 900 && num <= 1300) ||
num % 100000 == 0L ||
((traceFlags & ImportStubTraceFlags.PeriodicEvery1000) != 0 && (num <= 256 || num % 1000 == 0L)) ||
((traceFlags & ImportStubTraceFlags.PeriodicEvery128) != 0 && (num <= 256 || num % 128 == 0)) ||
(importStubEntry.Nid == "tsvEmnenz48" && (num <= 256 || num % 1000 == 0L)) ||
(importStubEntry.Nid == "rTXw65xmLIA" && (num <= 256 || num % 128 == 0)) ||
flag ||
flag2 ||
flag3;
@@ -312,15 +264,15 @@ public sealed partial class DirectExecutionBackend
cpuContext[CpuRegister.Rsi],
cpuContext[CpuRegister.Rdx]);
}
if ((traceFlags & ImportStubTraceFlags.Memset) != 0 && num <= 256)
if (importStubEntry.Nid == "8zTFvBIAIN8" && num <= 256)
{
Console.Error.WriteLine($"[LOADER][TRACE] memset#{num}: dst=0x{cpuContext[CpuRegister.Rdi]:X16} val=0x{cpuContext[CpuRegister.Rsi] & 0xFF:X2} len=0x{cpuContext[CpuRegister.Rdx]:X16} ret=0x{num7:X16}");
}
if ((traceFlags & ImportStubTraceFlags.CxaAtexit) != 0 && num <= 64)
if (importStubEntry.Nid == "tsvEmnenz48" && num <= 64)
{
Console.Error.WriteLine($"[LOADER][TRACE] __cxa_atexit#{num}: func=0x{cpuContext[CpuRegister.Rdi]:X16} arg=0x{cpuContext[CpuRegister.Rsi]:X16} dso=0x{cpuContext[CpuRegister.Rdx]:X16} ret=0x{num7:X16}");
}
if ((traceFlags & ImportStubTraceFlags.RawArgs) != 0)
if (importStubEntry.Nid == "bzQExy189ZI" || importStubEntry.Nid == "8G2LB+A3rzg")
{
Console.Error.WriteLine($"[LOADER][TRACE] {importStubEntry.Nid}#{num}: rdi=0x{cpuContext[CpuRegister.Rdi]:X16} rsi=0x{cpuContext[CpuRegister.Rsi]:X16} rdx=0x{cpuContext[CpuRegister.Rdx]:X16} ret=0x{num7:X16}");
}
@@ -350,7 +302,7 @@ public sealed partial class DirectExecutionBackend
Console.Error.Flush();
}
}
if ((traceFlags & ImportStubTraceFlags.StackChkFail) != 0)
if (importStubEntry.Nid == "Ou3iL1abvng")
{
if (_logStackCheck)
{
@@ -382,20 +334,16 @@ public sealed partial class DirectExecutionBackend
ActiveGuestReturnSlotAddress);
try
{
if (importStubEntry.Kind == ImportStubKind.BootstrapBridge)
if (string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
{
if (_logBootstrap)
{
RecordDeferredBootstrapTrace(num, value, value2, num3, num7);
}
orbisGen2Result = DispatchBootstrapBridge();
}
else if (importStubEntry.Kind == ImportStubKind.KernelDynlibDlsym)
else if (string.Equals(importStubEntry.Nid, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal) ||
string.Equals(importStubEntry.Nid, "LwG8g3niqwA", StringComparison.Ordinal))
{
orbisGen2Result = DispatchKernelDynlibDlsym();
}
else if (importStubEntry.Kind == ImportStubKind.Il2CppApiLookupSymbol)
else if (string.Equals(importStubEntry.Nid, "r8mvOaWdi28", StringComparison.Ordinal))
{
orbisGen2Result = DispatchIl2CppApiLookupSymbol();
}
@@ -543,14 +491,6 @@ public sealed partial class DirectExecutionBackend
Console.Error.Flush();
}
}
// Publish the handler's XMM0 back into the argpack's xmm0 save slot; the
// trampoline epilogue reloads it into the guest's XMM0, delivering float/double
// return values (powf/logf/wcstod). Harmless for int/pointer returns (XMM is
// volatile across a SysV call, so the guest never relies on a preserved XMM0).
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)
@@ -559,7 +499,6 @@ 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;
}
}
@@ -683,13 +622,6 @@ public sealed partial class DirectExecutionBackend
return true;
}
// Subset of the leaf set that additionally skips the import-call-frame
// bookkeeping. The same scalar-only (no XMM args, no XMM return) constraint
// as IsLeafImport applies — see the note there before adding entries.
// NOTE: this filter is only consulted after IsLeafImport accepts the NID;
// entries listed here but not in IsLeafImport (scePadRead, scePadOpen,
// sceUserServiceGetEvent, sceUserServiceGetPlatformPrivacySetting,
// pthread_mutex_trylock) currently take the full gateway path.
private static bool IsNoBlockLeafImport(string nid) =>
nid is
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
@@ -712,12 +644,11 @@ 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
"D-CzAxQL0XI" or // sceUserServiceGetPlatformPrivacySetting
"K-jXhbt2gn4"; // pthread_mutex_trylock (scePthreadMutexTrylock is upoVrzMHFeE)
"K-jXhbt2gn4"; // scePthreadMutexTrylock
private bool ShouldLogImportResult(string nid, OrbisGen2Result result)
{
@@ -736,9 +667,6 @@ public sealed partial class DirectExecutionBackend
var expectedEqueueTimeout =
string.Equals(nid, "fzyMKs9kim0", StringComparison.Ordinal) &&
result == OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
var expectedEventFlagTimeout =
string.Equals(nid, "JTvBflhYazQ", StringComparison.Ordinal) &&
result == OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
var expectedMutexTrylockBusy =
string.Equals(nid, "K-jXhbt2gn4", StringComparison.Ordinal) &&
result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
@@ -751,7 +679,6 @@ public sealed partial class DirectExecutionBackend
if (!expectedFileProbeMiss &&
!expectedTimedWaitTimeout &&
!expectedEqueueTimeout &&
!expectedEventFlagTimeout &&
!expectedMutexTrylockBusy &&
!expectedUserServiceNoEvent &&
!expectedPrivacyInvalidParameter)
@@ -788,45 +715,22 @@ public sealed partial class DirectExecutionBackend
"1G3lF1Gg1k8" or // sceKernelOpen
"gEpBkcwxUjw"; // sceKernelAprResolveFilepathsToIdsAndFileSizes
// LEAF-IMPORT REGISTRATION — scalar-only constraint.
//
// TryDispatchLeafImport is a fast path that never copies the trampoline's XMM
// save area into CpuContext and never publishes an XMM0 return back to the
// guest (see DispatchImport). Every NID listed here must therefore satisfy BOTH:
// 1. no float/double parameters (nothing passed in xmm0..xmm7), and
// 2. no float/double return value (nothing returned in xmm0).
// Integer/pointer arguments and returns only. va_list-based functions
// (vsnprintf) are safe: SysV va_list floats are read from the caller-built
// reg_save_area in guest memory, not from the callee's incoming XMM registers.
//
// If a function that consumes XMM arguments or returns in xmm0 (e.g. powf,
// logf, wcstod, sceVideoOutColorSettingsSetGamma_) is ever added here, its
// float arguments will silently arrive stale and its return value will be
// dropped. Such functions must stay on the full gateway path — do not list them.
//
// Audited 2026-07-11 (PR #59): every NID below maps to a registered
// SysAbiExport whose handler neither reads nor writes CpuContext XMM state
// and whose known guest signature is integer/pointer only.
private bool IsLeafImport(string nid)
{
if (nid == "1jfXLRVzisc") // sceKernelUsleep
if (nid == "1jfXLRVzisc")
{
return !_logUsleep;
}
// Only mutex/rwlock *lock* is excluded: it may block a contended acquire, which the
// leaf path can't. unlock never blocks and stays here — routing it off the fast path
// slows guest spinlocks enough to livelock (Demon's Souls).
return nid is
"tn3VlD0hG60" or // scePthreadMutexUnlock
"2Z+PpY6CaJg" or // pthread_mutex_unlock
"EgmLo6EWgso" or // pthread_rwlock_unlock
"+L98PIbGttk" or // scePthreadRwlockUnlock
"q1cHNfGycLI" or // scePadRead
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
"9UK1vLZQft4" or
"tn3VlD0hG60" or
"7H0iTOciTLo" or
"2Z+PpY6CaJg" or
"8aI7R7WaOlc" or
"zgXifHT9ErY" or // sceVideoOutIsFlipPending
"V++UgBtQhn0" or // sceAgcGetDataPacketPayloadAddress
"qj7QZpgr9Uw" or // sceAgcUnknownQj7QZpgr9Uw (unknown NID; observed scalar: command-buffer pointer in, packet address out)
"qj7QZpgr9Uw" or // Gen5 graphics type-2 packet
"LtTouSCZjHM" or // sceAgcCbNop
"k3GhuSNmBLU" or // sceAgcCbDispatch
"UZbQjYAwwXM" or // sceAgcCbSetShRegistersDirect
@@ -849,24 +753,24 @@ public sealed partial class DirectExecutionBackend
"IxYiarKlXxM" or // sceAgcDmaDataPatchSetDstAddressOrOffset
"3KDcnM3lrcU" or // sceAgcWaitRegMemPatchAddress
"0fWWK5uG9rQ" or // sceAgcQueueEndOfPipeActionPatchAddress
"a8uLzYY--tM" or // sceAmprAprCommandBufferConstructor
"Qs1xtplKo0U" or // sceAmprAprCommandBufferDestructor
"GuchCTefuZw" or // sceAmprCommandBufferDestructor
"N-FSPA4S3nI" or // sceAmprCommandBufferSetBuffer
"baQO9ez2gL4" or // sceAmprCommandBufferReset
"ULvXMDz56po" or // sceAmprCommandBufferClearBuffer
"mQ16-QdKv7k" or // sceAmprAprCommandBufferReadFile
"vWU-odnS+fU" or // sceAmprMeasureCommandSizeReadFile
"sSAUCCU1dv4" or // sceAmprMeasureCommandSizeWriteKernelEventQueue_04_00
"C+IEj+BsAFM" or // sceAmprMeasureCommandSizeWriteAddressOnCompletion
"tZDDEo2tE5k" or // sceAmprCommandBufferGetSize
"GnxKOHEawhk" or // sceAmprCommandBufferGetCurrentOffset
"H896Pt-yB4I" or // sceAmprCommandBufferWriteKernelEventQueue_04_00
"sJXyWHjP-F8" or // sceAmprCommandBufferWriteAddressOnCompletion
"ASoW5WE-UPo" or // sceKernelAprSubmitCommandBufferAndGetResult
"rqwFKI4PAiM" or // sceKernelAprWaitCommandBuffer
"eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
"qvMUCyyaCSI" or // sceKernelAprSubmitCommandBufferAndGetId
"a8uLzYY--tM" or
"Qs1xtplKo0U" or
"GuchCTefuZw" or
"N-FSPA4S3nI" or
"baQO9ez2gL4" or
"ULvXMDz56po" or
"mQ16-QdKv7k" or
"vWU-odnS+fU" or
"sSAUCCU1dv4" or
"C+IEj+BsAFM" or
"tZDDEo2tE5k" or
"GnxKOHEawhk" or
"H896Pt-yB4I" or
"sJXyWHjP-F8" or
"ASoW5WE-UPo" or
"rqwFKI4PAiM" or
"eE4Szl8sil8" or
"qvMUCyyaCSI" or
"Vo5V8KAwCmk" or // sceSystemServiceHideSplashScreen
"TywrFKCoLGY" or // sceSaveDataInitialize3
"dyIhnXq-0SM" or // sceSaveDataDirNameSearch
@@ -881,11 +785,10 @@ public sealed partial class DirectExecutionBackend
"Q2V+iqvjgC0" or // vsnprintf
"j4ViWNHEgww" or // strlen
"5jNubw4vlAA" or // strnlen
"LHMrG7e8G78" or // wcsmisc
"LHMrG7e8G78" or // wcslen
"WkkeywLJcgU" or // wcslen
"Ovb2dSJOAuE" or // strcmp
"aesyjrHVWy4" or // strncmp
"AV6ipCNa4Rw" or // strcasecmp
"pNtJdE3x49E" or // wcscmp
"fV2xHER+bKE" or // wcscoll
"E8wCoUEbfzk" or // wcsncmp
@@ -896,7 +799,12 @@ public sealed partial class DirectExecutionBackend
"6ULAa0fq4jA" or // scePthreadRwlockInit
"1471ajPzxh0" or // pthread_rwlock_destroy
"BB+kb08Tl9A" or // scePthreadRwlockDestroy
// rwlock rd/wr lock removed (can block); init/destroy/unlock stay.
"iGjsr1WAtI0" or // pthread_rwlock_rdlock
"Ox9i0c7L5w0" or // scePthreadRwlockRdlock
"sIlRvQqsN2Y" or // pthread_rwlock_wrlock
"mqdNorrB+gI" or // scePthreadRwlockWrlock
"EgmLo6EWgso" or // pthread_rwlock_unlock
"+L98PIbGttk" or // scePthreadRwlockUnlock
"aI+OeCz8xrQ" or // scePthreadSelf
"EotR8a3ASf4" or // pthread_self
"eoht7mQOCmo" or // scePthreadGetspecific
@@ -906,12 +814,7 @@ public sealed partial class DirectExecutionBackend
"vz+pg2zdopI" or // sceKernelGetEventUserData
"mJ7aghmgvfc" or // sceKernelGetEventId
"23CPPI1tyBY" or // sceKernelGetEventFilter
"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
"kwGyyjohI50"; // sceKernelGetEventData
}
private long NextImportDispatchIndex()
@@ -1081,15 +984,7 @@ public sealed partial class DirectExecutionBackend
}
private static bool IsImportLoopGuardBoundary(string nid) =>
nid switch
{
"1jfXLRVzisc" => true, // sceKernelUsleep
"QcteRwbsnV0" => true, // usleep
"n88vx3C5nW8" => true, // gettimeofday
"Zxa0VhQVTsk" => true, // sceKernelWaitSema
"T72hz6ffq08" => true, // scePthreadYield
_ => false
};
string.Equals(nid, "1jfXLRVzisc", StringComparison.Ordinal);
private void ResetImportLoopPattern()
{
@@ -1329,485 +1224,30 @@ public sealed partial class DirectExecutionBackend
{
return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
NormalizeKernelDynlibDlsymArguments(cpuContext, out var symbolNameAddress, out var outputAddress);
try
ulong symbolNameAddress = cpuContext[CpuRegister.Rsi];
ulong outputAddress = cpuContext[CpuRegister.Rdx];
if (!TryReadAsciiZ(symbolNameAddress, 512, out var symbolName))
{
if (!TryReadAsciiZ(symbolNameAddress, 512, out var symbolName) ||
!TryResolveDlsymGuestAddress(symbolName, out var resolvedAddress) ||
outputAddress == 0 ||
!TryWriteUInt64Compat(outputAddress, resolvedAddress))
{
return CompleteKernelDynlibDlsymFailure(cpuContext, outputAddress);
}
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
return OrbisGen2Result.ORBIS_GEN2_OK;
}
catch
if (!TryResolveRuntimeSymbolAddress(symbolName, out var resolvedAddress) &&
!TryResolveRuntimeSymbolAlias(symbolName, out resolvedAddress))
{
return CompleteKernelDynlibDlsymFailure(cpuContext, outputAddress);
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;
}
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;
@@ -1872,20 +1312,29 @@ 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];
try
_ = TryReadAsciiZ(symbolNameAddress, 512, out var symbolName);
OrbisGen2Result result = DispatchKernelDynlibDlsym();
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
{
OrbisGen2Result result = DispatchKernelDynlibDlsym();
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
{
return result;
}
return result;
}
catch
if (_logBootstrap)
{
return CompleteKernelDynlibDlsymFailure(cpuContext, outputAddress);
Console.Error.WriteLine(
$"[LOADER][TRACE] bootstrap_dispatch: handle=0x{bridgeHandle:X16} symbol='{symbolName}' out=0x{outputAddress:X16} rax=0x{cpuContext[CpuRegister.Rax]:X16}");
}
if (cpuContext[CpuRegister.Rax] == 0uL)
{
return OrbisGen2Result.ORBIS_GEN2_OK;
}
Console.Error.WriteLine(
$"[LOADER][WARN] bootstrap_bridge unresolved: handle=0x{bridgeHandle:X} symbol='{symbolName}' out=0x{outputAddress:X16}");
return OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -1926,7 +1375,6 @@ 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++)
@@ -1,582 +0,0 @@
// 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();
}
}
}
File diff suppressed because it is too large Load Diff
+15 -39
View File
@@ -45,13 +45,7 @@ public static class Ps5ParamJsonReader
try
{
ReadOnlyMemory<byte> json = data;
if (json.Span.StartsWith("\uFEFF"u8))
{
json = json[3..];
}
using var doc = JsonDocument.Parse(json);
using var doc = JsonDocument.Parse(data);
return TryReadPs5Param(doc.RootElement);
}
catch (JsonException)
@@ -62,15 +56,12 @@ public static class Ps5ParamJsonReader
private static (string? Title, string? TitleId, string? Version) TryReadPs5Param(JsonElement root)
{
if (root.ValueKind != JsonValueKind.Object)
return (null, null, null);
var titleId = GetString(root, "titleId");
string? titleId = root.TryGetProperty("titleId", out var eTid) ? eTid.GetString() : null;
string? ver =
GetString(root, "contentVersion")
?? GetString(root, "masterVersion")
?? GetString(root, "targetContentVersion");
(root.TryGetProperty("contentVersion", out var cv) ? cv.GetString() : null)
?? (root.TryGetProperty("masterVersion", out var mv) ? mv.GetString() : null)
?? (root.TryGetProperty("targetContentVersion", out var tv) ? tv.GetString() : null);
string? title = ExtractTitleName(root);
@@ -79,49 +70,34 @@ public static class Ps5ParamJsonReader
private static string? ExtractTitleName(JsonElement root)
{
if ((!root.TryGetProperty("localizedParameters", out var lp) || lp.ValueKind != JsonValueKind.Object) &&
root.TryGetProperty("disc", out var disc) && disc.ValueKind == JsonValueKind.Object)
if (!root.TryGetProperty("localizedParameters", out var lp))
{
disc.TryGetProperty("localizedParameters", out lp);
if (root.TryGetProperty("disc", out var disc) && disc.ValueKind == JsonValueKind.Object)
{
disc.TryGetProperty("localizedParameters", out lp);
}
}
if (lp.ValueKind != JsonValueKind.Object)
return null;
var defLang = GetString(lp, "defaultLanguage");
string? defLang = lp.TryGetProperty("defaultLanguage", out var dl) ? dl.GetString() : null;
if (!string.IsNullOrEmpty(defLang))
{
if (lp.TryGetProperty(defLang, out var langObj) && langObj.ValueKind == JsonValueKind.Object)
{
var title = GetString(langObj, "titleName");
if (!string.IsNullOrWhiteSpace(title))
return title;
if (langObj.TryGetProperty("titleName", out var tn))
return tn.GetString();
}
}
if (lp.TryGetProperty("en-US", out var en) && en.ValueKind == JsonValueKind.Object)
{
var title = GetString(en, "titleName");
if (!string.IsNullOrWhiteSpace(title))
return title;
}
foreach (var property in lp.EnumerateObject())
{
if (property.Value.ValueKind == JsonValueKind.Object)
{
var title = GetString(property.Value, "titleName");
if (!string.IsNullOrWhiteSpace(title))
return title;
}
if (en.TryGetProperty("titleName", out var tn2))
return tn2.GetString();
}
return null;
}
private static string? GetString(JsonElement parent, string propertyName) =>
parent.TryGetProperty(propertyName, out var value) && value.ValueKind == JsonValueKind.String
? value.GetString()
: null;
}
+41 -41
View File
@@ -10,13 +10,11 @@ using SharpEmu.Core;
using SharpEmu.Core.Cpu;
using SharpEmu.Core.Memory;
using SharpEmu.HLE;
using SharpEmu.Logging;
namespace SharpEmu.Core.Loader;
public sealed class SelfLoader : ISelfLoader
{
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Loader");
private const uint SelfMagic = 0x4F153D1D;
private const ulong SelfSegmentFlag = 0x800;
private const int PageSize = 0x1000;
@@ -157,7 +155,7 @@ public sealed class SelfLoader : ISelfLoader
}
var tlsModuleId = _nextTlsModuleId == 0 ? 1u : _nextTlsModuleId;
Log.Debug($"TLS load_start clear={clearVirtualMemory} next={_nextTlsModuleId} assigned={tlsModuleId}");
Console.Error.WriteLine($"[LOADER][TLS] load_start clear={clearVirtualMemory} next={_nextTlsModuleId} assigned={tlsModuleId}");
var loadContext = ParseLayout(imageData);
var elfHeader = ReadUnmanaged<ElfHeader>(imageData, loadContext.ElfOffset);
@@ -257,7 +255,7 @@ public sealed class SelfLoader : ISelfLoader
{
_nextTlsModuleId++;
}
Log.Debug($"TLS load_done assigned={tlsModuleId} next={_nextTlsModuleId}");
Console.Error.WriteLine($"[LOADER][TLS] load_done assigned={tlsModuleId} next={_nextTlsModuleId}");
return new SelfImage(
loadContext.IsSelf,
@@ -405,15 +403,15 @@ public sealed class SelfLoader : ISelfLoader
var virtualAddress = header.VirtualAddress + imageBase;
Log.Debug($"Segment {index}: VAddr=0x{virtualAddress:X16}, FileSize=0x{header.FileSize:X}, MemSize=0x{header.MemorySize:X}, Align=0x{header.Alignment:X}");
Console.Error.WriteLine($"[LOADER] Segment {index}: VAddr=0x{virtualAddress:X16}, FileSize=0x{header.FileSize:X}, MemSize=0x{header.MemorySize:X}, Align=0x{header.Alignment:X}");
if (header.Alignment > 1)
{
var vaddrMod = virtualAddress % header.Alignment;
var offsetMod = header.Offset % header.Alignment;
if (vaddrMod != offsetMod)
{
Log.Warning(
$"Segment {index} ELF alignment mismatch! " +
Console.Error.WriteLine(
$"[LOADER] WARNING: Segment {index} ELF alignment mismatch! " +
$"VAddr=0x{virtualAddress:X}, Offset=0x{header.Offset:X}, Align=0x{header.Alignment:X}, " +
$"VAddr%Align=0x{vaddrMod:X}, Offset%Align=0x{offsetMod:X}");
}
@@ -673,13 +671,13 @@ public sealed class SelfLoader : ISelfLoader
{
if (descriptor.ValueKind == RelocationValueKind.TlsModuleId)
{
Log.Debug(
$"Patching DTPMOD64 at 0x{descriptor.TargetAddress:X} with module id 0x{targetValue:X}");
Console.Error.WriteLine(
$"[LOADER][TLS] Patching DTPMOD64 at 0x{descriptor.TargetAddress:X} with module id 0x{targetValue:X}");
}
else
{
Log.Warning($"!!! CRITICAL !!! Patching address 0x{descriptor.TargetAddress:X} with INVALID value 0x{targetValue:X} for NID {descriptor.ImportNid ?? "(null)"}");
Log.Warning($" SymbolValue=0x{descriptor.SymbolValue:X}, Addend=0x{descriptor.Addend:X}, StubAddress=0x{(addressesByNid.TryGetValue(descriptor.ImportNid ?? "", out var sa) ? sa : 0):X}");
Console.Error.WriteLine($"[LOADER] !!! CRITICAL !!! Patching address 0x{descriptor.TargetAddress:X} with INVALID value 0x{targetValue:X} for NID {descriptor.ImportNid ?? "(null)"}");
Console.Error.WriteLine($"[LOADER] SymbolValue=0x{descriptor.SymbolValue:X}, Addend=0x{descriptor.Addend:X}, StubAddress=0x{(addressesByNid.TryGetValue(descriptor.ImportNid ?? "", out var sa) ? sa : 0):X}");
}
}
@@ -691,8 +689,8 @@ public sealed class SelfLoader : ISelfLoader
if (descriptor.TargetAddress >= 0x00000008030FC300UL &&
descriptor.TargetAddress <= 0x00000008030FC3F0UL)
{
Log.Debug(
$"[RELOC] target=0x{descriptor.TargetAddress:X16} value=0x{targetValue:X16} addend=0x{descriptor.Addend:X} nid={(descriptor.ImportNid ?? "<sym>")}");
Console.Error.WriteLine(
$"[LOADER][RELOC] target=0x{descriptor.TargetAddress:X16} value=0x{targetValue:X16} addend=0x{descriptor.Addend:X} nid={(descriptor.ImportNid ?? "<sym>")}");
}
}
@@ -785,15 +783,15 @@ public sealed class SelfLoader : ISelfLoader
{
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
{
Log.Debug(
$"[FOCUS][SCAN] off=0x{relocation.Offset:X16} type={relocation.Type} sym={relocation.SymbolIndex} addend=0x{relocation.Addend:X}");
Console.Error.WriteLine(
$"[LOADER][FOCUS][SCAN] off=0x{relocation.Offset:X16} type={relocation.Type} sym={relocation.SymbolIndex} addend=0x{relocation.Addend:X}");
}
if (!IsSupportedRelocationType(relocation.Type))
{
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
{
Log.Debug($"[FOCUS][SKIP] unsupported type={relocation.Type}");
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] unsupported type={relocation.Type}");
}
continue;
}
@@ -802,7 +800,7 @@ public sealed class SelfLoader : ISelfLoader
{
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
{
Log.Debug("[FOCUS][SKIP] target address not mapped");
Console.Error.WriteLine("[LOADER][FOCUS][SKIP] target address not mapped");
}
continue;
}
@@ -842,7 +840,7 @@ public sealed class SelfLoader : ISelfLoader
{
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
{
Log.Debug($"[FOCUS][SKIP] symbol read failed index={symbolIndex}");
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] symbol read failed index={symbolIndex}");
}
continue;
}
@@ -854,9 +852,10 @@ public sealed class SelfLoader : ISelfLoader
var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
if (symbolAddress == 0)
{
Log.Warning(
$"Skipping local relocation with invalid symbol value 0x{symbol.Value:X} " +
Console.Error.WriteLine(
$"[LOADER] Skipping local relocation with invalid symbol value 0x{symbol.Value:X} " +
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
continue;
}
descriptors.Add(new RelocationDescriptor(
@@ -874,9 +873,10 @@ public sealed class SelfLoader : ISelfLoader
var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
if (symbolAddress == 0)
{
Log.Warning(
$"Skipping relocation with invalid symbol value 0x{symbol.Value:X} " +
Console.Error.WriteLine(
$"[LOADER] Skipping relocation with invalid symbol value 0x{symbol.Value:X} " +
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
continue;
}
descriptors.Add(new RelocationDescriptor(
@@ -893,7 +893,7 @@ public sealed class SelfLoader : ISelfLoader
{
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
{
Log.Debug($"[FOCUS][SKIP] bind={symbolBind} not importable");
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] bind={symbolBind} not importable");
}
continue;
}
@@ -902,7 +902,7 @@ public sealed class SelfLoader : ISelfLoader
{
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
{
Log.Debug($"[FOCUS][SKIP] symbol name read failed offset={symbol.NameOffset}");
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] symbol name read failed offset={symbol.NameOffset}");
}
continue;
}
@@ -950,8 +950,8 @@ public sealed class SelfLoader : ISelfLoader
if (sectionSymbols > 0 || dynamicSymbols > 0)
{
Log.Info(
$"Runtime symbol index populated: section={sectionSymbols}, dynamic={dynamicSymbols}, total={runtimeSymbols.Count}");
Console.Error.WriteLine(
$"[LOADER] Runtime symbol index populated: section={sectionSymbols}, dynamic={dynamicSymbols}, total={runtimeSymbols.Count}");
}
}
@@ -1025,8 +1025,8 @@ public sealed class SelfLoader : ISelfLoader
if (preInitializers.Count != 0 || initializers.Count != 0)
{
Log.Info(
$"Initializers discovered: preinit={preInitializers.Count}, init={initializers.Count}");
Console.Error.WriteLine(
$"[LOADER] Initializers discovered: preinit={preInitializers.Count}, init={initializers.Count}");
}
}
@@ -1409,8 +1409,8 @@ public sealed class SelfLoader : ISelfLoader
var requiredBytes = checked((ulong)orderedImportNids.Count * ImportStubSlotSize);
var mapSize = AlignUp(Math.Max(requiredBytes, (ulong)PageSize), PageSize);
Log.Debug(
$"CreateImportStubMapping: nids={orderedImportNids.Count}, required=0x{requiredBytes:X}, map_size=0x{mapSize:X}");
Console.Error.WriteLine(
$"[LOADER] CreateImportStubMapping: nids={orderedImportNids.Count}, required=0x{requiredBytes:X}, map_size=0x{mapSize:X}");
if (mapSize > int.MaxValue)
{
throw new NotSupportedException("Import stub mapping exceeds 2 GB and is not supported.");
@@ -1842,16 +1842,16 @@ public sealed class SelfLoader : ISelfLoader
tableBytes = GC.AllocateUninitializedArray<byte>((int)size);
var guestAddr = location + imageBase;
Log.Debug($"TryLoadTableBytes: trying guest address 0x{guestAddr:X}");
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: trying guest address 0x{guestAddr:X}");
if (virtualMemory.TryRead(guestAddr, tableBytes))
{
Log.Debug($"TryLoadTableBytes: loaded from guest memory at 0x{guestAddr:X}");
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: loaded from guest memory at 0x{guestAddr:X}");
return true;
}
if (virtualMemory.TryRead(location, tableBytes))
{
Log.Debug($"TryLoadTableBytes: loaded from absolute guest address 0x{location:X}");
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: loaded from absolute guest address 0x{location:X}");
return true;
}
@@ -1859,11 +1859,11 @@ public sealed class SelfLoader : ISelfLoader
{
var slice = elfData.Slice((int)location, (int)size);
tableBytes = slice.ToArray();
Log.Debug($"TryLoadTableBytes: loaded from elfData as file offset at 0x{location:X}");
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: loaded from elfData as file offset at 0x{location:X}");
return true;
}
Log.Warning($"TryLoadTableBytes: FAILED for location 0x{location:X}");
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: FAILED for location 0x{location:X}");
tableBytes = Array.Empty<byte>();
return false;
}
@@ -1920,17 +1920,17 @@ public sealed class SelfLoader : ISelfLoader
private static ulong ResolveMappedAddressOrFallback(IVirtualMemory virtualMemory, ulong address, ulong imageBase)
{
Log.Debug($"ResolveMappedAddressOrFallback addr=0x{address:X} imageBase=0x{imageBase:X16}");
Console.Error.WriteLine($"[LOADER][TEST] ResolveMappedAddressOrFallback addr=0x{address:X} imageBase=0x{imageBase:X16}");
if (address == 0)
{
Log.Debug("-> return 0 (null)");
Console.Error.WriteLine("[LOADER][TEST] -> return 0 (null)");
return 0;
}
if (TryResolveMappedAddress(virtualMemory, address, imageBase, 1, out var resolved))
{
Log.Debug($"-> resolved raw 0x{resolved:X16}");
Console.Error.WriteLine($"[LOADER][TEST] -> resolved raw 0x{resolved:X16}");
return resolved;
}
@@ -1939,18 +1939,18 @@ public sealed class SelfLoader : ISelfLoader
var rebased = address + imageBase;
if (TryResolveMappedAddress(virtualMemory, rebased, imageBase, 1, out var resolvedRebased))
{
Log.Debug($"-> resolved rebased 0x{resolvedRebased:X16}");
Console.Error.WriteLine($"[LOADER][TEST] -> resolved rebased 0x{resolvedRebased:X16}");
return resolvedRebased;
}
}
if (address < 0x10000)
{
Log.Debug($"-> reject small 0x{address:X}");
Console.Error.WriteLine($"[LOADER][TEST] -> reject small 0x{address:X}");
return 0;
}
Log.Debug($"-> fallback raw 0x{address:X}");
Console.Error.WriteLine($"[LOADER][TEST] -> fallback raw 0x{address:X}");
return address;
}
@@ -4,14 +4,11 @@
using System.Runtime.InteropServices;
using SharpEmu.Core.Loader;
using SharpEmu.HLE;
using SharpEmu.Logging;
namespace SharpEmu.Core.Memory;
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryAllocator, IDisposable
{
private static readonly SharpEmuLogger Log = SharpEmuLog.For("VMEM");
private readonly ReaderWriterLockSlim _gate = new(LockRecursionPolicy.SupportsRecursion);
private readonly object _guestAllocationGate = new();
private readonly object _allocationSearchHintGate = new();
@@ -1055,7 +1052,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
return;
}
Log.Debug(message);
Console.Error.WriteLine($"[VMEM] {message}");
}
public void Dispose()
+37 -101
View File
@@ -11,19 +11,15 @@ using SharpEmu.Libs.VideoOut;
using SharpEmu.Libs.Kernel;
using SharpEmu.Libs.AppContent;
using SharpEmu.Libs.SaveData;
using SharpEmu.Logging;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
namespace SharpEmu.Core.Runtime;
public sealed class SharpEmuRuntime : ISharpEmuRuntime
{
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Runtime");
private readonly record struct LoadedModuleImage(string Path, SelfImage Image);
private static readonly HashSet<string> PreloadSkipModules = new(StringComparer.OrdinalIgnoreCase)
@@ -83,6 +79,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
ImportTraceLimit = Math.Max(0, options.ImportTraceLimit),
};
var moduleManager = new ModuleManager();
moduleManager.RegisterFromAssembly(typeof(VideoOutExports).Assembly, Generation.Gen4 | Generation.Gen5, Aerolib.Instance);
moduleManager.RegisterFromAssembly(typeof(KernelExports).Assembly, Generation.Gen4 | Generation.Gen5, Aerolib.Instance);
moduleManager.Freeze();
@@ -131,7 +128,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
{
var normalizedEbootPath = Path.GetFullPath(ebootPath);
using var app0Binding = BindApp0Root(normalizedEbootPath);
Log.Info($"Loading: {ebootPath}");
Console.Error.WriteLine($"[RUNTIME] Loading: {ebootPath}");
LastExecutionDiagnostics = null;
LastExecutionTrace = null;
LastSessionSummary = null;
@@ -139,12 +136,11 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
LastMilestoneLog = null;
KernelModuleRegistry.Reset();
var image = LoadImage(normalizedEbootPath);
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version, BuildInfo.CommitSha);
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
LogAppBundleInfo(normalizedEbootPath, image);
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
Log.Info($"Entry: 0x{image.EntryPoint:X16}");
Console.Error.WriteLine($"[RUNTIME] Entry: 0x{image.EntryPoint:X16}");
var generation = image.ElfHeader.AbiVersion == 2 ? Generation.Gen5 : Generation.Gen4;
var activeImportStubs = new Dictionary<ulong, string>(image.ImportStubs);
var activeRuntimeSymbols = new Dictionary<string, ulong>(image.RuntimeSymbols, StringComparer.Ordinal);
@@ -167,7 +163,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
processImageName);
if (initializerResult is { } failedInitializerResult)
{
Log.Error($"Initializer dispatch failed: {failedInitializerResult}");
Console.Error.WriteLine($"[RUNTIME] Initializer dispatch failed: {failedInitializerResult}");
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
@@ -175,8 +171,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
return failedInitializerResult;
}
Log.Info($"Dispatching, gen: {generation}");
Log.Debug($"About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
Console.Error.WriteLine($"[RUNTIME] Dispatching, gen: {generation}");
Console.Error.WriteLine($"[RUNTIME] About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
var result = _cpuDispatcher.DispatchEntry(
image.EntryPoint,
@@ -186,8 +182,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
processImageName,
_cpuExecutionOptions);
Log.Info($"DispatchEntry returned: {result}");
Log.Info($"Dispatch result: {result}");
Console.Error.WriteLine($"[RUNTIME] DispatchEntry returned: {result}");
Console.Error.WriteLine($"[RUNTIME] Dispatch result: {result}");
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
@@ -358,66 +354,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
return result;
}
private static void LogAppBundleInfo(string ebootPath, SelfImage image)
{
var executableName = Path.GetFileName(ebootPath);
if (string.IsNullOrWhiteSpace(executableName))
{
executableName = "eboot.bin";
}
var displayName = string.IsNullOrWhiteSpace(image.Title) ? "(unknown)" : image.Title!.Trim();
var titleId = string.IsNullOrWhiteSpace(image.TitleId) ? "(unknown)" : image.TitleId!.Trim();
var version = string.IsNullOrWhiteSpace(image.Version) ? "(unknown)" : image.Version!.Trim();
var contentId = ResolveContentId(Path.GetDirectoryName(ebootPath)) ?? "(unknown)";
var builder = new StringBuilder();
builder.AppendLine("App bundle info:");
builder.AppendLine($"- Display name: {displayName}");
builder.AppendLine($"- Version: {version}");
builder.AppendLine($"- Title ID: {titleId}");
builder.AppendLine($"- Content ID: {contentId}");
builder.AppendLine($"- Executable: {executableName}");
builder.Append("- Platform: PlayStation 5");
Log.Info(builder.ToString());
}
private static string? ResolveContentId(string? bundleRoot)
{
if (string.IsNullOrEmpty(bundleRoot))
{
return null;
}
foreach (var candidate in new[]
{
Path.Combine(bundleRoot, "sce_sys", "param.json"),
Path.Combine(bundleRoot, "param.json"),
})
{
if (!File.Exists(candidate))
{
continue;
}
try
{
using var doc = System.Text.Json.JsonDocument.Parse(File.ReadAllBytes(candidate));
if (doc.RootElement.TryGetProperty("contentId", out var contentId))
{
return contentId.GetString();
}
}
catch (Exception ex) when (ex is IOException or System.Text.Json.JsonException)
{
}
break;
}
return null;
}
private static App0BindingScope? BindApp0Root(string normalizedEbootPath)
{
const string app0VariableName = "SHARPEMU_APP0_DIR";
@@ -498,8 +434,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
moduleName = $"module#{i}";
}
Log.Info(
$"Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}");
Console.Error.WriteLine(
$"[RUNTIME] Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}");
var result = _cpuDispatcher.DispatchModuleInitializer(
initEntryPoint,
@@ -510,8 +446,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
_cpuExecutionOptions);
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
{
Log.Error(
$"Module start failed: {moduleName} -> {result}");
Console.Error.WriteLine(
$"[RUNTIME] Module start failed: {moduleName} -> {result}");
return result;
}
}
@@ -532,8 +468,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
return null;
}
Log.Info(
$"Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}");
Console.Error.WriteLine(
$"[RUNTIME] Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}");
var result = RunInitializerList(
$"{label}:preinit",
@@ -572,8 +508,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
continue;
}
Log.Debug(
$" Initializer {label}[{i}] -> 0x{initializerAddress:X16}");
Console.Error.WriteLine(
$"[RUNTIME] Initializer {label}[{i}] -> 0x{initializerAddress:X16}");
var result = _cpuDispatcher.DispatchEntry(
initializerAddress,
@@ -641,7 +577,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
.ToArray();
if (skippedModules.Length > 0)
{
Log.Info($"Skipping {skippedModules.Length} core module(s): {string.Join(", ", skippedModules)}");
Console.Error.WriteLine($"[RUNTIME] Skipping {skippedModules.Length} core module(s): {string.Join(", ", skippedModules)}");
}
if (modulePaths.Length == 0)
@@ -649,8 +585,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
return loadedImages;
}
Log.Debug($"Module search directories: {string.Join(", ", moduleDirectories)}");
Log.Info($"Loading {modulePaths.Length} module(s)...");
Console.Error.WriteLine($"[RUNTIME] Module search directories: {string.Join(", ", moduleDirectories)}");
Console.Error.WriteLine($"[RUNTIME] Loading {modulePaths.Length} module(s)...");
var loadedModules = 0;
var failedModules = 0;
var mergedImportCount = 0;
@@ -685,18 +621,18 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
loadedImages.Add(new LoadedModuleImage(modulePath, moduleImage));
loadedModules++;
Log.Info(
$"Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
Console.Error.WriteLine(
$"[RUNTIME] Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
}
catch (Exception ex)
{
failedModules++;
Log.Error($"Module load failed: {modulePath} ({ex.GetType().Name}: {ex.Message})", ex);
Console.Error.WriteLine($"[RUNTIME] Module load failed: {modulePath} ({ex.GetType().Name}: {ex.Message})");
}
}
Log.Info(
$"Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
Console.Error.WriteLine(
$"[RUNTIME] Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
return loadedImages;
}
@@ -716,8 +652,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
if (rebound != 0 || unresolved != 0)
{
Log.Info(
$"Imported data rebind: rebound={rebound}, unresolved={unresolved}");
Console.Error.WriteLine(
$"[RUNTIME] Imported data rebind: rebound={rebound}, unresolved={unresolved}");
}
}
@@ -749,8 +685,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
{
if (logRebind)
{
Log.Warning(
$"Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}");
Console.Error.WriteLine(
$"[RUNTIME] Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}");
}
unresolved++;
@@ -762,8 +698,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
{
if (logRebind)
{
Log.Error(
$"Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
Console.Error.WriteLine(
$"[RUNTIME] Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
}
unresolved++;
@@ -772,8 +708,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
if (logRebind)
{
Log.Debug(
$"Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
Console.Error.WriteLine(
$"[RUNTIME] Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
}
rebound++;
@@ -809,8 +745,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
{
if (!string.Equals(existingNid, nid, StringComparison.Ordinal))
{
Log.Warning(
$"Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
Console.Error.WriteLine(
$"[RUNTIME] Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
}
continue;
@@ -919,8 +855,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
image.EntryPoint,
isMain,
isSystemModule);
Log.Info(
$"Registered module handle={handle} name={Path.GetFileName(modulePath)} base=0x{baseAddress:X16} size=0x{size:X16}");
Console.Error.WriteLine(
$"[RUNTIME] Registered module handle={handle} name={Path.GetFileName(modulePath)} base=0x{baseAddress:X16} size=0x{size:X16}");
}
private static bool TryComputeImageRange(SelfImage image, out ulong baseAddress, out ulong size)
+1 -4
View File
@@ -65,10 +65,7 @@
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
},
"sharpemu.hle": {
"type": "Project",
"dependencies": {
"SharpEmu.Logging": "[1.0.0, )"
}
"type": "Project"
},
"sharpemu.libs": {
"type": "Project",
+6 -114
View File
@@ -11,12 +11,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Application.Resources>
<Color x:Key="SystemAccentColor">#7C5CFC</Color>
<LinearGradientBrush x:Key="BgBrush" StartPoint="0%,0%" EndPoint="100%,100%">
<GradientStop Offset="0" Color="#12151F" />
<GradientStop Offset="0.55" Color="#0D1017" />
<GradientStop Offset="1" Color="#0B0D14" />
</LinearGradientBrush>
<SolidColorBrush x:Key="BgBrush" Color="#0D1017" />
<SolidColorBrush x:Key="ChromeBrush" Color="#090C12" />
<SolidColorBrush x:Key="CardBrush" Color="#141924" />
<SolidColorBrush x:Key="CardBorderBrush" Color="#232B3A" />
@@ -29,8 +24,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
<SolidColorBrush x:Key="DangerBrush" Color="#E5484D" />
<SolidColorBrush x:Key="DangerHoverBrush" Color="#F2555A" />
<SolidColorBrush x:Key="SuccessBrush" Color="#46C46B" />
<SolidColorBrush x:Key="TileHoverBrush" Color="#1A2130" />
<SolidColorBrush x:Key="TileSelectedBrush" Color="#212A3F" />
</Application.Resources>
<Application.Styles>
@@ -45,7 +38,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Setter Property="Background" Value="{StaticResource CardBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="12" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Padding" Value="16" />
</Style>
@@ -68,10 +61,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Setter Property="Margin" Value="0,0,0,6" />
</Style>
<Style Selector="TextBox">
<Setter Property="CornerRadius" Value="8" />
</Style>
<Style Selector="Button.accent">
<Setter Property="Background" Value="{StaticResource AccentBrush}" />
<Setter Property="Foreground" Value="White" />
@@ -109,69 +98,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
</Style>
<Style Selector="ToggleButton.ghost">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
<Setter Property="Padding" Value="12,7" />
<Setter Property="CornerRadius" Value="8" />
</Style>
<Style Selector="ToggleButton.ghost:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
</Style>
<Style Selector="ToggleButton.ghost:checked /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
</Style>
<!-- Top-level page switcher (Library / Options): plain transparent
buttons, not TabItem, so there is no Fluent selected-tab underline.
The active page is conveyed by brightness alone; LB/RB gamepad
hints flank the pair. -->
<Style Selector="Button.segment">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Foreground" Value="{StaticResource MutedBrush}" />
<Setter Property="FontSize" Value="22" />
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="Padding" Value="6,4" />
<Setter Property="CornerRadius" Value="8" />
</Style>
<Style Selector="Button.segment:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
</Style>
<Style Selector="Button.segment.active">
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
</Style>
<!-- Gamepad shoulder-button hint chip (LB/RB, L1/R1). -->
<Style Selector="Border.padHint">
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
<Setter Property="CornerRadius" Value="6" />
<Setter Property="Padding" Value="8,3" />
</Style>
<Style Selector="ContextMenu">
<Setter Property="Background" Value="{StaticResource CardBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Padding" Value="6" />
</Style>
<Style Selector="ContextMenu MenuItem">
<Setter Property="Padding" Value="10,7" />
<Setter Property="CornerRadius" Value="7" />
</Style>
<Style Selector="ContextMenu Separator">
<Setter Property="Background" Value="{StaticResource CardBorderBrush}" />
<Setter Property="Height" Value="1" />
<Setter Property="Margin" Value="8,4" />
</Style>
<Style Selector="ListBox.console">
<Setter Property="Background" Value="#0B0E14" />
<Setter Property="FontFamily" Value="Cascadia Mono, Consolas, Courier New, monospace" />
@@ -182,44 +108,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Setter Property="MinHeight" Value="0" />
</Style>
<!-- Cover-art library grid -->
<Style Selector="ListBox.tileGrid ListBoxItem">
<Setter Property="Padding" Value="10" />
<Setter Property="Margin" Value="5" />
<Setter Property="CornerRadius" Value="14" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="RenderTransform" Value="translateY(0px)" />
<Setter Property="Transitions">
<Transitions>
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.12" />
</Transitions>
</Setter>
</Style>
<Style Selector="ListBox.tileGrid ListBoxItem:pointerover">
<Setter Property="RenderTransform" Value="translateY(-3px)" />
</Style>
<Style Selector="ListBox.tileGrid ListBoxItem:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource TileHoverBrush}" />
</Style>
<Style Selector="ListBox.tileGrid ListBoxItem:selected /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource TileSelectedBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
</Style>
<Style Selector="ListBox.tileGrid ListBoxItem:selected:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource TileSelectedBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource AccentHoverBrush}" />
</Style>
<Style Selector="Border.coverShadow">
<Setter Property="CornerRadius" Value="10" />
<Setter Property="BoxShadow" Value="0 6 14 0 #55000000" />
</Style>
<Style Selector="Border.coverClip">
<Setter Property="CornerRadius" Value="10" />
<Setter Property="ClipToBounds" Value="True" />
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
<Style Selector="ListBox.library ListBoxItem">
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Margin" Value="4,2" />
<Setter Property="Padding" Value="8,4" />
</Style>
</Application.Styles>
-120
View File
@@ -1,120 +0,0 @@
#nullable disable
using System.IO;
using LibAtrac9.Utilities;
namespace LibAtrac9
{
/// <summary>
/// Stores the configuration data needed to decode or encode an ATRAC9 stream.
/// </summary>
public class Atrac9Config
{
/// <summary>
/// The 4-byte ATRAC9 configuration data.
/// </summary>
public byte[] ConfigData { get; }
/// <summary>
/// A 4-bit value specifying one of 16 sample rates.
/// </summary>
public int SampleRateIndex { get; }
/// <summary>
/// A 3-bit value specifying one of 6 substream channel mappings.
/// </summary>
public int ChannelConfigIndex { get; }
/// <summary>
/// An 11-bit value containing the average size of a single frame.
/// </summary>
public int FrameBytes { get; }
/// <summary>
/// A 2-bit value indicating how many frames are in each superframe.
/// </summary>
public int SuperframeIndex { get; }
/// <summary>
/// The channel mapping used by the ATRAC9 stream.
/// </summary>
public ChannelConfig ChannelConfig { get; }
/// <summary>
/// The total number of channels in the ATRAC9 stream.
/// </summary>
public int ChannelCount { get; }
/// <summary>
/// The sample rate of the ATRAC9 stream.
/// </summary>
public int SampleRate { get; }
/// <summary>
/// Indicates whether the ATRAC9 stream has a <see cref="SampleRateIndex"/> of 8 or above.
/// </summary>
public bool HighSampleRate { get; }
/// <summary>
/// The number of frames in each superframe.
/// </summary>
public int FramesPerSuperframe { get; }
/// <summary>
/// The number of samples in one frame as an exponent of 2.
/// <see cref="FrameSamples"/> = 2^<see cref="FrameSamplesPower"/>.
/// </summary>
public int FrameSamplesPower { get; }
/// <summary>
/// The number of samples in one frame.
/// </summary>
public int FrameSamples { get; }
/// <summary>
/// The number of bytes in one superframe.
/// </summary>
public int SuperframeBytes { get; }
/// <summary>
/// The number of samples in one superframe.
/// </summary>
public int SuperframeSamples { get; }
/// <summary>
/// Reads ATRAC9 configuration data and calculates the stream parameters from it.
/// </summary>
/// <param name="configData">The processed ATRAC9 configuration.</param>
public Atrac9Config(byte[] configData)
{
if (configData == null || configData.Length != 4)
{
throw new InvalidDataException("Config data must be 4 bytes long");
}
ReadConfigData(configData, out int a, out int b, out int c, out int d);
SampleRateIndex = a;
ChannelConfigIndex = b;
FrameBytes = c;
SuperframeIndex = d;
ConfigData = configData;
FramesPerSuperframe = 1 << SuperframeIndex;
SuperframeBytes = FrameBytes << SuperframeIndex;
ChannelConfig = Tables.ChannelConfig[ChannelConfigIndex];
ChannelCount = ChannelConfig.ChannelCount;
SampleRate = Tables.SampleRates[SampleRateIndex];
HighSampleRate = SampleRateIndex > 7;
FrameSamplesPower = Tables.SamplingRateIndexToFrameSamplesPower[SampleRateIndex];
FrameSamples = 1 << FrameSamplesPower;
SuperframeSamples = FrameSamples * FramesPerSuperframe;
}
private static void ReadConfigData(byte[] configData, out int sampleRateIndex, out int channelConfigIndex, out int frameBytes, out int superframeIndex)
{
var reader = new BitReader(configData);
int header = reader.ReadInt(8);
sampleRateIndex = reader.ReadInt(4);
channelConfigIndex = reader.ReadInt(3);
int validationBit = reader.ReadInt(1);
frameBytes = reader.ReadInt(11) + 1;
superframeIndex = reader.ReadInt(2);
if (header != 0xFE || validationBit != 0)
{
throw new InvalidDataException("ATRAC9 Config Data is invalid");
}
}
}
}
-128
View File
@@ -1,128 +0,0 @@
#nullable disable
using System;
using LibAtrac9.Utilities;
namespace LibAtrac9
{
/// <summary>
/// Decodes an ATRAC9 stream into 16-bit PCM.
/// </summary>
public class Atrac9Decoder
{
/// <summary>
/// The config data for the current ATRAC9 stream.
/// </summary>
public Atrac9Config Config { get; private set; }
private Frame Frame { get; set; }
private BitReader Reader { get; set; }
private bool _initialized;
/// <summary>
/// Sets up the decoder to decode an ATRAC9 stream based on the information in <paramref name="configData"/>.
/// </summary>
/// <param name="configData">A 4-byte value containing information about the ATRAC9 stream.</param>
public void Initialize(byte[] configData)
{
Config = new Atrac9Config(configData);
Frame = new Frame(Config);
Reader = new BitReader(null);
_initialized = true;
}
/// <summary>
/// Decodes one superframe of ATRAC9 data.
/// </summary>
/// <param name="atrac9Data">The ATRAC9 data to decode. The array must be at least
/// <see cref="Config"/>.<see cref="Atrac9Config.SuperframeBytes"/> bytes long.</param>
/// <param name="pcmOut">A buffer that the decoded PCM data will be placed in.
/// The array must have dimensions of at least [<see cref="Config"/>.<see cref="Atrac9Config.ChannelCount"/>]
/// [<see cref="Config"/>.<see cref="Atrac9Config.SuperframeSamples"/>].</param>
public void Decode(byte[] atrac9Data, short[][] pcmOut)
{
if (!_initialized) throw new InvalidOperationException("Decoder must be initialized before decoding.");
ValidateDecodeBuffers(atrac9Data, pcmOut);
Reader.SetBuffer(atrac9Data);
DecodeSuperFrame(pcmOut);
}
private void ValidateDecodeBuffers(byte[] atrac9Buffer, short[][] pcmBuffer)
{
if (atrac9Buffer == null) throw new ArgumentNullException(nameof(atrac9Buffer));
if (pcmBuffer == null) throw new ArgumentNullException(nameof(pcmBuffer));
if (atrac9Buffer.Length < Config.SuperframeBytes)
{
throw new ArgumentException("ATRAC9 buffer is too small");
}
if (pcmBuffer.Length < Config.ChannelCount)
{
throw new ArgumentException("PCM buffer is too small");
}
for (int i = 0; i < Config.ChannelCount; i++)
{
if (pcmBuffer[i]?.Length < Config.SuperframeSamples)
{
throw new ArgumentException("PCM buffer is too small");
}
}
}
private void DecodeSuperFrame(short[][] pcmOut)
{
for (int i = 0; i < Config.FramesPerSuperframe; i++)
{
Frame.FrameIndex = i;
DecodeFrame(Reader, Frame);
PcmFloatToShort(pcmOut, i * Config.FrameSamples);
Reader.AlignPosition(8);
}
}
private void PcmFloatToShort(short[][] pcmOut, int start)
{
int endSample = start + Config.FrameSamples;
int channelNum = 0;
foreach (Block block in Frame.Blocks)
{
foreach (Channel channel in block.Channels)
{
double[] pcmSrc = channel.Pcm;
short[] pcmDest = pcmOut[channelNum++];
for (int d = 0, s = start; s < endSample; d++, s++)
{
double sample = pcmSrc[d];
// Not using Math.Round because it's ~20x slower on 64-bit
int roundedSample = (int)Math.Floor(sample + 0.5);
pcmDest[s] = Helpers.Clamp16(roundedSample);
}
}
}
}
private static void DecodeFrame(BitReader reader, Frame frame)
{
Unpack.UnpackFrame(reader, frame);
foreach (Block block in frame.Blocks)
{
Quantization.DequantizeSpectra(block);
Stereo.ApplyIntensityStereo(block);
Quantization.ScaleSpectrum(block);
BandExtension.ApplyBandExtension(block);
ImdctBlock(block);
}
}
private static void ImdctBlock(Block block)
{
foreach (Channel channel in block.Channels)
{
channel.Mdct.RunImdct(channel.Spectra, channel.Pcm);
}
}
}
}
-34
View File
@@ -1,34 +0,0 @@
#nullable disable
namespace LibAtrac9
{
/// <summary>
/// An Xorshift RNG used by the ATRAC9 codec
/// </summary>
internal class Atrac9Rng
{
private ushort _stateA;
private ushort _stateB;
private ushort _stateC;
private ushort _stateD;
public Atrac9Rng(ushort seed)
{
int startValue = 0x4D93 * (seed ^ (seed >> 14));
_stateA = (ushort)(3 - startValue);
_stateB = (ushort)(2 - startValue);
_stateC = (ushort)(1 - startValue);
_stateD = (ushort)(0 - startValue);
}
public ushort Next()
{
ushort t = (ushort)(_stateD ^ (_stateD << 5));
_stateD = _stateC;
_stateC = _stateB;
_stateB = _stateA;
_stateA = (ushort)(t ^ _stateA ^ ((t ^ (_stateA >> 5)) >> 4));
return _stateA;
}
}
}
-373
View File
@@ -1,373 +0,0 @@
#nullable disable
using System;
namespace LibAtrac9
{
internal static class BandExtension
{
public static void ApplyBandExtension(Block block)
{
if (!block.BandExtensionEnabled || !block.HasExtensionData) return;
foreach (Channel channel in block.Channels)
{
ApplyBandExtensionChannel(channel);
}
}
private static void ApplyBandExtensionChannel(Channel channel)
{
int groupAUnit = channel.Block.QuantizationUnitCount;
int[] scaleFactors = channel.ScaleFactors;
double[] spectra = channel.Spectra;
double[] scales = channel.BexScales;
int[] values = channel.BexValues;
GetBexBandInfo(out int bandCount, out int groupBUnit, out int groupCUnit, groupAUnit);
int totalUnits = Math.Max(groupCUnit, 22);
int groupABin = Tables.QuantUnitToCoeffIndex[groupAUnit];
int groupBBin = Tables.QuantUnitToCoeffIndex[groupBUnit];
int groupCBin = Tables.QuantUnitToCoeffIndex[groupCUnit];
int totalBins = Tables.QuantUnitToCoeffIndex[totalUnits];
FillHighFrequencies(spectra, groupABin, groupBBin, groupCBin, totalBins);
switch (channel.BexMode)
{
case 0:
int bexQuantUnits = totalUnits - groupAUnit;
switch (bandCount)
{
case 3:
scales[0] = BexMode0Bands3[0][values[0]];
scales[1] = BexMode0Bands3[1][values[0]];
scales[2] = BexMode0Bands3[2][values[1]];
scales[3] = BexMode0Bands3[3][values[2]];
scales[4] = BexMode0Bands3[4][values[3]];
break;
case 4:
scales[0] = BexMode0Bands4[0][values[0]];
scales[1] = BexMode0Bands4[1][values[0]];
scales[2] = BexMode0Bands4[2][values[1]];
scales[3] = BexMode0Bands4[3][values[2]];
scales[4] = BexMode0Bands4[4][values[3]];
break;
case 5:
scales[0] = BexMode0Bands5[0][values[0]];
scales[1] = BexMode0Bands5[1][values[1]];
scales[2] = BexMode0Bands5[2][values[1]];
break;
}
scales[bexQuantUnits - 1] = Tables.SpectrumScale[scaleFactors[groupAUnit]];
AddNoiseToSpectrum(channel, Tables.QuantUnitToCoeffIndex[totalUnits - 1],
Tables.QuantUnitToCoeffCount[totalUnits - 1]);
ScaleBexQuantUnits(spectra, scales, groupAUnit, totalUnits);
break;
case 1:
for (int i = groupAUnit; i < totalUnits; i++)
{
scales[i - groupAUnit] = Tables.SpectrumScale[scaleFactors[i]];
}
AddNoiseToSpectrum(channel, groupABin, totalBins - groupABin);
ScaleBexQuantUnits(spectra, scales, groupAUnit, totalUnits);
break;
case 2:
double groupAScale2 = BexMode2Scale[values[0]];
double groupBScale2 = BexMode2Scale[values[1]];
for (int i = groupABin; i < groupBBin; i++)
{
spectra[i] *= groupAScale2;
}
for (int i = groupBBin; i < groupCBin; i++)
{
spectra[i] *= groupBScale2;
}
return;
case 3:
double rate = Math.Pow(2, BexMode3Rate[values[1]]);
double scale = BexMode3Initial[values[0]];
for (int i = groupABin; i < totalBins; i++)
{
scale *= rate;
spectra[i] *= scale;
}
return;
case 4:
double mult = BexMode4Multiplier[values[0]];
double groupAScale4 = 0.7079468 * mult;
double groupBScale4 = 0.5011902 * mult;
double groupCScale4 = 0.3548279 * mult;
for (int i = groupABin; i < groupBBin; i++)
{
spectra[i] *= groupAScale4;
}
for (int i = groupBBin; i < groupCBin; i++)
{
spectra[i] *= groupBScale4;
}
for (int i = groupCBin; i < totalBins; i++)
{
spectra[i] *= groupCScale4;
}
return;
}
}
private static void ScaleBexQuantUnits(double[] spectra, double[] scales, int startUnit, int totalUnits)
{
for (int i = startUnit; i < totalUnits; i++)
{
for (int k = Tables.QuantUnitToCoeffIndex[i]; k < Tables.QuantUnitToCoeffIndex[i + 1]; k++)
{
spectra[k] *= scales[i - startUnit];
}
}
}
private static void FillHighFrequencies(double[] spectra, int groupABin, int groupBBin, int groupCBin, int totalBins)
{
for (int i = 0; i < groupBBin - groupABin; i++)
{
spectra[groupABin + i] = spectra[groupABin - i - 1];
}
for (int i = 0; i < groupCBin - groupBBin; i++)
{
spectra[groupBBin + i] = spectra[groupBBin - i - 1];
}
for (int i = 0; i < totalBins - groupCBin; i++)
{
spectra[groupCBin + i] = spectra[groupCBin - i - 1];
}
}
private static void AddNoiseToSpectrum(Channel channel, int index, int count)
{
if (channel.Rng == null)
{
int[] sf = channel.ScaleFactors;
ushort seed = (ushort)(543 * (sf[8] + sf[12] + sf[15] + 1));
channel.Rng = new Atrac9Rng(seed);
}
for (int i = 0; i < count; i++)
{
channel.Spectra[i + index] = channel.Rng.Next() / 65535.0 * 2.0 - 1.0;
}
}
public static void GetBexBandInfo(out int bandCount, out int groupAUnit, out int groupBUnit, int quantUnits)
{
groupAUnit = BexGroupInfo[quantUnits - 13][0];
groupBUnit = BexGroupInfo[quantUnits - 13][1];
bandCount = BexGroupInfo[quantUnits - 13][2];
}
public static readonly byte[][] BexGroupInfo =
{
new byte[] {16, 21, 0},
new byte[] {18, 22, 1},
new byte[] {20, 22, 2},
new byte[] {21, 22, 3},
new byte[] {21, 22, 3},
new byte[] {23, 24, 4},
new byte[] {23, 24, 4},
new byte[] {24, 24, 5}
};
// [mode][bands]
public static readonly byte[][] BexEncodedValueCounts =
{
new byte[] {0, 0, 0, 4, 4, 2},
new byte[] {0, 0, 0, 0, 0, 0},
new byte[] {0, 0, 0, 2, 2, 1},
new byte[] {0, 0, 0, 2, 2, 2},
new byte[] {1, 1, 1, 0, 0, 0}
};
// [mode][bands][valueIndex]
public static readonly byte[][][] BexDataLengths =
{
new[] {
new byte[] {0, 0, 0, 0},
new byte[] {0, 0, 0, 0},
new byte[] {0, 0, 0, 0},
new byte[] {5, 4, 3, 3},
new byte[] {4, 4, 3, 4},
new byte[] {4, 5, 0, 0}
}, new[] {
new byte[] {0, 0, 0, 0},
new byte[] {0, 0, 0, 0},
new byte[] {0, 0, 0, 0},
new byte[] {0, 0, 0, 0},
new byte[] {0, 0, 0, 0},
new byte[] {0, 0, 0, 0}
}, new[] {
new byte[] {0, 0, 0, 0},
new byte[] {0, 0, 0, 0},
new byte[] {0, 0, 0, 0},
new byte[] {6, 6, 0, 0},
new byte[] {6, 6, 0, 0},
new byte[] {6, 0, 0, 0}
}, new[] {
new byte[] {0, 0, 0, 0},
new byte[] {0, 0, 0, 0},
new byte[] {0, 0, 0, 0},
new byte[] {4, 4, 0, 0},
new byte[] {4, 4, 0, 0},
new byte[] {4, 4, 0, 0}
}, new[] {
new byte[] {3, 0, 0, 0},
new byte[] {3, 0, 0, 0},
new byte[] {3, 0, 0, 0},
new byte[] {0, 0, 0, 0},
new byte[] {0, 0, 0, 0},
new byte[] {0, 0, 0, 0}
}
};
public static readonly double[][] BexMode0Bands3 =
{
new[] {
0.000000e+0, 1.988220e-1, 2.514343e-1, 2.960510e-1,
3.263550e-1, 3.771362e-1, 3.786926e-1, 4.540405e-1,
4.877625e-1, 5.262451e-1, 5.447083e-1, 5.737000e-1,
6.212158e-1, 6.222839e-1, 6.560974e-1, 6.896667e-1,
7.555542e-1, 7.677917e-1, 7.918091e-1, 7.971497e-1,
8.188171e-1, 8.446045e-1, 9.790649e-1, 9.822083e-1,
9.846191e-1, 9.859314e-1, 9.863586e-1, 9.863892e-1,
9.873352e-1, 9.881287e-1, 9.898682e-1, 9.913330e-1
}, new[] {
0.000000e+0, 9.982910e-1, 7.592773e-2, 7.179565e-1,
9.851379e-1, 5.340271e-1, 9.013672e-1, 6.349182e-1,
7.226257e-1, 1.948547e-1, 7.628174e-1, 9.873657e-1,
8.112183e-1, 2.715454e-1, 9.734192e-1, 1.443787e-1,
4.640198e-1, 3.249207e-1, 3.790894e-1, 8.276367e-2,
5.954590e-1, 2.864380e-1, 9.806824e-1, 7.929077e-1,
6.292114e-1, 4.887085e-1, 2.905273e-1, 1.301880e-1,
3.140869e-1, 5.482483e-1, 4.210815e-1, 1.182861e-1
}, new[] {
0.000000e+0, 3.155518e-2, 8.581543e-2, 1.364746e-1,
1.858826e-1, 2.368469e-1, 2.888184e-1, 3.432617e-1,
4.012451e-1, 4.623108e-1, 5.271301e-1, 5.954895e-1,
6.681213e-1, 7.448425e-1, 8.245239e-1, 9.097290e-1
}, new[] {
0.000000e+0, 4.418945e-2, 1.303711e-1, 2.273560e-1,
3.395996e-1, 4.735718e-1, 6.267090e-1, 8.003845e-1
}, new[] {
0.000000e+0, 2.804565e-2, 9.683228e-2, 1.849976e-1,
3.005981e-1, 4.470520e-1, 6.168518e-1, 8.007813e-1
}
};
public static readonly double[][] BexMode0Bands4 =
{
new[] {
0.000000e+0, 2.708740e-1, 3.479614e-1, 3.578186e-1,
5.083618e-1, 5.299072e-1, 5.819092e-1, 6.381836e-1,
7.276917e-1, 7.595520e-1, 7.878723e-1, 9.707336e-1,
9.713135e-1, 9.736023e-1, 9.759827e-1, 9.832458e-1
}, new[] {
0.000000e+0, 2.330627e-1, 5.891418e-1, 7.170410e-1,
2.036438e-1, 1.613464e-1, 6.668701e-1, 9.481201e-1,
9.769897e-1, 5.111694e-1, 3.522644e-1, 8.209534e-1,
2.933960e-1, 9.757690e-1, 5.289917e-1, 4.372253e-1
}, new[] {
0.000000e+0, 4.360962e-2, 1.056519e-1, 1.590576e-1,
2.078857e-1, 2.572937e-1, 3.082581e-1, 3.616028e-1,
4.191589e-1, 4.792175e-1, 5.438538e-1, 6.125183e-1,
6.841125e-1, 7.589417e-1, 8.365173e-1, 9.148254e-1
}, new[] {
0.000000e+0, 4.074097e-2, 1.164551e-1, 2.077026e-1,
3.184509e-1, 4.532166e-1, 6.124268e-1, 7.932129e-1
}, new[] {
0.000000e+0, 8.880615e-3, 2.932739e-2, 5.593872e-2,
8.825684e-2, 1.259155e-1, 1.721497e-1, 2.270813e-1,
2.901611e-1, 3.579712e-1, 4.334106e-1, 5.147095e-1,
6.023254e-1, 6.956177e-1, 7.952881e-1, 8.977356e-1
}
};
public static readonly double[][] BexMode0Bands5 =
{
new[] {
0.000000e+0, 7.379150e-2, 1.806335e-1, 2.687073e-1,
3.407898e-1, 4.047546e-1, 4.621887e-1, 5.168762e-1,
5.703125e-1, 6.237488e-1, 6.763611e-1, 7.288208e-1,
7.808533e-1, 8.337708e-1, 8.874512e-1, 9.418030e-1
}, new[] {
0.000000e+0, 7.980347e-2, 1.615295e-1, 1.665649e-1,
1.822205e-1, 2.185669e-1, 2.292175e-1, 2.456665e-1,
2.666321e-1, 3.306580e-1, 3.330688e-1, 3.765259e-1,
4.085083e-1, 4.400024e-1, 4.407654e-1, 4.817505e-1,
4.924011e-1, 5.320740e-1, 5.893860e-1, 6.131287e-1,
6.212463e-1, 6.278076e-1, 6.308899e-1, 7.660828e-1,
7.850647e-1, 7.910461e-1, 7.929382e-1, 8.038330e-1,
9.834900e-1, 9.846191e-1, 9.852295e-1, 9.862671e-1
}, new[] {
0.000000e+0, 6.084290e-1, 3.672791e-1, 3.151855e-1,
1.488953e-1, 2.571716e-1, 5.103455e-1, 3.311157e-1,
5.426025e-2, 4.254456e-1, 7.998352e-1, 7.873230e-1,
5.418701e-1, 2.925110e-1, 8.468628e-2, 1.410522e-1,
9.819641e-1, 9.609070e-1, 3.530884e-2, 9.729004e-2,
5.758362e-1, 9.941711e-1, 7.215576e-1, 7.183228e-1,
2.028809e-1, 9.588623e-2, 2.032166e-1, 1.338806e-1,
5.003357e-1, 1.874390e-1, 9.804993e-1, 1.107788e-1
}
};
public static readonly double[] BexMode2Scale =
{
4.272461e-4, 1.312256e-3, 2.441406e-3, 3.692627e-3,
4.913330e-3, 6.134033e-3, 7.507324e-3, 8.972168e-3,
1.049805e-2, 1.223755e-2, 1.406860e-2, 1.599121e-2,
1.800537e-2, 2.026367e-2, 2.264404e-2, 2.517700e-2,
2.792358e-2, 3.073120e-2, 3.344727e-2, 3.631592e-2,
3.952026e-2, 4.275513e-2, 4.608154e-2, 4.968262e-2,
5.355835e-2, 5.783081e-2, 6.195068e-2, 6.677246e-2,
7.196045e-2, 7.745361e-2, 8.319092e-2, 8.993530e-2,
9.759521e-2, 1.056213e-1, 1.138916e-1, 1.236267e-1,
1.348267e-1, 1.470337e-1, 1.603394e-1, 1.755676e-1,
1.905823e-1, 2.071228e-1, 2.245178e-1, 2.444153e-1,
2.658997e-1, 2.897644e-1, 3.146057e-1, 3.450012e-1,
3.766174e-1, 4.122620e-1, 4.505615e-1, 4.893799e-1,
5.305481e-1, 5.731201e-1, 6.157837e-1, 6.580811e-1,
6.985168e-1, 7.435303e-1, 7.865906e-1, 8.302612e-1,
8.718567e-1, 9.125671e-1, 9.575806e-1, 9.996643e-1
};
public static readonly double[] BexMode3Initial =
{
3.491211e-1, 5.371094e-1, 6.782227e-1, 7.910156e-1,
9.057617e-1, 1.024902e+0, 1.156250e+0, 1.290527e+0,
1.458984e+0, 1.664551e+0, 1.929688e+0, 2.278320e+0,
2.831543e+0, 3.659180e+0, 5.257813e+0, 8.373047e+0
};
public static readonly double[] BexMode3Rate =
{
-2.913818e-1, -2.541504e-1, -1.664429e-1, -1.476440e-1,
-1.342163e-1, -1.220703e-1, -1.117554e-1, -1.026611e-1,
-9.436035e-2, -8.483887e-2, -7.476807e-2, -6.304932e-2,
-4.492188e-2, -2.447510e-2, +1.831055e-4, +4.174805e-2
};
public static readonly double[] BexMode4Multiplier =
{
3.610229e-2, 1.260681e-1, 2.227478e-1, 3.338318e-1,
4.662170e-1, 6.221313e-1, 7.989197e-1, 9.939575e-1
};
}
}
-141
View File
@@ -1,141 +0,0 @@
#nullable disable
using System;
namespace LibAtrac9
{
internal static class BitAllocation
{
public static void CreateGradient(Block block)
{
int valueCount = block.GradientEndValue - block.GradientStartValue;
int unitCount = block.GradientEndUnit - block.GradientStartUnit;
for (int i = 0; i < block.GradientEndUnit; i++)
{
block.Gradient[i] = block.GradientStartValue;
}
for (int i = block.GradientEndUnit; i <= block.QuantizationUnitCount; i++)
{
block.Gradient[i] = block.GradientEndValue;
}
if (unitCount <= 0) return;
if (valueCount == 0) return;
byte[] curve = Tables.GradientCurves[unitCount - 1];
if (valueCount <= 0)
{
double scale = (-valueCount - 1) / 31.0;
int baseVal = block.GradientStartValue - 1;
for (int i = block.GradientStartUnit; i < block.GradientEndUnit; i++)
{
block.Gradient[i] = baseVal - (int)(curve[i - block.GradientStartUnit] * scale);
}
}
else
{
double scale = (valueCount - 1) / 31.0;
int baseVal = block.GradientStartValue + 1;
for (int i = block.GradientStartUnit; i < block.GradientEndUnit; i++)
{
block.Gradient[i] = baseVal + (int)(curve[i - block.GradientStartUnit] * scale);
}
}
}
public static void CalculateMask(Channel channel)
{
Array.Clear(channel.PrecisionMask, 0, channel.PrecisionMask.Length);
for (int i = 1; i < channel.Block.QuantizationUnitCount; i++)
{
int delta = channel.ScaleFactors[i] - channel.ScaleFactors[i - 1];
if (delta > 1)
{
channel.PrecisionMask[i] += Math.Min(delta - 1, 5);
}
else if (delta < -1)
{
channel.PrecisionMask[i - 1] += Math.Min(delta * -1 - 1, 5);
}
}
}
public static void CalculatePrecisions(Channel channel)
{
Block block = channel.Block;
if (block.GradientMode != 0)
{
for (int i = 0; i < block.QuantizationUnitCount; i++)
{
channel.Precisions[i] = channel.ScaleFactors[i] + channel.PrecisionMask[i] - block.Gradient[i];
if (channel.Precisions[i] > 0)
{
switch (block.GradientMode)
{
case 1:
channel.Precisions[i] /= 2;
break;
case 2:
channel.Precisions[i] = 3 * channel.Precisions[i] / 8;
break;
case 3:
channel.Precisions[i] /= 4;
break;
}
}
}
}
else
{
for (int i = 0; i < block.QuantizationUnitCount; i++)
{
channel.Precisions[i] = channel.ScaleFactors[i] - block.Gradient[i];
}
}
for (int i = 0; i < block.QuantizationUnitCount; i++)
{
if (channel.Precisions[i] < 1)
{
channel.Precisions[i] = 1;
}
}
for (int i = 0; i < block.GradientBoundary; i++)
{
channel.Precisions[i]++;
}
for (int i = 0; i < block.QuantizationUnitCount; i++)
{
channel.PrecisionsFine[i] = 0;
if (channel.Precisions[i] > 15)
{
channel.PrecisionsFine[i] = channel.Precisions[i] - 15;
channel.Precisions[i] = 15;
}
}
}
public static byte[][] GenerateGradientCurves()
{
byte[] main =
{
01, 01, 01, 01, 02, 02, 02, 02, 03, 03, 03, 04, 04, 05, 05, 06, 07, 08, 09, 10, 11, 12, 13, 15,
16, 18, 19, 20, 21, 22, 23, 24, 25, 26, 26, 27, 27, 28, 28, 28, 29, 29, 29, 29, 30, 30, 30, 30
};
var curves = new byte[main.Length][];
for (int length = 1; length <= main.Length; length++)
{
curves[length - 1] = new byte[length];
for (int i = 0; i < length; i++)
{
curves[length - 1][i] = main[i * main.Length / length];
}
}
return curves;
}
}
}
-92
View File
@@ -1,92 +0,0 @@
#nullable disable
namespace LibAtrac9
{
internal class Block
{
public Atrac9Config Config { get; }
public BlockType BlockType { get; }
public int BlockIndex { get; }
public Frame Frame { get; }
public Channel[] Channels { get; }
public int ChannelCount { get; }
public bool FirstInSuperframe { get; set; }
public bool ReuseBandParams { get; set; }
public int BandCount { get; set; }
public int StereoBand { get; set; }
public int ExtensionBand { get; set; }
public int QuantizationUnitCount { get; set; }
public int StereoQuantizationUnit { get; set; }
public int ExtensionUnit { get; set; }
public int QuantizationUnitsPrev { get; set; }
public int[] Gradient { get; } = new int[31];
public int GradientMode { get; set; }
public int GradientStartUnit { get; set; }
public int GradientStartValue { get; set; }
public int GradientEndUnit { get; set; }
public int GradientEndValue { get; set; }
public int GradientBoundary { get; set; }
public int PrimaryChannelIndex { get; set; }
public int[] JointStereoSigns { get; } = new int[30];
public bool HasJointStereoSigns { get; set; }
public Channel PrimaryChannel => Channels[PrimaryChannelIndex == 0 ? 0 : 1];
public Channel SecondaryChannel => Channels[PrimaryChannelIndex == 0 ? 1 : 0];
public bool BandExtensionEnabled { get; set; }
public bool HasExtensionData { get; set; }
public int BexDataLength { get; set; }
public int BexMode { get; set; }
public Block(Frame parentFrame, int blockIndex)
{
Frame = parentFrame;
BlockIndex = blockIndex;
Config = parentFrame.Config;
BlockType = Config.ChannelConfig.BlockTypes[blockIndex];
ChannelCount = BlockTypeToChannelCount(BlockType);
Channels = new Channel[ChannelCount];
for (int i = 0; i < ChannelCount; i++)
{
Channels[i] = new Channel(this, i);
}
}
public static int BlockTypeToChannelCount(BlockType blockType)
{
switch (blockType)
{
case BlockType.Mono:
return 1;
case BlockType.Stereo:
return 2;
case BlockType.LFE:
return 1;
default:
return 0;
}
}
}
/// <summary>
/// An ATRAC9 block (substream) type
/// </summary>
public enum BlockType
{
/// <summary>
/// Mono ATRAC9 block
/// </summary>
Mono = 0,
/// <summary>
/// Stereo ATRAC9 block
/// </summary>
Stereo = 1,
/// <summary>
/// Low-frequency effects ATRAC9 block
/// </summary>
LFE = 2
}
}
-49
View File
@@ -1,49 +0,0 @@
#nullable disable
using LibAtrac9.Utilities;
namespace LibAtrac9
{
internal class Channel
{
public Atrac9Config Config { get; }
public int ChannelIndex { get; }
public bool IsPrimary => Block.PrimaryChannelIndex == ChannelIndex;
public Block Block { get; }
public Mdct Mdct { get; }
public double[] Pcm { get; } = new double[256];
public double[] Spectra { get; } = new double[256];
public int CodedQuantUnits { get; set; }
public int ScaleFactorCodingMode { get; set; }
public int[] ScaleFactors { get; } = new int[31];
public int[] ScaleFactorsPrev { get; } = new int[31];
public int[] Precisions { get; } = new int[30];
public int[] PrecisionsFine { get; } = new int[30];
public int[] PrecisionMask { get; } = new int[30];
public int[] SpectraValuesBuffer { get; } = new int[16];
public int[] CodebookSet { get; } = new int[30];
public int[] QuantizedSpectra { get; } = new int[256];
public int[] QuantizedSpectraFine { get; } = new int[256];
public int BexMode { get; set; }
public int BexValueCount { get; set; }
public int[] BexValues { get; } = new int[4];
public double[] BexScales { get; } = new double[6];
public Atrac9Rng Rng { get; set; }
public Channel(Block parentBlock, int channelIndex)
{
Block = parentBlock;
ChannelIndex = channelIndex;
Config = parentBlock.Config;
Mdct = new Mdct(Config.FrameSamplesPower, Tables.ImdctWindow[Config.FrameSamplesPower - 6]);
}
public void UpdateCodedUnits() =>
CodedQuantUnits = IsPrimary ? Block.QuantizationUnitCount : Block.StereoQuantizationUnit;
}
}
-34
View File
@@ -1,34 +0,0 @@
#nullable disable
namespace LibAtrac9
{
/// <summary>
/// Describes the channel mapping for an ATRAC9 stream
/// </summary>
public class ChannelConfig
{
internal ChannelConfig(params BlockType[] blockTypes)
{
BlockCount = blockTypes.Length;
BlockTypes = blockTypes;
foreach (BlockType type in blockTypes)
{
ChannelCount += Block.BlockTypeToChannelCount(type);
}
}
/// <summary>
/// The number of blocks or substreams in the ATRAC9 stream
/// </summary>
public int BlockCount { get; }
/// <summary>
/// The type of each block or substream in the ATRAC9 stream
/// </summary>
public BlockType[] BlockTypes { get; }
/// <summary>
/// The number of channels in the ATRAC9 stream
/// </summary>
public int ChannelCount { get; }
}
}
-21
View File
@@ -1,21 +0,0 @@
#nullable disable
namespace LibAtrac9
{
internal class Frame
{
public Atrac9Config Config { get; }
public int FrameIndex { get; set; }
public Block[] Blocks { get; }
public Frame(Atrac9Config config)
{
Config = config;
Blocks = new Block[config.ChannelConfig.BlockCount];
for (int i = 0; i < config.ChannelConfig.BlockCount; i++)
{
Blocks[i] = new Block(this, i);
}
}
}
}
@@ -1,63 +0,0 @@
#nullable disable
using System;
using LibAtrac9.Utilities;
namespace LibAtrac9
{
internal class HuffmanCodebook
{
public HuffmanCodebook(short[] codes, byte[] bits, byte valueCountPower)
{
Codes = codes;
Bits = bits;
if (Codes == null || Bits == null) return;
ValueCount = 1 << valueCountPower;
ValueCountPower = valueCountPower;
ValueBits = Helpers.Log2(codes.Length) >> valueCountPower;
ValueMax = 1 << ValueBits;
int max = 0;
foreach (byte bitSize in bits)
{
max = Math.Max(max, bitSize);
}
MaxBitSize = max;
Lookup = CreateLookupTable();
}
private byte[] CreateLookupTable()
{
if (Codes == null || Bits == null) return null;
int tableSize = 1 << MaxBitSize;
var dest = new byte[tableSize];
for (int i = 0; i < Bits.Length; i++)
{
if (Bits[i] == 0) continue;
int unusedBits = MaxBitSize - Bits[i];
int start = Codes[i] << unusedBits;
int length = 1 << unusedBits;
int end = start + length;
for (int j = start; j < end; j++)
{
dest[j] = (byte)i;
}
}
return dest;
}
public short[] Codes { get; }
public byte[] Bits { get; }
public byte[] Lookup { get; }
public int ValueCount { get; }
public int ValueCountPower { get; }
public int ValueBits { get; }
public int ValueMax { get; }
public int MaxBitSize { get; }
}
}
File diff suppressed because it is too large Load Diff
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2018 Alex Barney
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-58
View File
@@ -1,58 +0,0 @@
#nullable disable
using System;
namespace LibAtrac9
{
internal static class Quantization
{
public static void DequantizeSpectra(Block block)
{
foreach (Channel channel in block.Channels)
{
Array.Clear(channel.Spectra, 0, channel.Spectra.Length);
for (int i = 0; i < channel.CodedQuantUnits; i++)
{
DequantizeQuantUnit(channel, i);
}
}
}
private static void DequantizeQuantUnit(Channel channel, int band)
{
int subBandIndex = Tables.QuantUnitToCoeffIndex[band];
int subBandCount = Tables.QuantUnitToCoeffCount[band];
double stepSize = Tables.QuantizerStepSize[channel.Precisions[band]];
double stepSizeFine = Tables.QuantizerFineStepSize[channel.PrecisionsFine[band]];
for (int sb = 0; sb < subBandCount; sb++)
{
double coarse = channel.QuantizedSpectra[subBandIndex + sb] * stepSize;
double fine = channel.QuantizedSpectraFine[subBandIndex + sb] * stepSizeFine;
channel.Spectra[subBandIndex + sb] = coarse + fine;
}
}
public static void ScaleSpectrum(Block block)
{
foreach (Channel channel in block.Channels)
{
ScaleSpectrum(channel);
}
}
private static void ScaleSpectrum(Channel channel)
{
int quantUnitCount = channel.Block.QuantizationUnitCount;
double[] spectra = channel.Spectra;
for (int i = 0; i < quantUnitCount; i++)
{
for (int sb = Tables.QuantUnitToCoeffIndex[i]; sb < Tables.QuantUnitToCoeffIndex[i + 1]; sb++)
{
spectra[sb] *= Tables.SpectrumScale[channel.ScaleFactors[i]];
}
}
}
}
}
-172
View File
@@ -1,172 +0,0 @@
#nullable disable
using System;
using System.IO;
using LibAtrac9.Utilities;
namespace LibAtrac9
{
internal static class ScaleFactors
{
public static void Read(BitReader reader, Channel channel)
{
Array.Clear(channel.ScaleFactors, 0, channel.ScaleFactors.Length);
channel.ScaleFactorCodingMode = reader.ReadInt(2);
if (channel.ChannelIndex == 0)
{
switch (channel.ScaleFactorCodingMode)
{
case 0:
ReadVlcDeltaOffset(reader, channel);
break;
case 1:
ReadClcOffset(reader, channel);
break;
case 2:
if (channel.Block.FirstInSuperframe) throw new InvalidDataException();
ReadVlcDistanceToBaseline(reader, channel, channel.ScaleFactorsPrev, channel.Block.QuantizationUnitsPrev);
break;
case 3:
if (channel.Block.FirstInSuperframe) throw new InvalidDataException();
ReadVlcDeltaOffsetWithBaseline(reader, channel, channel.ScaleFactorsPrev, channel.Block.QuantizationUnitsPrev);
break;
}
}
else
{
switch (channel.ScaleFactorCodingMode)
{
case 0:
ReadVlcDeltaOffset(reader, channel);
break;
case 1:
ReadVlcDistanceToBaseline(reader, channel, channel.Block.Channels[0].ScaleFactors, channel.Block.ExtensionUnit);
break;
case 2:
ReadVlcDeltaOffsetWithBaseline(reader, channel, channel.Block.Channels[0].ScaleFactors, channel.Block.ExtensionUnit);
break;
case 3:
if (channel.Block.FirstInSuperframe) throw new InvalidDataException();
ReadVlcDistanceToBaseline(reader, channel, channel.ScaleFactorsPrev, channel.Block.QuantizationUnitsPrev);
break;
}
}
for (int i = 0; i < channel.Block.ExtensionUnit; i++)
{
if (channel.ScaleFactors[i] < 0 || channel.ScaleFactors[i] > 31)
{
throw new InvalidDataException("Scale factor values are out of range.");
}
}
Array.Copy(channel.ScaleFactors, channel.ScaleFactorsPrev, channel.ScaleFactors.Length);
}
private static void ReadClcOffset(BitReader reader, Channel channel)
{
const int maxBits = 5;
int[] sf = channel.ScaleFactors;
int bitLength = reader.ReadInt(2) + 2;
int baseValue = bitLength < maxBits ? reader.ReadInt(maxBits) : 0;
for (int i = 0; i < channel.Block.ExtensionUnit; i++)
{
sf[i] = reader.ReadInt(bitLength) + baseValue;
}
}
private static void ReadVlcDeltaOffset(BitReader reader, Channel channel)
{
int weightIndex = reader.ReadInt(3);
byte[] weights = ScaleFactorWeights[weightIndex];
int[] sf = channel.ScaleFactors;
int baseValue = reader.ReadInt(5);
int bitLength = reader.ReadInt(2) + 3;
HuffmanCodebook codebook = Tables.HuffmanScaleFactorsUnsigned[bitLength];
sf[0] = reader.ReadInt(bitLength);
for (int i = 1; i < channel.Block.ExtensionUnit; i++)
{
int delta = Unpack.ReadHuffmanValue(codebook, reader);
sf[i] = (sf[i - 1] + delta) & (codebook.ValueMax - 1);
}
for (int i = 0; i < channel.Block.ExtensionUnit; i++)
{
sf[i] += baseValue - weights[i];
}
}
private static void ReadVlcDistanceToBaseline(BitReader reader, Channel channel, int[] baseline, int baselineLength)
{
int[] sf = channel.ScaleFactors;
int bitLength = reader.ReadInt(2) + 2;
HuffmanCodebook codebook = Tables.HuffmanScaleFactorsSigned[bitLength];
int unitCount = Math.Min(channel.Block.ExtensionUnit, baselineLength);
for (int i = 0; i < unitCount; i++)
{
int distance = Unpack.ReadHuffmanValue(codebook, reader, true);
sf[i] = (baseline[i] + distance) & 31;
}
for (int i = unitCount; i < channel.Block.ExtensionUnit; i++)
{
sf[i] = reader.ReadInt(5);
}
}
private static void ReadVlcDeltaOffsetWithBaseline(BitReader reader, Channel channel, int[] baseline, int baselineLength)
{
int[] sf = channel.ScaleFactors;
int baseValue = reader.ReadOffsetBinary(5, BitReader.OffsetBias.Negative);
int bitLength = reader.ReadInt(2) + 1;
HuffmanCodebook codebook = Tables.HuffmanScaleFactorsUnsigned[bitLength];
int unitCount = Math.Min(channel.Block.ExtensionUnit, baselineLength);
sf[0] = reader.ReadInt(bitLength);
for (int i = 1; i < unitCount; i++)
{
int delta = Unpack.ReadHuffmanValue(codebook, reader);
sf[i] = (sf[i - 1] + delta) & (codebook.ValueMax - 1);
}
for (int i = 0; i < unitCount; i++)
{
sf[i] += baseValue + baseline[i];
}
for (int i = unitCount; i < channel.Block.ExtensionUnit; i++)
{
sf[i] = reader.ReadInt(5);
}
}
public static readonly byte[][] ScaleFactorWeights =
{
new byte[] {
0, 0, 0, 1, 1, 2, 2, 2, 2, 2, 2, 3, 2, 3, 3, 4, 4, 4, 4, 4, 4, 5, 5, 6, 6, 7, 7, 8, 10, 12, 12, 12
}, new byte[] {
3, 2, 2, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 2, 3, 3, 4, 5, 7, 10, 10, 10
}, new byte[] {
0, 2, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 8, 9, 12, 12, 12
}, new byte[] {
0, 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 6, 7, 8, 8, 10, 11, 11, 12, 13, 13, 13, 13
}, new byte[] {
0, 2, 2, 3, 3, 4, 4, 5, 4, 5, 5, 5, 5, 6, 7, 8, 8, 8, 8, 9, 9, 9, 10, 10, 11, 12, 12, 13, 13, 14, 14, 14
}, new byte[] {
1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 4, 4, 5, 6, 7, 7, 9, 11, 11, 11
}, new byte[] {
0, 5, 8, 10, 11, 11, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 12, 12, 12,
12, 13, 15, 15, 15
}, new byte[] {
0, 2, 3, 4, 5, 6, 6, 7, 7, 8, 8, 8, 9, 9, 10, 10, 10, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 13, 13,
15, 15, 15
}
};
}
}
-34
View File
@@ -1,34 +0,0 @@
#nullable disable
namespace LibAtrac9
{
internal static class Stereo
{
public static void ApplyIntensityStereo(Block block)
{
if (block.BlockType != BlockType.Stereo) return;
int totalUnits = block.QuantizationUnitCount;
int stereoUnits = block.StereoQuantizationUnit;
if (stereoUnits >= totalUnits) return;
Channel source = block.PrimaryChannel;
Channel dest = block.SecondaryChannel;
for (int i = stereoUnits; i < totalUnits; i++)
{
int sign = block.JointStereoSigns[i];
for (int sb = Tables.QuantUnitToCoeffIndex[i]; sb < Tables.QuantUnitToCoeffIndex[i + 1]; sb++)
{
if (sign > 0)
{
dest.Spectra[sb] = -source.Spectra[sb];
}
else
{
dest.Spectra[sb] = source.Spectra[sb];
}
}
}
}
}
}
-116
View File
@@ -1,116 +0,0 @@
#nullable disable
using System;
using static LibAtrac9.HuffmanCodebooks;
namespace LibAtrac9
{
internal static class Tables
{
public static int MaxHuffPrecision(bool highSampleRate) => highSampleRate ? 1 : 7;
public static int MinBandCount(bool highSampleRate) => highSampleRate ? 1 : 3;
public static int MaxExtensionBand(bool highSampleRate) => highSampleRate ? 16 : 18;
public static readonly int[] SampleRates =
{
11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000,
44100, 48000, 64000, 88200, 96000, 128000, 176400, 192000
};
public static readonly byte[] SamplingRateIndexToFrameSamplesPower = { 6, 6, 7, 7, 7, 8, 8, 8, 6, 6, 7, 7, 7, 8, 8, 8 };
// From sampling rate index
public static readonly byte[] MaxBandCount = { 8, 8, 12, 12, 12, 18, 18, 18, 8, 8, 12, 12, 12, 16, 16, 16 };
public static readonly byte[] BandToQuantUnitCount = { 0, 4, 8, 10, 12, 13, 14, 15, 16, 18, 20, 21, 22, 23, 24, 25, 26, 28, 30 };
public static readonly byte[] QuantUnitToCoeffCount =
{
02, 02, 02, 02, 02, 02, 02, 02, 04, 04, 04, 04, 08, 08, 08,
08, 08, 08, 08, 08, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16
};
public static readonly short[] QuantUnitToCoeffIndex =
{
0, 2, 4, 6, 8, 10, 12, 14, 16, 20, 24, 28, 32, 40, 48, 56,
64, 72, 80, 88, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256
};
public static readonly byte[] QuantUnitToCodebookIndex =
{
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2,
2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3
};
public static readonly ChannelConfig[] ChannelConfig =
{
new ChannelConfig(BlockType.Mono),
new ChannelConfig(BlockType.Mono, BlockType.Mono),
new ChannelConfig(BlockType.Stereo),
new ChannelConfig(BlockType.Stereo, BlockType.Mono, BlockType.LFE, BlockType.Stereo),
new ChannelConfig(BlockType.Stereo, BlockType.Mono, BlockType.LFE, BlockType.Stereo, BlockType.Stereo),
new ChannelConfig(BlockType.Stereo, BlockType.Stereo)
};
public static readonly HuffmanCodebook[] HuffmanScaleFactorsUnsigned =
GenerateHuffmanCodebooks(HuffmanScaleFactorsACodes, HuffmanScaleFactorsABits, HuffmanScaleFactorsGroupSizes);
public static readonly HuffmanCodebook[] HuffmanScaleFactorsSigned =
GenerateHuffmanCodebooks(HuffmanScaleFactorsBCodes, HuffmanScaleFactorsBBits, HuffmanScaleFactorsGroupSizes);
public static readonly HuffmanCodebook[][][] HuffmanSpectrum =
{
GenerateHuffmanCodebooks(HuffmanSpectrumACodes, HuffmanSpectrumABits, HuffmanSpectrumAGroupSizes),
GenerateHuffmanCodebooks(HuffmanSpectrumBCodes, HuffmanSpectrumBBits, HuffmanSpectrumBGroupSizes)
};
public static readonly double[][] ImdctWindow = { GenerateImdctWindow(6), GenerateImdctWindow(7), GenerateImdctWindow(8) };
public static readonly double[] SpectrumScale = Generate(32, SpectrumScaleFunction);
public static readonly double[] QuantizerStepSize = Generate(16, QuantizerStepSizeFunction);
public static readonly double[] QuantizerFineStepSize = Generate(16, QuantizerFineStepSizeFunction);
public static readonly byte[][] GradientCurves = BitAllocation.GenerateGradientCurves();
private static double QuantizerStepSizeFunction(int x) => 2.0 / ((1 << (x + 1)) - 1);
private static double QuantizerFineStepSizeFunction(int x) => QuantizerStepSizeFunction(x) / ushort.MaxValue;
private static double SpectrumScaleFunction(int x) => Math.Pow(2, x - 15);
private static double[] GenerateImdctWindow(int frameSizePower)
{
int frameSize = 1 << frameSizePower;
var output = new double[frameSize];
double[] a1 = GenerateMdctWindow(frameSizePower);
for (int i = 0; i < frameSize; i++)
{
output[i] = a1[i] / (a1[frameSize - 1 - i] * a1[frameSize - 1 - i] + a1[i] * a1[i]);
}
return output;
}
private static double[] GenerateMdctWindow(int frameSizePower)
{
int frameSize = 1 << frameSizePower;
var output = new double[frameSize];
for (int i = 0; i < frameSize; i++)
{
output[i] = (Math.Sin(((i + 0.5) / frameSize - 0.5) * Math.PI) + 1.0) * 0.5;
}
return output;
}
private static T[] Generate<T>(int count, Func<int, T> elementGenerator)
{
var table = new T[count];
for (int i = 0; i < count; i++)
{
table[i] = elementGenerator(i);
}
return table;
}
}
}
-426
View File
@@ -1,426 +0,0 @@
#nullable disable
using System;
using System.IO;
using LibAtrac9.Utilities;
namespace LibAtrac9
{
internal static class Unpack
{
public static void UnpackFrame(BitReader reader, Frame frame)
{
foreach (Block block in frame.Blocks)
{
UnpackBlock(reader, block);
}
}
private static void UnpackBlock(BitReader reader, Block block)
{
ReadBlockHeader(reader, block);
if (block.BlockType == BlockType.LFE)
{
UnpackLfeBlock(reader, block);
}
else
{
UnpackStandardBlock(reader, block);
}
reader.AlignPosition(8);
}
private static void ReadBlockHeader(BitReader reader, Block block)
{
bool firstInSuperframe = block.Frame.FrameIndex == 0;
block.FirstInSuperframe = !reader.ReadBool();
block.ReuseBandParams = reader.ReadBool();
if (block.FirstInSuperframe != firstInSuperframe)
{
throw new InvalidDataException();
}
if (firstInSuperframe && block.ReuseBandParams && block.BlockType != BlockType.LFE)
{
throw new InvalidDataException();
}
}
private static void UnpackStandardBlock(BitReader reader, Block block)
{
Channel[] channels = block.Channels;
if (!block.ReuseBandParams)
{
ReadBandParams(reader, block);
}
ReadGradientParams(reader, block);
BitAllocation.CreateGradient(block);
ReadStereoParams(reader, block);
ReadExtensionParams(reader, block);
foreach (Channel channel in channels)
{
channel.UpdateCodedUnits();
ScaleFactors.Read(reader, channel);
BitAllocation.CalculateMask(channel);
BitAllocation.CalculatePrecisions(channel);
CalculateSpectrumCodebookIndex(channel);
ReadSpectra(reader, channel);
ReadSpectraFine(reader, channel);
}
block.QuantizationUnitsPrev = block.BandExtensionEnabled ? block.ExtensionUnit : block.QuantizationUnitCount;
}
private static void ReadBandParams(BitReader reader, Block block)
{
int minBandCount = Tables.MinBandCount(block.Config.HighSampleRate);
int maxExtensionBand = Tables.MaxExtensionBand(block.Config.HighSampleRate);
block.BandCount = reader.ReadInt(4);
block.BandCount += minBandCount;
block.QuantizationUnitCount = Tables.BandToQuantUnitCount[block.BandCount];
if (block.BandCount < minBandCount || block.BandCount >
Tables.MaxBandCount[block.Config.SampleRateIndex])
{
return;
}
if (block.BlockType == BlockType.Stereo)
{
block.StereoBand = reader.ReadInt(4);
block.StereoBand += minBandCount;
block.StereoQuantizationUnit = Tables.BandToQuantUnitCount[block.StereoBand];
}
else
{
block.StereoBand = block.BandCount;
}
block.BandExtensionEnabled = reader.ReadBool();
if (block.BandExtensionEnabled)
{
block.ExtensionBand = reader.ReadInt(4);
block.ExtensionBand += minBandCount;
if (block.ExtensionBand < block.BandCount || block.ExtensionBand > maxExtensionBand)
{
throw new InvalidDataException();
}
block.ExtensionUnit = Tables.BandToQuantUnitCount[block.ExtensionBand];
}
else
{
block.ExtensionBand = block.BandCount;
block.ExtensionUnit = block.QuantizationUnitCount;
}
}
private static void ReadGradientParams(BitReader reader, Block block)
{
block.GradientMode = reader.ReadInt(2);
if (block.GradientMode > 0)
{
block.GradientEndUnit = 31;
block.GradientEndValue = 31;
block.GradientStartUnit = reader.ReadInt(5);
block.GradientStartValue = reader.ReadInt(5);
}
else
{
block.GradientStartUnit = reader.ReadInt(6);
block.GradientEndUnit = reader.ReadInt(6) + 1;
block.GradientStartValue = reader.ReadInt(5);
block.GradientEndValue = reader.ReadInt(5);
}
block.GradientBoundary = reader.ReadInt(4);
if (block.GradientBoundary > block.QuantizationUnitCount)
{
throw new InvalidDataException();
}
if (block.GradientStartUnit < 1 || block.GradientStartUnit >= 48)
{
throw new InvalidDataException();
}
if (block.GradientEndUnit < 1 || block.GradientEndUnit >= 48)
{
throw new InvalidDataException();
}
if (block.GradientStartUnit > block.GradientEndUnit)
{
throw new InvalidDataException();
}
if (block.GradientStartValue < 0 || block.GradientStartValue >= 32)
{
throw new InvalidDataException();
}
if (block.GradientEndValue < 0 || block.GradientEndValue >= 32)
{
throw new InvalidDataException();
}
}
private static void ReadStereoParams(BitReader reader, Block block)
{
if (block.BlockType != BlockType.Stereo) return;
block.PrimaryChannelIndex = reader.ReadInt(1);
block.HasJointStereoSigns = reader.ReadBool();
if (block.HasJointStereoSigns)
{
for (int i = block.StereoQuantizationUnit; i < block.QuantizationUnitCount; i++)
{
block.JointStereoSigns[i] = reader.ReadInt(1);
}
}
else
{
Array.Clear(block.JointStereoSigns, 0, block.JointStereoSigns.Length);
}
}
private static void ReadExtensionParams(BitReader reader, Block block)
{
// ReSharper disable once RedundantAssignment
int bexBand = 0;
if (block.BandExtensionEnabled)
{
BandExtension.GetBexBandInfo(out bexBand, out _, out _, block.QuantizationUnitCount);
if (block.BlockType == BlockType.Stereo)
{
ReadHeader(block.Channels[1]);
}
else
{
reader.Position += 1;
}
}
block.HasExtensionData = reader.ReadBool();
if (!block.HasExtensionData) return;
if (!block.BandExtensionEnabled)
{
block.BexMode = reader.ReadInt(2);
block.BexDataLength = reader.ReadInt(5);
reader.Position += block.BexDataLength;
return;
}
ReadHeader(block.Channels[0]);
block.BexDataLength = reader.ReadInt(5);
if (block.BexDataLength <= 0) return;
int bexDataEnd = reader.Position + block.BexDataLength;
ReadData(block.Channels[0]);
if (block.BlockType == BlockType.Stereo)
{
ReadData(block.Channels[1]);
}
// Make sure we didn't read too many bits
if (reader.Position > bexDataEnd)
{
throw new InvalidDataException();
}
void ReadHeader(Channel channel)
{
int bexMode = reader.ReadInt(2);
channel.BexMode = bexBand > 2 ? bexMode : 4;
channel.BexValueCount = BandExtension.BexEncodedValueCounts[channel.BexMode][bexBand];
}
void ReadData(Channel channel)
{
for (int i = 0; i < channel.BexValueCount; i++)
{
int dataLength = BandExtension.BexDataLengths[channel.BexMode][bexBand][i];
channel.BexValues[i] = reader.ReadInt(dataLength);
}
}
}
private static void CalculateSpectrumCodebookIndex(Channel channel)
{
Array.Clear(channel.CodebookSet, 0, channel.CodebookSet.Length);
int quantUnits = channel.CodedQuantUnits;
int[] sf = channel.ScaleFactors;
if (quantUnits <= 1) return;
if (channel.Config.HighSampleRate) return;
// Temporarily setting this value allows for simpler code by
// making the last value a non-special case.
int originalScaleTmp = sf[quantUnits];
sf[quantUnits] = sf[quantUnits - 1];
int avg = 0;
if (quantUnits > 12)
{
for (int i = 0; i < 12; i++)
{
avg += sf[i];
}
avg = (avg + 6) / 12;
}
for (int i = 8; i < quantUnits; i++)
{
int prevSf = sf[i - 1];
int nextSf = sf[i + 1];
int minSf = Math.Min(prevSf, nextSf);
if (sf[i] - minSf >= 3 || sf[i] - prevSf + sf[i] - nextSf >= 3)
{
channel.CodebookSet[i] = 1;
}
}
for (int i = 12; i < quantUnits; i++)
{
if (channel.CodebookSet[i] == 0)
{
int minSf = Math.Min(sf[i - 1], sf[i + 1]);
if (sf[i] - minSf >= 2 && sf[i] >= avg - (Tables.QuantUnitToCoeffCount[i] == 16 ? 1 : 0))
{
channel.CodebookSet[i] = 1;
}
}
}
sf[quantUnits] = originalScaleTmp;
}
private static void ReadSpectra(BitReader reader, Channel channel)
{
int[] values = channel.SpectraValuesBuffer;
Array.Clear(channel.QuantizedSpectra, 0, channel.QuantizedSpectra.Length);
int maxHuffPrecision = Tables.MaxHuffPrecision(channel.Config.HighSampleRate);
for (int i = 0; i < channel.CodedQuantUnits; i++)
{
int subbandCount = Tables.QuantUnitToCoeffCount[i];
int precision = channel.Precisions[i] + 1;
if (precision <= maxHuffPrecision)
{
HuffmanCodebook huff = Tables.HuffmanSpectrum[channel.CodebookSet[i]][precision][Tables.QuantUnitToCodebookIndex[i]];
int groupCount = subbandCount >> huff.ValueCountPower;
for (int j = 0; j < groupCount; j++)
{
values[j] = ReadHuffmanValue(huff, reader);
}
DecodeHuffmanValues(channel.QuantizedSpectra, Tables.QuantUnitToCoeffIndex[i], subbandCount, huff, values);
}
else
{
int subbandIndex = Tables.QuantUnitToCoeffIndex[i];
for (int j = subbandIndex; j < Tables.QuantUnitToCoeffIndex[i + 1]; j++)
{
channel.QuantizedSpectra[j] = reader.ReadSignedInt(precision);
}
}
}
}
private static void ReadSpectraFine(BitReader reader, Channel channel)
{
Array.Clear(channel.QuantizedSpectraFine, 0, channel.QuantizedSpectraFine.Length);
for (int i = 0; i < channel.CodedQuantUnits; i++)
{
if (channel.PrecisionsFine[i] > 0)
{
int overflowBits = channel.PrecisionsFine[i] + 1;
int startSubband = Tables.QuantUnitToCoeffIndex[i];
int endSubband = Tables.QuantUnitToCoeffIndex[i + 1];
for (int j = startSubband; j < endSubband; j++)
{
channel.QuantizedSpectraFine[j] = reader.ReadSignedInt(overflowBits);
}
}
}
}
private static void DecodeHuffmanValues(int[] spectrum, int index, int bandCount, HuffmanCodebook huff, int[] values)
{
int valueCount = bandCount >> huff.ValueCountPower;
int mask = (1 << huff.ValueBits) - 1;
for (int i = 0; i < valueCount; i++)
{
int value = values[i];
for (int j = 0; j < huff.ValueCount; j++)
{
spectrum[index++] = Bit.SignExtend32(value & mask, huff.ValueBits);
value >>= huff.ValueBits;
}
}
}
public static int ReadHuffmanValue(HuffmanCodebook huff, BitReader reader, bool signed = false)
{
int code = reader.PeekInt(huff.MaxBitSize);
byte value = huff.Lookup[code];
int bits = huff.Bits[value];
reader.Position += bits;
return signed ? Bit.SignExtend32(value, huff.ValueBits) : value;
}
private static void UnpackLfeBlock(BitReader reader, Block block)
{
Channel channel = block.Channels[0];
block.QuantizationUnitCount = 2;
DecodeLfeScaleFactors(reader, channel);
CalculateLfePrecision(channel);
channel.CodedQuantUnits = block.QuantizationUnitCount;
ReadLfeSpectra(reader, channel);
}
private static void DecodeLfeScaleFactors(BitReader reader, Channel channel)
{
Array.Clear(channel.ScaleFactors, 0, channel.ScaleFactors.Length);
for (int i = 0; i < channel.Block.QuantizationUnitCount; i++)
{
channel.ScaleFactors[i] = reader.ReadInt(5);
}
}
private static void CalculateLfePrecision(Channel channel)
{
Block block = channel.Block;
int precision = block.ReuseBandParams ? 8 : 4;
for (int i = 0; i < block.QuantizationUnitCount; i++)
{
channel.Precisions[i] = precision;
channel.PrecisionsFine[i] = 0;
}
}
private static void ReadLfeSpectra(BitReader reader, Channel channel)
{
Array.Clear(channel.QuantizedSpectra, 0, channel.QuantizedSpectra.Length);
for (int i = 0; i < channel.CodedQuantUnits; i++)
{
if (channel.Precisions[i] <= 0) continue;
int precision = channel.Precisions[i] + 1;
for (int j = Tables.QuantUnitToCoeffIndex[i]; j < Tables.QuantUnitToCoeffIndex[i + 1]; j++)
{
channel.QuantizedSpectra[j] = reader.ReadSignedInt(precision);
}
}
}
}
}
-23
View File
@@ -1,23 +0,0 @@
#nullable disable
namespace LibAtrac9.Utilities
{
internal static class Bit
{
private static uint BitReverse32(uint value)
{
value = ((value & 0xaaaaaaaa) >> 1) | ((value & 0x55555555) << 1);
value = ((value & 0xcccccccc) >> 2) | ((value & 0x33333333) << 2);
value = ((value & 0xf0f0f0f0) >> 4) | ((value & 0x0f0f0f0f) << 4);
value = ((value & 0xff00ff00) >> 8) | ((value & 0x00ff00ff) << 8);
return (value >> 16) | (value << 16);
}
private static uint BitReverse32(uint value, int bitCount) => BitReverse32(value) >> (32 - bitCount);
public static int BitReverse32(int value, int bitCount) => (int) BitReverse32((uint) value, bitCount);
public static int SignExtend32(int value, int bits)
{
int shift = 8 * sizeof(int) - bits;
return (value << shift) >> shift;
}
}
}
@@ -1,133 +0,0 @@
#nullable disable
using System;
using System.Diagnostics;
namespace LibAtrac9.Utilities
{
internal class BitReader
{
private byte[] Buffer { get; set; }
private int LengthBits { get; set; }
public int Position { get; set; }
private int Remaining => LengthBits - Position;
public BitReader(byte[] buffer) => SetBuffer(buffer);
public void SetBuffer(byte[] buffer)
{
Buffer = buffer;
LengthBits = Buffer?.Length * 8 ?? 0;
Position = 0;
}
public int ReadInt(int bitCount)
{
int value = PeekInt(bitCount);
Position += bitCount;
return value;
}
public int ReadSignedInt(int bitCount)
{
int value = PeekInt(bitCount);
Position += bitCount;
return Bit.SignExtend32(value, bitCount);
}
public bool ReadBool() => ReadInt(1) == 1;
public int ReadOffsetBinary(int bitCount, OffsetBias bias)
{
int offset = (1 << (bitCount - 1)) - (int)bias;
int value = PeekInt(bitCount) - offset;
Position += bitCount;
return value;
}
public void AlignPosition(int multiple)
{
Position = Helpers.GetNextMultiple(Position, multiple);
}
public int PeekInt(int bitCount)
{
Debug.Assert(bitCount >= 0 && bitCount <= 32);
if (bitCount > Remaining)
{
if (Position >= LengthBits) return 0;
int extraBits = bitCount - Remaining;
return PeekIntFallback(Remaining) << extraBits;
}
int byteIndex = Position / 8;
int bitIndex = Position % 8;
if (bitCount <= 9 && Remaining >= 16)
{
int value = Buffer[byteIndex] << 8 | Buffer[byteIndex + 1];
value &= 0xFFFF >> bitIndex;
value >>= 16 - bitCount - bitIndex;
return value;
}
if (bitCount <= 17 && Remaining >= 24)
{
int value = Buffer[byteIndex] << 16 | Buffer[byteIndex + 1] << 8 | Buffer[byteIndex + 2];
value &= 0xFFFFFF >> bitIndex;
value >>= 24 - bitCount - bitIndex;
return value;
}
if (bitCount <= 25 && Remaining >= 32)
{
int value = Buffer[byteIndex] << 24 | Buffer[byteIndex + 1] << 16 | Buffer[byteIndex + 2] << 8 | Buffer[byteIndex + 3];
value &= (int)(0xFFFFFFFF >> bitIndex);
value >>= 32 - bitCount - bitIndex;
return value;
}
return PeekIntFallback(bitCount);
}
private int PeekIntFallback(int bitCount)
{
int value = 0;
int byteIndex = Position / 8;
int bitIndex = Position % 8;
while (bitCount > 0)
{
if (bitIndex >= 8)
{
bitIndex = 0;
byteIndex++;
}
int bitsToRead = Math.Min(bitCount, 8 - bitIndex);
int mask = 0xFF >> bitIndex;
int currentByte = (mask & Buffer[byteIndex]) >> (8 - bitIndex - bitsToRead);
value = (value << bitsToRead) | currentByte;
bitIndex += bitsToRead;
bitCount -= bitsToRead;
}
return value;
}
/// <summary>
/// Specifies the bias of an offset binary value. A positive bias can represent one more
/// positive value than negative value, and a negative bias can represent one more
/// negative value than positive value.
/// </summary>
/// <remarks>Example:
/// A 4-bit offset binary value with a positive bias can store
/// the values 8 through -7 inclusive.
/// A 4-bit offset binary value with a positive bias can store
/// the values 7 through -8 inclusive.</remarks>
public enum OffsetBias
{
Negative = 0
}
}
}
@@ -1,51 +0,0 @@
#nullable disable
using System.Runtime.CompilerServices;
namespace LibAtrac9.Utilities
{
internal static class Helpers
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static short Clamp16(int value)
{
if (value > short.MaxValue)
return short.MaxValue;
if (value < short.MinValue)
return short.MinValue;
return (short)value;
}
public static int GetNextMultiple(int value, int multiple)
{
if (multiple <= 0)
return value;
if (value % multiple == 0)
return value;
return value + multiple - value % multiple;
}
/// <summary>
/// Returns the floor of the base 2 logarithm of a specified number.
/// </summary>
/// <param name="value">The number whose logarithm is to be found.</param>
/// <returns>The floor of the base 2 logarithm of <paramref name="value"/>.</returns>
public static int Log2(int value)
{
value |= value >> 1;
value |= value >> 2;
value |= value >> 4;
value |= value >> 8;
value |= value >> 16;
return MultiplyDeBruijnBitPosition[(uint)(value * 0x07C4ACDDU) >> 27];
}
private static readonly int[] MultiplyDeBruijnBitPosition =
{
0, 9, 1, 10, 13, 21, 2, 29, 11, 14, 16, 18, 22, 25, 3, 30,
8, 12, 20, 28, 15, 17, 24, 7, 19, 27, 23, 6, 26, 5, 4, 31
};
}
}
-178
View File
@@ -1,178 +0,0 @@
#nullable disable
using System;
using System.Collections.Generic;
namespace LibAtrac9.Utilities
{
internal class Mdct
{
private int MdctBits { get; }
private int MdctSize { get; }
private double Scale { get; }
private static readonly object TableLock = new object();
private static int _tableBits = -1;
private static readonly List<double[]> SinTables = new List<double[]>();
private static readonly List<double[]> CosTables = new List<double[]>();
private static readonly List<int[]> ShuffleTables = new List<int[]>();
private readonly double[] _imdctPrevious;
private readonly double[] _imdctWindow;
private readonly double[] _scratchMdct;
private readonly double[] _scratchDct;
public Mdct(int mdctBits, double[] window, double scale = 1)
{
SetTables(mdctBits);
MdctBits = mdctBits;
MdctSize = 1 << mdctBits;
Scale = scale;
if (window.Length < MdctSize)
{
throw new ArgumentException("Window must be as long as the MDCT size.", nameof(window));
}
_imdctPrevious = new double[MdctSize];
_scratchMdct = new double[MdctSize];
_scratchDct = new double[MdctSize];
_imdctWindow = window;
}
private static void SetTables(int maxBits)
{
lock (TableLock)
{
if (maxBits > _tableBits)
{
for (int i = _tableBits + 1; i <= maxBits; i++)
{
GenerateTrigTables(i, out double[] sin, out double[] cos);
SinTables.Add(sin);
CosTables.Add(cos);
ShuffleTables.Add(GenerateShuffleTable(i));
}
_tableBits = maxBits;
}
}
}
public void RunImdct(double[] input, double[] output)
{
if (input.Length < MdctSize)
{
throw new ArgumentException("Input must be as long as the MDCT size.", nameof(input));
}
if (output.Length < MdctSize)
{
throw new ArgumentException("Output must be as long as the MDCT size.", nameof(output));
}
int size = MdctSize;
int half = size / 2;
double[] dctOut = _scratchMdct;
Dct4(input, dctOut);
for (int i = 0; i < half; i++)
{
output[i] = _imdctWindow[i] * dctOut[i + half] + _imdctPrevious[i];
output[i + half] = _imdctWindow[i + half] * -dctOut[size - 1 - i] - _imdctPrevious[i + half];
_imdctPrevious[i] = _imdctWindow[size - 1 - i] * -dctOut[half - i - 1];
_imdctPrevious[i + half] = _imdctWindow[half - i - 1] * dctOut[i];
}
}
/// <summary>
/// Does a Type-4 DCT.
/// </summary>
/// <param name="input">The input array containing the time or frequency-domain samples</param>
/// <param name="output">The output array that will contain the transformed time or frequency-domain samples</param>
private void Dct4(double[] input, double[] output)
{
int[] shuffleTable = ShuffleTables[MdctBits];
double[] sinTable = SinTables[MdctBits];
double[] cosTable = CosTables[MdctBits];
double[] dctTemp = _scratchDct;
int size = MdctSize;
int lastIndex = size - 1;
int halfSize = size / 2;
for (int i = 0; i < halfSize; i++)
{
int i2 = i * 2;
double a = input[i2];
double b = input[lastIndex - i2];
double sin = sinTable[i];
double cos = cosTable[i];
dctTemp[i2] = a * cos + b * sin;
dctTemp[i2 + 1] = a * sin - b * cos;
}
int stageCount = MdctBits - 1;
for (int stage = 0; stage < stageCount; stage++)
{
int blockCount = 1 << stage;
int blockSizeBits = stageCount - stage;
int blockHalfSizeBits = blockSizeBits - 1;
int blockSize = 1 << blockSizeBits;
int blockHalfSize = 1 << blockHalfSizeBits;
sinTable = SinTables[blockHalfSizeBits];
cosTable = CosTables[blockHalfSizeBits];
for (int block = 0; block < blockCount; block++)
{
for (int i = 0; i < blockHalfSize; i++)
{
int frontPos = (block * blockSize + i) * 2;
int backPos = frontPos + blockSize;
double a = dctTemp[frontPos] - dctTemp[backPos];
double b = dctTemp[frontPos + 1] - dctTemp[backPos + 1];
double sin = sinTable[i];
double cos = cosTable[i];
dctTemp[frontPos] += dctTemp[backPos];
dctTemp[frontPos + 1] += dctTemp[backPos + 1];
dctTemp[backPos] = a * cos + b * sin;
dctTemp[backPos + 1] = a * sin - b * cos;
}
}
}
for (int i = 0; i < MdctSize; i++)
{
output[i] = dctTemp[shuffleTable[i]] * Scale;
}
}
internal static void GenerateTrigTables(int sizeBits, out double[] sin, out double[] cos)
{
int size = 1 << sizeBits;
sin = new double[size];
cos = new double[size];
for (int i = 0; i < size; i++)
{
double value = Math.PI * (4 * i + 1) / (4 * size);
sin[i] = Math.Sin(value);
cos[i] = Math.Cos(value);
}
}
internal static int[] GenerateShuffleTable(int sizeBits)
{
int size = 1 << sizeBits;
var table = new int[size];
for (int i = 0; i < size; i++)
{
table[i] = Bit.BitReverse32(i ^ (i / 2), sizeBits);
}
return table;
}
}
}
-162
View File
@@ -1,162 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Threading;
using System.Collections.Specialized;
namespace SharpEmu.GUI;
public sealed class ConsoleWindow : Window
{
private readonly AvaloniaList<LogLine> _sourceLines;
private readonly AvaloniaList<LogLine> _visibleLines = new();
private readonly ListBox _list;
private readonly TextBox _searchBox;
private readonly CheckBox _autoScrollCheck;
public ConsoleWindow(
AvaloniaList<LogLine> lines,
Action clear,
bool autoScroll)
{
var loc = Localization.Instance;
_sourceLines = lines;
Title = loc.Get("Console.WindowTitle");
Width = 980;
Height = 620;
MinWidth = 520;
MinHeight = 320;
Background = new SolidColorBrush(Color.Parse("#0D1017"));
Icon = new WindowIcon(AssetLoader.Open(new Uri("avares://SharpEmu.GUI/Assets/SharpEmu.ico")));
_searchBox = new TextBox
{
Watermark = loc.Get("Console.SearchWatermark"),
Width = 320,
Margin = new Thickness(0, 0, 12, 0),
};
_autoScrollCheck = new CheckBox
{
Content = loc.Get("Console.AutoScroll"),
IsChecked = autoScroll,
FontSize = 12,
Margin = new Thickness(0, 0, 12, 0),
VerticalAlignment = VerticalAlignment.Center,
};
var copyButton = new Button
{
Classes = { "ghost" },
Content = loc.Get("Console.Copy"),
Padding = new Thickness(10, 4),
Margin = new Thickness(0, 0, 8, 0),
};
var clearButton = new Button
{
Classes = { "ghost" },
Content = loc.Get("Console.Clear"),
Padding = new Thickness(10, 4),
};
copyButton.Click += async (_, _) => await CopyAsync();
clearButton.Click += (_, _) => clear();
_searchBox.TextChanged += (_, _) => RefreshVisibleLines();
_list = new ListBox
{
Classes = { "console" },
ItemsSource = _visibleLines,
BorderThickness = new Thickness(1),
BorderBrush = new SolidColorBrush(Color.Parse("#232B3A")),
ItemTemplate = new FuncDataTemplate<LogLine>((_, _) =>
{
var text = new TextBlock { TextWrapping = TextWrapping.NoWrap };
text.Bind(TextBlock.TextProperty, new Binding(nameof(LogLine.Text)));
text.Bind(TextBlock.ForegroundProperty, new Binding(nameof(LogLine.Brush)));
return text;
}),
};
Content = new Grid
{
Margin = new Thickness(12),
RowDefinitions = new RowDefinitions("Auto,*"),
Children =
{
new Grid
{
Margin = new Thickness(0, 0, 0, 8),
ColumnDefinitions = new ColumnDefinitions("*,Auto,Auto,Auto,Auto"),
Children =
{
new TextBlock
{
Classes = { "sectionTitle" },
Text = loc.Get("Console.Title"),
VerticalAlignment = VerticalAlignment.Center,
},
_searchBox.WithGridColumn(1),
_autoScrollCheck.WithGridColumn(2),
copyButton.WithGridColumn(3),
clearButton.WithGridColumn(4),
},
},
_list.WithGridRow(1),
},
};
lines.CollectionChanged += OnLinesChanged;
Closed += (_, _) => lines.CollectionChanged -= OnLinesChanged;
RefreshVisibleLines();
}
private void OnLinesChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
RefreshVisibleLines();
if (_autoScrollCheck.IsChecked == true)
{
Dispatcher.UIThread.Post(() => (_list.Scroll as ScrollViewer)?.ScrollToEnd());
}
}
private void RefreshVisibleLines()
{
var query = _searchBox.Text ?? string.Empty;
_visibleLines.Clear();
_visibleLines.AddRange(string.IsNullOrWhiteSpace(query)
? _sourceLines
: _sourceLines.Where(line => line.Text.Contains(query, StringComparison.OrdinalIgnoreCase)));
}
private async Task CopyAsync()
{
if (_visibleLines.Count == 0 || Clipboard is null)
{
return;
}
await Clipboard.SetTextAsync(string.Join(Environment.NewLine, _visibleLines.Select(line => line.Text)));
}
}
file static class GridExtensions
{
public static T WithGridColumn<T>(this T control, int column) where T : Control
{
Grid.SetColumn(control, column);
return control;
}
public static T WithGridRow<T>(this T control, int row) where T : Control
{
Grid.SetRow(control, row);
return control;
}
}
-252
View File
@@ -1,252 +0,0 @@
// 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.
}
}
}
+22 -14
View File
@@ -85,14 +85,6 @@ 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);
@@ -168,7 +160,8 @@ 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_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF |
PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF;
mitigationPolicies = Marshal.AllocHGlobal(sizeof(ulong) * 2);
Marshal.WriteInt64(mitigationPolicies, unchecked((long)policy1));
@@ -199,10 +192,29 @@ 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}' with CET/CFG mitigation disabled (Win32 error {error}: {new Win32Exception(error).Message}).");
throw new Win32Exception(error, $"Failed to start '{exePath}' (Win32 error {error}: {new Win32Exception(error).Message}).");
}
CloseHandle(processInfo.hThread);
@@ -627,10 +639,6 @@ 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);
+4 -149
View File
@@ -1,158 +1,13 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.ComponentModel;
using Avalonia;
using Avalonia.Media;
using Avalonia.Media.Imaging;
namespace SharpEmu.GUI;
public sealed class GameEntry : INotifyPropertyChanged
public sealed record GameEntry(string Name, string? TitleId, string Path, long SizeBytes)
{
// Placeholder gradients for games without cover art, picked
// deterministically from the game name so a game keeps its color.
private static readonly (Color Start, Color End)[] PlaceholderPalette =
{
(Color.Parse("#5B4B8A"), Color.Parse("#2C2A4A")),
(Color.Parse("#1F6E8C"), Color.Parse("#173B45")),
(Color.Parse("#7A4069"), Color.Parse("#3B1C32")),
(Color.Parse("#2D6A4F"), Color.Parse("#1B3A2B")),
(Color.Parse("#8C5425"), Color.Parse("#4A2B12")),
(Color.Parse("#4F6D9E"), Color.Parse("#263349")),
(Color.Parse("#8A4B4B"), Color.Parse("#3F2222")),
(Color.Parse("#3E7C7B"), Color.Parse("#1E3D3C")),
};
private Bitmap? _cover;
private IBrush? _placeholderBrush;
private long _sizeBytes;
public GameEntry(
string name, string? titleId, string? version, string path, long sizeBytes,
string? coverPath, string? backgroundPath)
{
Name = name;
TitleId = titleId;
Version = version;
Path = path;
_sizeBytes = sizeBytes;
CoverPath = coverPath;
BackgroundPath = backgroundPath;
Initials = ComputeInitials(name);
}
public event PropertyChangedEventHandler? PropertyChanged;
public string Name { get; }
public string? TitleId { get; }
/// <summary>Content version from sce_sys/param.json, e.g. "01.000.000".</summary>
public string? Version { get; }
public string Path { get; }
/// <summary>
/// Total size of the game. Initially the eboot's own size from the scan;
/// replaced with the full install folder size once computed in the
/// background.
/// </summary>
public long SizeBytes
{
get => _sizeBytes;
set
{
if (_sizeBytes == value)
{
return;
}
_sizeBytes = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SizeBytes)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SizeText)));
}
}
/// <summary>Path to the cover art image shipped with the game, if found.</summary>
public string? CoverPath { get; }
/// <summary>Path to the key art (pic0/pic1) shipped with the game, if found.</summary>
public string? BackgroundPath { get; }
/// <summary>
/// Decoded key art used as the window backdrop while this game is
/// selected. Loaded on demand and cached; not exposed via binding.
/// </summary>
public Bitmap? Background { get; set; }
public string Initials { get; }
// Built lazily: brushes are AvaloniaObjects that must be created on the
// UI thread, while GameEntry itself is constructed on the scan thread.
public IBrush PlaceholderBrush => _placeholderBrush ??= BuildPlaceholderBrush(Name);
/// <summary>Decoded cover art; loaded asynchronously after the library scan.</summary>
public Bitmap? Cover
{
get => _cover;
set
{
if (ReferenceEquals(_cover, value))
{
return;
}
_cover = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Cover)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HasCover)));
}
}
public bool HasCover => _cover is not null;
public bool HasTitleId => TitleId is not null;
/// <summary>Badge text shown in the launch bar, e.g. "v01.000.000".</summary>
public string? VersionText => Version is null ? null : $"v{Version}";
public bool HasVersion => Version is not null;
/// <summary>Formatted install size badge shown in the launch bar.</summary>
public string SizeText => FormatSize(SizeBytes);
private static string ComputeInitials(string name)
{
var initials = name
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Where(word => char.IsLetterOrDigit(word[0]))
.Select(word => char.ToUpperInvariant(word[0]))
.Take(2)
.ToArray();
return initials.Length > 0 ? new string(initials) : "?";
}
private static IBrush BuildPlaceholderBrush(string name)
{
var hash = 0;
foreach (var ch in name)
{
hash = unchecked(hash * 31 + ch);
}
var (start, end) = PlaceholderPalette[(int)((uint)hash % PlaceholderPalette.Length)];
return new LinearGradientBrush
{
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
GradientStops =
{
new GradientStop(start, 0),
new GradientStop(end, 1),
},
};
}
public string Detail => TitleId is not null
? $"{TitleId} • {FormatSize(SizeBytes)}"
: $"{FormatSize(SizeBytes)} • {Path}";
private static string FormatSize(long bytes)
{
-33
View File
@@ -14,47 +14,14 @@ public sealed class GuiSettings
public List<string> GameFolders { get; set; } = new();
/// <summary>Eboot paths hidden from the library via "Remove from library".</summary>
public List<string> ExcludedGames { get; set; } = new();
public string LogLevel { get; set; } = "Info";
public int ImportTraceLimit { get; set; }
public bool StrictDynlibResolution { get; set; }
/// <summary>
/// Mirror emulator output to user/logs/&lt;titleId&gt;-&lt;timestamp&gt;.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 &lt;titleId&gt;-&lt;timestamp&gt; to the filename specified by
/// <see cref="LogFilePath"/>. Otherwise it uses the exact filename from <see cref="LogFilePath"/>
/// </summary>
public bool OverrideLogFile { get; set; }
/// <summary>Loop the selected game's sce_sys/snd0.at9 preview music.</summary>
public bool PlayTitleMusic { get; set; } = true;
public string? EmulatorPath { get; set; }
/// <summary>UI language, matching a file code under Languages/ (e.g. "en", "tr").</summary>
public string Language { get; set; } = "en";
/// <summary>Publish launcher/game status to Discord Rich Presence.</summary>
public bool DiscordRichPresence { get; set; } = true;
/// <summary>
/// Discord application ID used for Rich Presence; the default is the
/// SharpEmu application. Override to rebrand what Discord shows as
/// "Playing …" (register at discord.com/developers/applications).
/// </summary>
public string DiscordClientId { get; set; } = "1525606762248540221";
// The emulator is portable and keeps its data next to the executable;
// the GUI follows the same convention.
public static string SettingsPath => Path.Combine(AppContext.BaseDirectory, "gui-settings.json");
-129
View File
@@ -1,129 +0,0 @@
{
"_languageName": "العربية",
"Page.Library": "المكتبة",
"Page.Options": "الخيارات",
"Page.GameCount.One": "لعبة واحدة",
"Page.GameCount.Other": "{0} لعبة",
"Library.SearchWatermark": "ابحث في المكتبة...",
"Library.AddFolder": "+ إضافة مجلد",
"Library.Rescan": "⟳ إعادة الفحص",
"Library.OpenFile": "فتح ملف...",
"Library.Context.Launch": "تشغيل",
"Library.Context.OpenFolder": "فتح مجلد اللعبة",
"Library.Context.CopyPath": "نسخ المسار",
"Library.Context.CopyTitleId": "نسخ معرف العنوان",
"Library.Context.Remove": "إزالة من المكتبة",
"Library.Empty.Title": "مكتبتك فارغة",
"Library.Empty.Hint": "أضف مجلداً يحتوي على ألعابك للبدء.",
"Library.Empty.SearchTitle": "لا توجد ألعاب تطابق بحثك",
"Library.Empty.SearchHint": "لا شيء في المكتبة يطابق “{0}”.",
"Library.Empty.AddFolder": "+ إضافة مجلد ألعاب",
"Library.Loading": "جارٍ تحميل المكتبة...",
"Options.General": "عام",
"Options.Section.Emulation": "المحاكاة",
"Options.Section.Logging": "التسجيل",
"Options.Section.Launcher": "المُشغِّل",
"Options.CpuEngine.Label": "محرك المعالج",
"Options.CpuEngine.Desc": "محرك التنفيذ المستخدم لتشغيل كود اللعبة.",
"Options.CpuEngine.Native": "أصلي",
"Options.Strict.Label": "ربط صارم للمكتبات الديناميكية (dynlib)",
"Options.Strict.Desc": "إفشال التشغيل عندما يتعذر التعرف على رمز مستورد.",
"Options.LogLevel.Label": "مستوى التسجيل",
"Options.LogLevel.Desc": "مدى تفصيل مخرجات نافذة سجلات المحاكي.",
"Options.LogLevel.Trace": "تتبع",
"Options.LogLevel.Debug": "تصحيح الأخطاء",
"Options.LogLevel.Info": "معلومات",
"Options.LogLevel.Warning": "تحذير",
"Options.LogLevel.Error": "خطأ",
"Options.LogLevel.Critical": "حرج",
"Options.TraceImports.Label": "حد تتبع الاستيراد",
"Options.TraceImports.Desc": "تتبع أول N استيراد لكل وحدة (0 = إيقاف).",
"Options.LogToFile.Label": "التسجيل في ملف",
"Options.LogToFile.Desc": "نسخ مخرجات المحاكي إلى ملف سجل.",
"Options.LogFilePath.Label": "مسار ملف السجل",
"Options.LogFilePath.Default": "لا يوجد مسار مخصص — تُحفظ السجلات في user/logs بجوار المحاكي.",
"Options.LogFilePath.Select": "تحديد...",
"Options.OverrideLogFile.Label": "الكتابة فوق ملف السجل",
"Options.OverrideLogFile.Desc": "استخدام مسار الملف الدقيق بدلاً من إلحاق معرف العنوان والطابع الزمني.",
"Options.TitleMusic.Label": "موسيقى اللعبة",
"Options.TitleMusic.Desc": "تكرار موسيقى المعاينة للعبة المحددة في المكتبة.",
"Options.Discord.Label": "حالة دسكورد",
"Options.Discord.Desc": "إظهار اللعبة قيد التشغيل في ملفك الشخصي على دسكورد.",
"Options.Language.Label": "لغة المحاكي",
"Options.Language.Desc": "اللغة المستخدمة في جميع أنحاء المشغل. تُطبق فوراً.",
"Common.On": "تشغيل",
"Common.Off": "إيقاف",
"Console.Title": "نافذة السجلات",
"Console.SearchWatermark": "بحث...",
"Console.AutoScroll": "تمرير تلقائي",
"Console.Split": "تقسيم",
"Console.Copy": "نسخ",
"Console.Clear": "مسح",
"Console.WindowTitle": "نافذة سجلات SharpEmu",
"Launch.NoGameSelected": "لم تُحدد أي لعبة",
"Launch.NoGameHint": "اختر لعبة من المكتبة، أو افتح ملف eboot.bin مباشرة.",
"Launch.Idle": "خامل",
"Launch.Console": "≡ نافذة السجلات",
"Launch.Launch": "▶ تشغيل",
"Launch.Stop": "■ إيقاف",
"Launch.Running": "قيد التشغيل — {0}",
"Launch.Stopping": "جارٍ الإيقاف...",
"Launch.Exited": "انتهى برمز {0} ({1})",
"Launch.ExeNotFound": "لم يُعثر على الملف التنفيذي SharpEmu. ابنِ مشروع SharpEmu.CLI أولاً (dotnet build).",
"Launch.LogFile": "ملف السجل: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "فشل بدء تشغيل المحاكي: {0}",
"Launch.ProcessExited": "انتهت العملية برمز {0} ({1}).",
"Exit.Ok": "موافق",
"Exit.InvalidArguments": "معطيات غير صالحة",
"Exit.EbootNotFound": "لم يُعثر على eboot",
"Exit.RuntimeException": "استثناء وقت التشغيل",
"Exit.EmulationError": "خطأ في المحاكاة",
"Exit.Unknown": "غير معروف",
"Status.EmulatorLocating": "المحاكي: جارٍ تحديد الموقع...",
"Status.EmulatorPath": "المحاكي: {0}",
"Status.EmulatorNotFound": "المحاكي: لم يُعثر على الملف التنفيذي SharpEmu — ابنِ SharpEmu.CLI أولاً.",
"Status.ScanningLibrary": "جارٍ فحص المكتبة...",
"Status.AddFolderPrompt": "أضف مجلد ألعاب لملء المكتبة.",
"Status.LibraryScanned": "فُحصت المكتبة: {0} لعبة في {1} مجلد.",
"Status.CouldNotOpenFolder": "تعذر فتح المجلد: {0}",
"Status.CopiedToClipboard": "نُسخ {0} إلى الحافظة.",
"Status.RemovedFromLibrary": "أُزيل “{0}” من المكتبة. أعد إضافة مجلده لاستعادته.",
"Status.Running": "جارٍ تشغيل {0}",
"Status.Stopping": "جارٍ الإيقاف...",
"Status.Idle": "خامل",
"Clipboard.Path": "المسار",
"Clipboard.TitleId": "معرف العنوان",
"Discord.Playing": "يلعب {0}",
"Discord.Browsing": "يتصفح المكتبة",
"Dialog.ChooseGameFolder": "اختر مجلداً يحتوي على ألعاب",
"Dialog.OpenExecutable": "افتح ملفاً تنفيذياً لتشغيله",
"Dialog.PsExecutables": "ملفات PS التنفيذية",
"Dialog.SaveLogFile": "حدد مكان حفظ ملف السجل",
"Dialog.PlainTextFiles": "ملفات نصية عادية",
"Dialog.LogFiles": "ملفات السجل"
}
-129
View File
@@ -1,129 +0,0 @@
{
"_languageName": "Deutsch",
"Page.Library": "Bibliothek",
"Page.Options": "Optionen",
"Page.GameCount.One": "1 Spiel",
"Page.GameCount.Other": "{0} Spiele",
"Library.SearchWatermark": "Bibliothek durchsuchen…",
"Library.AddFolder": " Spielordner hinzufügen",
"Library.Rescan": "⟳ Neu scannen",
"Library.OpenFile": "Datei öffnen…",
"Library.Context.Launch": "Starten",
"Library.Context.OpenFolder": "Spielordner öffnen",
"Library.Context.CopyPath": "Pfad kopieren",
"Library.Context.CopyTitleId": "Title ID kopieren",
"Library.Context.Remove": "Aus Bibliothek entfernen",
"Library.Empty.Title": "Deine Bibliothek ist leer",
"Library.Empty.Hint": "Füge einen Ordner mit deinen Spielen hinzu, um zu beginnen.",
"Library.Empty.SearchTitle": "Keine Spiele gefunden",
"Library.Empty.SearchHint": "Nichts in der Bibliothek entspricht "{0}".",
"Library.Empty.AddFolder": " Spielordner hinzufügen",
"Library.Loading": "Bibliothek wird geladen…",
"Options.General": "Allgemein",
"Options.Section.Emulation": "EMULATION",
"Options.Section.Logging": "PROTOKOLLIERUNG",
"Options.Section.Launcher": "LAUNCHER",
"Options.CpuEngine.Label": "CPU-Engine",
"Options.CpuEngine.Desc": "Ausführungs-Engine, die zum Ausführen des Spiel-Codes verwendet wird.",
"Options.CpuEngine.Native": "Nativ",
"Options.Strict.Label": "Strikte dynlib-Auflösung",
"Options.Strict.Desc": "Starten abbrechen, wenn ein importiertes Symbol nicht aufgelöst werden kann.",
"Options.LogLevel.Label": "Protokollstufe",
"Options.LogLevel.Desc": "Ausführlichkeit der Emulator-Konsolenausgabe.",
"Options.LogLevel.Trace": "Trace",
"Options.LogLevel.Debug": "Debug",
"Options.LogLevel.Info": "Info",
"Options.LogLevel.Warning": "Warnung",
"Options.LogLevel.Error": "Fehler",
"Options.LogLevel.Critical": "Kritisch",
"Options.TraceImports.Label": "Import-Trace-Limit",
"Options.TraceImports.Desc": "Die ersten N Imports pro Modul verfolgen (0 = aus).",
"Options.LogToFile.Label": "In Datei protokollieren",
"Options.LogToFile.Desc": "Emulator-Ausgabe zusätzlich in eine Log-Datei schreiben.",
"Options.LogFilePath.Label": "Protokolldatei-Pfad",
"Options.LogFilePath.Default": "Kein benutzerdefinierter Pfad Logs werden im Ordner user/logs neben dem Emulator gespeichert.",
"Options.LogFilePath.Select": "Auswählen…",
"Options.OverrideLogFile.Label": "Protokolldatei überschreiben",
"Options.OverrideLogFile.Desc": "Genauen Dateipfad verwenden, statt Title-ID und Zeitstempel anzuhängen.",
"Options.TitleMusic.Label": "Titel-Musik",
"Options.TitleMusic.Desc": "Die Vorschau-Musik des ausgewählten Spiels in der Bibliothek loopend abspielen.",
"Options.Discord.Label": "Discord-Präsenz",
"Options.Discord.Desc": "Zeigt das aktuell gespielte Spiel in deinem Discord-Profil an.",
"Options.Language.Label": "Emulator-Sprache",
"Options.Language.Desc": "Sprache der Benutzeroberfläche. Wird sofort angewendet.",
"Common.On": "An",
"Common.Off": "Aus",
"Console.Title": "KONSOLE",
"Console.SearchWatermark": "Suchen...",
"Console.AutoScroll": "Auto-Scroll",
"Console.Split": "Teilen",
"Console.Copy": "Kopieren",
"Console.Clear": "Leeren",
"Console.WindowTitle": "SharpEmu Konsole",
"Launch.NoGameSelected": "Kein Spiel ausgewählt",
"Launch.NoGameHint": "Wähle ein Spiel aus der Bibliothek aus oder öffne eine eboot.bin direkt.",
"Launch.Idle": "Bereit",
"Launch.Console": "≡ Konsole",
"Launch.Launch": "▶ Starten",
"Launch.Stop": "■ Stoppen",
"Launch.Running": "Läuft -- {0}",
"Launch.Stopping": "Wird beendet…",
"Launch.Exited": "Beendet mit Code {0} ({1})",
"Launch.ExeNotFound": "SharpEmu-Executable wurde nicht gefunden. Baue zuerst das SharpEmu.CLI-Projekt (dotnet build).",
"Launch.LogFile": "Log-Datei: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Emulator konnte nicht gestartet werden: {0}",
"Launch.ProcessExited": "Prozess wurde mit Code {0} ({1}) beendet.",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "Ungültige Argumente",
"Exit.EbootNotFound": "eboot nicht gefunden",
"Exit.RuntimeException": "Laufzeitfehler",
"Exit.EmulationError": "Emulationsfehler",
"Exit.Unknown": "unbekannt",
"Status.EmulatorLocating": "Emulator: wird gesucht…",
"Status.EmulatorPath": "Emulator: {0}",
"Status.EmulatorNotFound": "Emulator: SharpEmu-Executable nicht gefunden -- baue zuerst SharpEmu.CLI.",
"Status.ScanningLibrary": "Bibliothek wird gescannt…",
"Status.AddFolderPrompt": "Füge einen Spielordner hinzu, um die Bibliothek zu füllen.",
"Status.LibraryScanned": "Bibliothek gescannt: {0} Spiel(e) in {1} Ordner(n).",
"Status.CouldNotOpenFolder": "Ordner konnte nicht geöffnet werden: {0}",
"Status.CopiedToClipboard": "{0} in die Zwischenablage kopiert.",
"Status.RemovedFromLibrary": ""{0}" wurde aus der Bibliothek entfernt. Füge den Ordner erneut hinzu, um es wiederherzustellen.",
"Status.Running": "Läuft {0}",
"Status.Stopping": "Wird gestoppt…",
"Status.Idle": "Bereit",
"Clipboard.Path": "Pfad",
"Clipboard.TitleId": "Title ID",
"Discord.Playing": "Spielt {0}",
"Discord.Browsing": "Durchsucht die Bibliothek",
"Dialog.ChooseGameFolder": "Spielordner auswählen",
"Dialog.OpenExecutable": "Ausführbare Datei zum Starten öffnen",
"Dialog.PsExecutables": "PS-Ausführbare Dateien",
"Dialog.SaveLogFile": "Protokolldatei speichern unter",
"Dialog.PlainTextFiles": "Textdateien",
"Dialog.LogFiles": "Protokolldateien"
}
-129
View File
@@ -1,129 +0,0 @@
{
"_languageName": "Dansk",
"Page.Library": "Bibliotek",
"Page.Options": "Indstillinger",
"Page.GameCount.One": "1 spil",
"Page.GameCount.Other": "{0} spil",
"Library.SearchWatermark": "Søg i biblioteket…",
"Library.AddFolder": " Tilføj mappe",
"Library.Rescan": "⟳ Genindlæs",
"Library.OpenFile": "Åbn fil…",
"Library.Context.Launch": "Start",
"Library.Context.OpenFolder": "Åbn spilmappe",
"Library.Context.CopyPath": "Kopiér sti",
"Library.Context.CopyTitleId": "Kopiér titel-ID",
"Library.Context.Remove": "Fjern fra bibliotek",
"Library.Empty.Title": "Dit bibliotek er tomt",
"Library.Empty.Hint": "Tilføj en mappe med dine spil for at komme i gang.",
"Library.Empty.SearchTitle": "Ingen spil matcher din søgning",
"Library.Empty.SearchHint": "Intet i biblioteket matcher “{0}”.",
"Library.Empty.AddFolder": " Tilføj spilmappe",
"Library.Loading": "Indlæser bibliotek…",
"Options.General": "Generelt",
"Options.Section.Emulation": "EMULERING",
"Options.Section.Logging": "LOGGING",
"Options.Section.Launcher": "LAUNCHER",
"Options.CpuEngine.Label": "CPU-engine",
"Options.CpuEngine.Desc": "Den eksekveringsengine der bruges til at køre spilkode.",
"Options.CpuEngine.Native": "Native",
"Options.Strict.Label": "Streng dynlib-opløsning",
"Options.Strict.Desc": "Afbryd opstarten, når et importeret symbol ikke kan findes.",
"Options.LogLevel.Label": "Logniveau",
"Options.LogLevel.Desc": "Detaljeringsgrad for emulatorens konsoloutput.",
"Options.LogLevel.Trace": "Trace",
"Options.LogLevel.Debug": "Debug",
"Options.LogLevel.Info": "Info",
"Options.LogLevel.Warning": "Advarsel",
"Options.LogLevel.Error": "Fejl",
"Options.LogLevel.Critical": "Kritisk",
"Options.TraceImports.Label": "Grænse for import-trace",
"Options.TraceImports.Desc": "Spor de første N imports pr. modul (0 = fra).",
"Options.LogToFile.Label": "Log til fil",
"Options.LogToFile.Desc": "Spejl emulatorens output til en logfil.",
"Options.LogFilePath.Label": "Sti til logfil",
"Options.LogFilePath.Default": "Ingen brugerdefineret sti — logs gemmes i user/logs ved siden af emulatoren.",
"Options.LogFilePath.Select": "Vælg…",
"Options.OverrideLogFile.Label": "Tilsidesæt logfil",
"Options.OverrideLogFile.Desc": "Brug den præcise filsti i stedet for at tilføje titel-ID og tidsstempel.",
"Options.TitleMusic.Label": "Titelmusik",
"Options.TitleMusic.Desc": "Gentag det valgte spils forhåndsvisningsmusik i biblioteket.",
"Options.Discord.Label": "Discord-tilstedeværelse",
"Options.Discord.Desc": "Vis det kørende spil på din Discord-profil.",
"Options.Language.Label": "Emulatorsprog",
"Options.Language.Desc": "Sprog der bruges i hele launcheren. Anvendes med det samme.",
"Common.On": "Til",
"Common.Off": "Fra",
"Console.Title": "KONSOL",
"Console.SearchWatermark": "Søg...",
"Console.AutoScroll": "Auto-scroll",
"Console.Split": "Opdel",
"Console.Copy": "Kopiér",
"Console.Clear": "Ryd",
"Console.WindowTitle": "SharpEmu-konsol",
"Launch.NoGameSelected": "Intet spil valgt",
"Launch.NoGameHint": "Vælg et spil fra biblioteket, eller åbn en eboot.bin direkte.",
"Launch.Idle": "Inaktiv",
"Launch.Console": "≡ Konsol",
"Launch.Launch": "▶ Start",
"Launch.Stop": "■ Stop",
"Launch.Running": "Kører — {0}",
"Launch.Stopping": "Stopper…",
"Launch.Exited": "Afsluttet med kode {0} ({1})",
"Launch.ExeNotFound": "SharpEmu-programmet blev ikke fundet. Byg SharpEmu.CLI-projektet først (dotnet build).",
"Launch.LogFile": "Logfil: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Kunne ikke starte emulatoren: {0}",
"Launch.ProcessExited": "Processen afsluttede med kode {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "ugyldige argumenter",
"Exit.EbootNotFound": "eboot ikke fundet",
"Exit.RuntimeException": "runtime-fejl",
"Exit.EmulationError": "emuleringsfejl",
"Exit.Unknown": "ukendt",
"Status.EmulatorLocating": "Emulator: lokaliserer…",
"Status.EmulatorPath": "Emulator: {0}",
"Status.EmulatorNotFound": "Emulator: SharpEmu-programmet blev ikke fundet — byg SharpEmu.CLI først.",
"Status.ScanningLibrary": "Skanner bibliotek…",
"Status.AddFolderPrompt": "Tilføj en spilmappe for at udfylde biblioteket.",
"Status.LibraryScanned": "Bibliotek skannet: {0} spil i {1} mappe(r).",
"Status.CouldNotOpenFolder": "Kunne ikke åbne mappe: {0}",
"Status.CopiedToClipboard": "{0} kopieret til udklipsholderen.",
"Status.RemovedFromLibrary": "“{0}” fjernet fra biblioteket. Tilføj mappen igen for at gendanne det.",
"Status.Running": "Kører {0}",
"Status.Stopping": "Stopper…",
"Status.Idle": "Inaktiv",
"Clipboard.Path": "Sti",
"Clipboard.TitleId": "Titel-ID",
"Discord.Playing": "Spiller {0}",
"Discord.Browsing": "Gennemser biblioteket",
"Dialog.ChooseGameFolder": "Vælg en mappe der indeholder spil",
"Dialog.OpenExecutable": "Åbn et program der skal startes",
"Dialog.PsExecutables": "PS-programmer",
"Dialog.SaveLogFile": "Vælg hvor logfilen skal gemmes",
"Dialog.PlainTextFiles": "Almindelige tekstfiler",
"Dialog.LogFiles": "Logfiler"
}
-129
View File
@@ -1,129 +0,0 @@
{
"_languageName": "English",
"Page.Library": "Library",
"Page.Options": "Options",
"Page.GameCount.One": "1 game",
"Page.GameCount.Other": "{0} games",
"Library.SearchWatermark": "Search library…",
"Library.AddFolder": " Add folder",
"Library.Rescan": "⟳ Rescan",
"Library.OpenFile": "Open file…",
"Library.Context.Launch": "Launch",
"Library.Context.OpenFolder": "Open game folder",
"Library.Context.CopyPath": "Copy path",
"Library.Context.CopyTitleId": "Copy title ID",
"Library.Context.Remove": "Remove from library",
"Library.Empty.Title": "Your library is empty",
"Library.Empty.Hint": "Add a folder containing your games to get started.",
"Library.Empty.SearchTitle": "No games match your search",
"Library.Empty.SearchHint": "Nothing in the library matches “{0}”.",
"Library.Empty.AddFolder": " Add game folder",
"Library.Loading": "Loading library…",
"Options.General": "General",
"Options.Section.Emulation": "EMULATION",
"Options.Section.Logging": "LOGGING",
"Options.Section.Launcher": "LAUNCHER",
"Options.CpuEngine.Label": "CPU engine",
"Options.CpuEngine.Desc": "Execution engine used to run game code.",
"Options.CpuEngine.Native": "Native",
"Options.Strict.Label": "Strict dynlib resolution",
"Options.Strict.Desc": "Fail the launch when an imported symbol cannot be resolved.",
"Options.LogLevel.Label": "Log level",
"Options.LogLevel.Desc": "Verbosity of the emulator console output.",
"Options.LogLevel.Trace": "Trace",
"Options.LogLevel.Debug": "Debug",
"Options.LogLevel.Info": "Info",
"Options.LogLevel.Warning": "Warning",
"Options.LogLevel.Error": "Error",
"Options.LogLevel.Critical": "Critical",
"Options.TraceImports.Label": "Import trace limit",
"Options.TraceImports.Desc": "Trace the first N imports per module (0 = off).",
"Options.LogToFile.Label": "Log to file",
"Options.LogToFile.Desc": "Mirror emulator output to a log file.",
"Options.LogFilePath.Label": "Log file path",
"Options.LogFilePath.Default": "No custom path — logs go to user/logs next to the emulator.",
"Options.LogFilePath.Select": "Select…",
"Options.OverrideLogFile.Label": "Override log file",
"Options.OverrideLogFile.Desc": "Use the exact file path instead of appending title ID and timestamp.",
"Options.TitleMusic.Label": "Title music",
"Options.TitleMusic.Desc": "Loop the selected game's preview music in the library.",
"Options.Discord.Label": "Discord presence",
"Options.Discord.Desc": "Show the running game on your Discord profile.",
"Options.Language.Label": "Emulator language",
"Options.Language.Desc": "Language used throughout the launcher. Applies immediately.",
"Common.On": "On",
"Common.Off": "Off",
"Console.Title": "CONSOLE",
"Console.SearchWatermark": "Search...",
"Console.AutoScroll": "Auto-scroll",
"Console.Split": "Split",
"Console.Copy": "Copy",
"Console.Clear": "Clear",
"Console.WindowTitle": "SharpEmu Console",
"Launch.NoGameSelected": "No game selected",
"Launch.NoGameHint": "Pick a game from the library, or open an eboot.bin directly.",
"Launch.Idle": "Idle",
"Launch.Console": "≡ Console",
"Launch.Launch": "▶ Launch",
"Launch.Stop": "■ Stop",
"Launch.Running": "Running — {0}",
"Launch.Stopping": "Stopping…",
"Launch.Exited": "Exited with code {0} ({1})",
"Launch.ExeNotFound": "SharpEmu executable not found. Build the SharpEmu.CLI project first (dotnet build).",
"Launch.LogFile": "Log file: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Failed to start the emulator: {0}",
"Launch.ProcessExited": "Process exited with code {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "invalid arguments",
"Exit.EbootNotFound": "eboot not found",
"Exit.RuntimeException": "runtime exception",
"Exit.EmulationError": "emulation error",
"Exit.Unknown": "unknown",
"Status.EmulatorLocating": "Emulator: locating…",
"Status.EmulatorPath": "Emulator: {0}",
"Status.EmulatorNotFound": "Emulator: SharpEmu executable not found — build SharpEmu.CLI first.",
"Status.ScanningLibrary": "Scanning library…",
"Status.AddFolderPrompt": "Add a game folder to populate the library.",
"Status.LibraryScanned": "Library scanned: {0} game(s) in {1} folder(s).",
"Status.CouldNotOpenFolder": "Could not open folder: {0}",
"Status.CopiedToClipboard": "{0} copied to clipboard.",
"Status.RemovedFromLibrary": "Removed “{0}” from the library. Re-add its folder to restore it.",
"Status.Running": "Running {0}",
"Status.Stopping": "Stopping…",
"Status.Idle": "Idle",
"Clipboard.Path": "Path",
"Clipboard.TitleId": "Title ID",
"Discord.Playing": "Playing {0}",
"Discord.Browsing": "Browsing the library",
"Dialog.ChooseGameFolder": "Choose a folder containing games",
"Dialog.OpenExecutable": "Open an executable to launch",
"Dialog.PsExecutables": "PS executables",
"Dialog.SaveLogFile": "Select where to save the Log file",
"Dialog.PlainTextFiles": "Plain Text Files",
"Dialog.LogFiles": "Log Files"
}
-134
View File
@@ -1,134 +0,0 @@
{
"_languageName": "Italiano",
"Page.Library": "Libreria",
"Page.Options": "Opzioni",
"Page.GameCount.One": "1 gioco",
"Page.GameCount.Other": "{0} giochi",
"Library.SearchWatermark": "Cerca nella libreria…",
"Library.AddFolder": " Aggiungi cartella",
"Library.Rescan": "⟳ Riscansiona",
"Library.OpenFile": "Apri file…",
"Library.Context.Launch": "Avvia",
"Library.Context.OpenFolder": "Apri cartella gioco",
"Library.Context.CopyPath": "Copia percorso",
"Library.Context.CopyTitleId": "Copia ID titolo",
"Library.Context.Remove": "Rimuovi dalla libreria",
"Library.Empty.Title": "La tua libreria è vuota",
"Library.Empty.Hint": "Aggiungi la cartella che contiene i tuoi giochi per partire.",
"Library.Empty.SearchTitle": "Nessun gioco corrisponde alla ricerca",
"Library.Empty.SearchHint": "Nulla nella libreria corrisponde a “{0}”.",
"Library.Empty.AddFolder": " Aggiungi cartella giochi",
"Library.Loading": "Caricamento libreria…",
"Options.General": "Generale",
"Options.Section.Emulation": "EMULAZIONE",
"Options.Section.Logging": "LOG",
"Options.Section.Launcher": "LAUNCHER",
"Options.CpuEngine.Label": "CPU engine",
"Options.CpuEngine.Desc": "Motore di esecuzione utilizzato per eseguire il codice del gioco.",
"Options.CpuEngine.Native": "Nativo",
"Options.Strict.Label": "Risoluzione rigorosa dynlib",
"Options.Strict.Desc": "Interrompi l'avvio quando un simbolo importato non può essere trovato.",
"Options.LogLevel.Label": "Livello Log",
"Options.LogLevel.Desc": "Livello di dettaglio dell'output console dell'emulatore.",
"Options.LogLevel.Trace": "Trace",
"Options.LogLevel.Debug": "Debug",
"Options.LogLevel.Info": "Info",
"Options.LogLevel.Warning": "Warning",
"Options.LogLevel.Error": "Error",
"Options.LogLevel.Critical": "Critical",
"Options.TraceImports.Label": "Limite tracciamento import",
"Options.TraceImports.Desc": "Traccia i primi N import per modulo (0 = disattivato).",
"Options.LogToFile.Label": "Salva log su file",
"Options.LogToFile.Desc": "Duplica l'output dell'emulatore in un file di log.",
"Options.LogFilePath.Label": "Percorso file di log",
"Options.LogFilePath.Default": "Nessun percorso personalizzato — i log vengono salvati in user/logs accanto all'emulatore.",
"Options.LogFilePath.Select": "Seleziona…",
"Options.OverrideLogFile.Label": "Sovrascrivi file di log",
"Options.OverrideLogFile.Desc": "Usa esattamente il percorso specificato invece di aggiungere Title ID e timestamp.",
"Options.TitleMusic.Label": "Musica del titolo",
"Options.TitleMusic.Desc": "Riproduci in loop la musica di anteprima del gioco selezionato nella libreria.",
"Options.Discord.Label": "Presenza Discord",
"Options.Discord.Desc": "Mostra il gioco in esecuzione sul tuo profilo Discord.",
"Options.Language.Label": "Lingua dell'emulatore",
"Options.Language.Desc": "Lingua utilizzata in tutto il launcher. Viene applicata immediatamente.",
"Common.On": "On",
"Common.Off": "Off",
"Console.Title": "CONSOLE",
"Console.SearchWatermark": "Cerca...",
"Console.AutoScroll": "Scorrimento automatico",
"Console.Split": "Dividi",
"Console.Copy": "Copia",
"Console.Clear": "Cancella",
"Console.WindowTitle": "Console SharpEmu",
"Launch.NoGameSelected": "Nessun gioco selezionato",
"Launch.NoGameHint": "Scegli un gioco dalla libreria, oppure apri direttamente un eboot.bin.",
"Launch.Idle": "Inattivo",
"Launch.Console": "≡ Console",
"Launch.Launch": "▶ Avvia",
"Launch.Stop": "■ Ferma",
"Launch.Running": "In esecuzione — {0}",
"Launch.Stopping": "Arresto in corso…",
"Launch.Exited": "Terminato con codice {0} ({1})",
"Launch.ExeNotFound": "Eseguibile SharpEmu non trovato. Compila prima il progetto SharpEmu.CLI (dotnet build).",
"Launch.LogFile": "File di log: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Impossibile avviare l'emulatore: {0}",
"Launch.ProcessExited": "Processo terminato con codice {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "argomenti non validi",
"Exit.EbootNotFound": "eboot non trovato",
"Exit.RuntimeException": "errore di runtime",
"Exit.EmulationError": "errore di emulazione",
"Exit.Unknown": "sconosciuto",
"Status.EmulatorLocating": "Emulatore: ricerca in corso…",
"Status.EmulatorPath": "Emulatore: {0}",
"Status.EmulatorNotFound": "Emulatore: eseguibile SharpEmu non trovato — compila prima SharpEmu.CLI.",
"Status.ScanningLibrary": "Scansione della libreria…",
"Status.AddFolderPrompt": "Aggiungi una cartella di giochi per popolare la libreria.",
"Status.LibraryScanned": "Libreria scansionata: {0} gioco/giochi in {1} cartella/e.",
"Status.CouldNotOpenFolder": "Impossibile aprire la cartella: {0}",
"Status.CopiedToClipboard": "{0} copiato negli appunti.",
"Status.RemovedFromLibrary": "“{0}” rimosso dalla libreria. Riaggiungi la sua cartella per ripristinarlo.",
"Status.Running": "In esecuzione: {0}",
"Status.Stopping": "Arresto in corso…",
"Status.Idle": "Inattivo",
"Clipboard.Path": "Percorso",
"Clipboard.TitleId": "ID Titolo",
"Discord.Playing": "Sta giocando a {0}",
"Discord.Browsing": "Sta esplorando la libreria",
"Dialog.ChooseGameFolder": "Scegli una cartella contenente giochi",
"Dialog.OpenExecutable": "Apri un eseguibile da avviare",
"Dialog.PsExecutables": "Eseguibili PS",
"Dialog.SaveLogFile": "Scegli dove salvare il file di log",
"Dialog.PlainTextFiles": "File di testo semplice",
"Dialog.LogFiles": "File di log"
}
-129
View File
@@ -1,129 +0,0 @@
{
"_languageName": "Русский",
"Page.Library": "Библиотека",
"Page.Options": "Настройки",
"Page.GameCount.One": "1 игра",
"Page.GameCount.Other": "Игр: {0}",
"Library.SearchWatermark": "Поиск…",
"Library.AddFolder": "+ Добавить папку",
"Library.Rescan": "⟳ Сканировать",
"Library.OpenFile": "Открыть файл…",
"Library.Context.Launch": "Запустить",
"Library.Context.OpenFolder": "Открыть папку с игрой",
"Library.Context.CopyPath": "Скопировать путь",
"Library.Context.CopyTitleId": "Скопировать ID игры",
"Library.Context.Remove": "Удалить из библиотеки",
"Library.Empty.Title": "Ваша библиотека пуста",
"Library.Empty.Hint": "Добавьте папку с играми, чтобы начать.",
"Library.Empty.SearchTitle": "По вашему запросу ничего не найдено",
"Library.Empty.SearchHint": "В библиотеке нет игр, соответствующих запросу «{0}».",
"Library.Empty.AddFolder": "+ Добавить папку с играми",
"Library.Loading": "Загрузка библиотеки…",
"Options.General": "Основные",
"Options.Section.Emulation": "ЭМУЛЯЦИЯ",
"Options.Section.Logging": "ЛОГГИРОВАНИЕ",
"Options.Section.Launcher": "ЛАУНЧЕР",
"Options.CpuEngine.Label": "Движок ЦП",
"Options.CpuEngine.Desc": "Движок выполнения, используемый для запуска игрового кода.",
"Options.CpuEngine.Native": "Нативный",
"Options.Strict.Label": "Строгое разрешение динамических библиотек",
"Options.Strict.Desc": "Не запускать игру, если не удается найти или связать импортируемую функцию/переменную.",
"Options.LogLevel.Label": "Уровень логгирования",
"Options.LogLevel.Desc": "Подробность вывода в консоль эмулятора.",
"Options.LogLevel.Trace": "Trace",
"Options.LogLevel.Debug": "Debug",
"Options.LogLevel.Info": "Info",
"Options.LogLevel.Warning": "Warning",
"Options.LogLevel.Error": "Error",
"Options.LogLevel.Critical": "Critical",
"Options.TraceImports.Label": "Лимит трассировки импортов",
"Options.TraceImports.Desc": "Трассировать первые N импортов в каждом модуле (0 - выключено).",
"Options.LogToFile.Label": "Запись лога в файл",
"Options.LogToFile.Desc": "Дублировать вывод эмулятора в файл лога.",
"Options.LogFilePath.Label": "Путь к файлу лога",
"Options.LogFilePath.Default": "Пользовательский путь не задан: логи сохраняются в user/logs рядом с эмулятором.",
"Options.LogFilePath.Select": "Выбрать…",
"Options.OverrideLogFile.Label": "Переопределить файл лога",
"Options.OverrideLogFile.Desc": "Использовать указанный путь к файлу без добавления ID игры и метки времени.",
"Options.TitleMusic.Label": "Музыка игры",
"Options.TitleMusic.Desc": "Зацикленно воспроизводить музыку предпросмотра выбранной игры в библиотеке.",
"Options.Discord.Label": "Статус Discord",
"Options.Discord.Desc": "Показывать запущенную игру в профиле Discord.",
"Options.Language.Label": "Язык эмулятора",
"Options.Language.Desc": "Язык интерфейса лаунчера. Изменение применяется сразу.",
"Common.On": "Включено",
"Common.Off": "Выключено",
"Console.Title": "КОНСОЛЬ",
"Console.SearchWatermark": "Поиск...",
"Console.AutoScroll": "Авто-прокрутка",
"Console.Split": "Разделить",
"Console.Copy": "Скопировать",
"Console.Clear": "Очистить",
"Console.WindowTitle": "Консоль SharpEmu",
"Launch.NoGameSelected": "Ничего не выбрано",
"Launch.NoGameHint": "Выберите игру из библиотеки или откройте eboot.bin напрямую.",
"Launch.Idle": "Ожидание",
"Launch.Console": "≡ Консоль",
"Launch.Launch": "▶ Запустить",
"Launch.Stop": "■ Остановить",
"Launch.Running": "Запущено - {0}",
"Launch.Stopping": "Остановка…",
"Launch.Exited": "Завершено с кодом {0} ({1})",
"Launch.ExeNotFound": "Исполняемый файл SharpEmu не найден. Скомпилируйте сначала проект SharpEmu.CLI (dotnet build).",
"Launch.LogFile": "Лог: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Не удалось запустить эмулятор: {0}",
"Launch.ProcessExited": "Процесс завершился с кодом {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "некорректные аргументы",
"Exit.EbootNotFound": "eboot не найден",
"Exit.RuntimeException": "ошибка выполнения",
"Exit.EmulationError": "ошибка эмуляции",
"Exit.Unknown": "неизвестная ошибка",
"Status.EmulatorLocating": "Эмулятор: поиск…",
"Status.EmulatorPath": "Эмулятор: {0}",
"Status.EmulatorNotFound": "Эмулятор: исполняемый файл SharpEmu не был найден - скомпилируйте сначала SharpEmu.CLI.",
"Status.ScanningLibrary": "Сканирование библиотеки…",
"Status.AddFolderPrompt": "Добавьте папку с играми, чтобы заполнить библиотеку.",
"Status.LibraryScanned": "Сканирование библиотеки завершено: игр: {0}, папок: {1}.",
"Status.CouldNotOpenFolder": "Не удалось открыть папку: {0}",
"Status.CopiedToClipboard": "{0}: скопировано в буфер обмена.",
"Status.RemovedFromLibrary": "Игра «{0}» удалена из библиотеки. Добавьте её папку заново, чтобы вернуть.",
"Status.Running": "Запущено: {0}",
"Status.Stopping": "Остановка…",
"Status.Idle": "Ожидание",
"Clipboard.Path": "Путь",
"Clipboard.TitleId": "ID игры",
"Discord.Playing": "Играет в {0}",
"Discord.Browsing": "Просматривает библиотеку",
"Dialog.ChooseGameFolder": "Выберите папку, содержащую игры",
"Dialog.OpenExecutable": "Открыть исполняемый файл для запуска",
"Dialog.PsExecutables": "Исполняемые файлы PS",
"Dialog.SaveLogFile": "Выберите, куда сохранить файл с логами",
"Dialog.PlainTextFiles": "Текстовые файлы",
"Dialog.LogFiles": "Логи"
}
-129
View File
@@ -1,129 +0,0 @@
{
"_languageName": "Türkçe",
"Page.Library": "Kütüphane",
"Page.Options": "Ayarlar",
"Page.GameCount.One": "1 oyun",
"Page.GameCount.Other": "{0} oyun",
"Library.SearchWatermark": "Kütüphanede ara…",
"Library.AddFolder": " Klasör ekle",
"Library.Rescan": "⟳ Yeniden tara",
"Library.OpenFile": "Dosya aç…",
"Library.Context.Launch": "Başlat",
"Library.Context.OpenFolder": "Oyun klasörünü aç",
"Library.Context.CopyPath": "Yolu kopyala",
"Library.Context.CopyTitleId": "Title ID'yi kopyala",
"Library.Context.Remove": "Kütüphaneden kaldır",
"Library.Empty.Title": "Kütüphaneniz boş",
"Library.Empty.Hint": "Başlamak için oyunlarınızı içeren bir klasör ekleyin.",
"Library.Empty.SearchTitle": "Aramanızla eşleşen oyun yok",
"Library.Empty.SearchHint": "Kütüphanede “{0}” ile eşleşen bir şey yok.",
"Library.Empty.AddFolder": " Oyun klasörü ekle",
"Library.Loading": "Kütüphane yükleniyor…",
"Options.General": "Genel",
"Options.Section.Emulation": "EMÜLASYON",
"Options.Section.Logging": "GÜNLÜKLEME",
"Options.Section.Launcher": "BAŞLATICI",
"Options.CpuEngine.Label": "CPU motoru",
"Options.CpuEngine.Desc": "Oyun kodunu çalıştırmak için kullanılan yürütme motoru.",
"Options.CpuEngine.Native": "Native",
"Options.Strict.Label": "Katı dynlib çözümü",
"Options.Strict.Desc": "İçe aktarılan bir sembol çözümlenemediğinde başlatmayı başarısız kılar.",
"Options.LogLevel.Label": "Günlük seviyesi",
"Options.LogLevel.Desc": "Emülatör konsol çıktısının ayrıntı düzeyi.",
"Options.LogLevel.Trace": "Trace",
"Options.LogLevel.Debug": "Debug",
"Options.LogLevel.Info": "Info",
"Options.LogLevel.Warning": "Warning",
"Options.LogLevel.Error": "Error",
"Options.LogLevel.Critical": "Critical",
"Options.TraceImports.Label": "Import izleme sınırı",
"Options.TraceImports.Desc": "Modül başına ilk N import'u izle (0 = kapalı).",
"Options.LogToFile.Label": "Dosyaya günlükle",
"Options.LogToFile.Desc": "Emülatör çıktısını bir günlük dosyasına yansıt.",
"Options.LogFilePath.Label": "Günlük dosyası yolu",
"Options.LogFilePath.Default": "Özel yol yok — günlükler emülatörün yanındaki user/logs klasörüne yazılır.",
"Options.LogFilePath.Select": "Seç…",
"Options.OverrideLogFile.Label": "Günlük dosyasının üzerine yaz",
"Options.OverrideLogFile.Desc": "Title ID ve zaman damgası eklemek yerine tam dosya yolunu kullan.",
"Options.TitleMusic.Label": "Oyun müziği",
"Options.TitleMusic.Desc": "Kütüphanede seçili oyunun önizleme müziğini döngüye al.",
"Options.Discord.Label": "Discord durumu",
"Options.Discord.Desc": "Discord profilinde çalışan oyunu göster.",
"Options.Language.Label": "Emülatör dili",
"Options.Language.Desc": "Başlatıcı genelinde kullanılan dil. Hemen uygulanır.",
"Common.On": "Açık",
"Common.Off": "Kapalı",
"Console.Title": "KONSOL",
"Console.SearchWatermark": "Ara...",
"Console.AutoScroll": "Otomatik kaydır",
"Console.Split": "Ayır",
"Console.Copy": "Kopyala",
"Console.Clear": "Temizle",
"Console.WindowTitle": "SharpEmu Konsol",
"Launch.NoGameSelected": "Oyun seçilmedi",
"Launch.NoGameHint": "Kütüphaneden bir oyun seçin veya doğrudan bir eboot.bin açın.",
"Launch.Idle": "Boşta",
"Launch.Console": "≡ Konsol",
"Launch.Launch": "▶ Başlat",
"Launch.Stop": "■ Durdur",
"Launch.Running": "Çalışıyor — {0}",
"Launch.Stopping": "Durduruluyor…",
"Launch.Exited": "Çıkış kodu {0} ({1})",
"Launch.ExeNotFound": "SharpEmu çalıştırılabilir dosyası bulunamadı. Önce SharpEmu.CLI projesini derleyin (dotnet build).",
"Launch.LogFile": "Günlük dosyası: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Emülatör başlatılamadı: {0}",
"Launch.ProcessExited": "İşlem {0} kodla sonlandı ({1}).",
"Exit.Ok": "Tamam",
"Exit.InvalidArguments": "geçersiz argümanlar",
"Exit.EbootNotFound": "eboot bulunamadı",
"Exit.RuntimeException": "çalışma zamanı hatası",
"Exit.EmulationError": "emülasyon hatası",
"Exit.Unknown": "bilinmiyor",
"Status.EmulatorLocating": "Emülatör: bulunuyor…",
"Status.EmulatorPath": "Emülatör: {0}",
"Status.EmulatorNotFound": "Emülatör: SharpEmu çalıştırılabilir dosyası bulunamadı — önce SharpEmu.CLI'yi derleyin.",
"Status.ScanningLibrary": "Kütüphane taranıyor…",
"Status.AddFolderPrompt": "Kütüphaneyi doldurmak için bir oyun klasörü ekleyin.",
"Status.LibraryScanned": "Kütüphane tarandı: {1} klasörde {0} oyun.",
"Status.CouldNotOpenFolder": "Klasör açılamadı: {0}",
"Status.CopiedToClipboard": "{0} panoya kopyalandı.",
"Status.RemovedFromLibrary": "“{0}” kütüphaneden kaldırıldı. Geri getirmek için klasörünü yeniden ekleyin.",
"Status.Running": "{0} çalışıyor",
"Status.Stopping": "Durduruluyor…",
"Status.Idle": "Boşta",
"Clipboard.Path": "Yol",
"Clipboard.TitleId": "Title ID",
"Discord.Playing": "{0} oynuyor",
"Discord.Browsing": "Kütüphaneye göz atıyor",
"Dialog.ChooseGameFolder": "Oyunlarınızı içeren bir klasör seçin",
"Dialog.OpenExecutable": "Başlatılacak bir çalıştırılabilir dosya aç",
"Dialog.PsExecutables": "PS çalıştırılabilirleri",
"Dialog.SaveLogFile": "Günlük dosyasının kaydedileceği yeri seçin",
"Dialog.PlainTextFiles": "Düz Metin Dosyaları",
"Dialog.LogFiles": "Günlük Dosyaları"
}
-164
View File
@@ -1,164 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Text.Json;
namespace SharpEmu.GUI;
/// <summary>
/// Loads UI strings for the launcher. Every language ships embedded in the
/// assembly (see SharpEmu.GUI.csproj) so a release build is fully
/// self-contained; an optional Languages/&lt;code&gt;.json file next to the
/// executable overrides the embedded copy for that code, so a translation
/// fix or a brand-new language never needs a rebuild.
/// </summary>
public sealed class Localization
{
public static Localization Instance { get; } = new();
public sealed record LanguageInfo(string Code, string NativeName);
private const string EmbeddedResourcePrefix = "Languages.";
private const string EmbeddedResourceSuffix = ".json";
private Dictionary<string, string> _strings = new();
private Localization()
{
}
/// <summary>Directory holding optional *.json language overrides, next to the executable.</summary>
public static string LanguagesDirectory => Path.Combine(AppContext.BaseDirectory, "Languages");
public string CurrentCode { get; private set; } = "en";
public string Get(string key) => _strings.TryGetValue(key, out var value) ? value : key;
public string Format(string key, params object?[] args) => string.Format(Get(key), args);
/// <summary>
/// Languages available either embedded in the binary or as a loose
/// override file, sorted by code. A loose file's declared name wins when
/// the same code exists in both places.
/// </summary>
public List<LanguageInfo> DiscoverLanguages()
{
var languages = new Dictionary<string, LanguageInfo>(StringComparer.OrdinalIgnoreCase);
foreach (var code in EmbeddedLanguageCodes())
{
using var stream = OpenEmbeddedLanguageStream(code);
if (stream is not null)
{
languages[code] = new LanguageInfo(code, ReadLanguageName(stream) ?? code);
}
}
try
{
foreach (var file in Directory.EnumerateFiles(LanguagesDirectory, "*.json"))
{
var code = Path.GetFileNameWithoutExtension(file);
using var stream = File.OpenRead(file);
languages[code] = new LanguageInfo(code, ReadLanguageName(stream) ?? code);
}
}
catch (Exception)
{
// No loose Languages directory: the embedded languages still stand.
}
var result = languages.Values.ToList();
result.Sort((a, b) => string.CompareOrdinal(a.Code, b.Code));
return result;
}
/// <summary>Loads a language by code (e.g. "en"): a loose override file first, then the embedded copy.</summary>
public void Load(string code)
{
if (!TryLoadLooseFile(code) && !TryLoadEmbedded(code) &&
!string.Equals(code, "en", StringComparison.OrdinalIgnoreCase))
{
_ = TryLoadLooseFile("en") || TryLoadEmbedded("en");
}
}
private static IEnumerable<string> EmbeddedLanguageCodes()
{
foreach (var name in typeof(Localization).Assembly.GetManifestResourceNames())
{
if (name.StartsWith(EmbeddedResourcePrefix, StringComparison.Ordinal) &&
name.EndsWith(EmbeddedResourceSuffix, StringComparison.Ordinal))
{
yield return name[EmbeddedResourcePrefix.Length..^EmbeddedResourceSuffix.Length];
}
}
}
private static Stream? OpenEmbeddedLanguageStream(string code) =>
typeof(Localization).Assembly.GetManifestResourceStream($"{EmbeddedResourcePrefix}{code}{EmbeddedResourceSuffix}");
private static string? ReadLanguageName(Stream stream)
{
try
{
using var document = JsonDocument.Parse(stream);
if (document.RootElement.TryGetProperty("_languageName", out var name) &&
name.ValueKind == JsonValueKind.String)
{
return name.GetString();
}
}
catch (Exception)
{
// Malformed file: fall back to the code as its own display name.
}
return null;
}
private bool TryLoadLooseFile(string code)
{
try
{
var path = Path.Combine(LanguagesDirectory, $"{code}.json");
return File.Exists(path) && TryLoad(code, File.ReadAllText(path));
}
catch (Exception)
{
return false;
}
}
private bool TryLoadEmbedded(string code)
{
try
{
using var stream = OpenEmbeddedLanguageStream(code);
if (stream is null)
{
return false;
}
using var reader = new StreamReader(stream);
return TryLoad(code, reader.ReadToEnd());
}
catch (Exception)
{
return false;
}
}
private bool TryLoad(string code, string json)
{
var loaded = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
if (loaded is null)
{
return false;
}
_strings = loaded;
CurrentCode = code;
return true;
}
}
+103 -378
View File
@@ -7,47 +7,21 @@ SPDX-License-Identifier: GPL-2.0-or-later
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="SharpEmu.GUI.MainWindow"
Title="SharpEmu"
Width="1280" Height="820"
MinWidth="980" MinHeight="640"
WindowState="Maximized"
Width="1280" Height="800"
MinWidth="980" MinHeight="620"
WindowStartupLocation="CenterScreen"
Background="{StaticResource BgBrush}"
ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaChromeHints="PreferSystemChrome"
ExtendClientAreaTitleBarHeightHint="44"
Icon="avares://SharpEmu.GUI/Assets/SharpEmu.ico"
KeyDown="OnKeyDown">
Icon="avares://SharpEmu.GUI/Assets/SharpEmu.ico">
<Grid RowDefinitions="Auto,*,Auto">
<Grid RowDefinitions="44,*,32">
<!-- Selected-game backdrop: key art behind the main content, dimmed by
a scrim so tiles and text stay readable. Fades on selection. -->
<Panel Grid.Row="1" ClipToBounds="True">
<Image x:Name="BackdropImage" Stretch="UniformToFill" Opacity="0"
HorizontalAlignment="Center" VerticalAlignment="Center">
<Image.Transitions>
<Transitions>
<DoubleTransition Property="Opacity" Duration="0:0:0.35" />
</Transitions>
</Image.Transitions>
</Image>
<Border>
<Border.Background>
<LinearGradientBrush StartPoint="0%,0%" EndPoint="0%,100%">
<GradientStop Offset="0" Color="#730D1017" />
<GradientStop Offset="0.55" Color="#BF0D1017" />
<GradientStop Offset="1" Color="#EB0D1017" />
</LinearGradientBrush>
</Border.Background>
</Border>
</Panel>
<!-- Title bar; hidden in fullscreen (F11) along with the status bar, so
the game gets the whole screen. -->
<Grid x:Name="TitleBar" Grid.Row="0" Height="44" Background="{StaticResource ChromeBrush}">
<!-- Title bar -->
<Grid x:Name="TitleBar" Grid.Row="0" Background="{StaticResource ChromeBrush}">
<StackPanel Orientation="Horizontal" Spacing="10" Margin="16,0" VerticalAlignment="Center">
<Image Source="avares://SharpEmu.GUI/Assets/SharpEmu.ico" Width="20" Height="20"
RenderOptions.BitmapInterpolationMode="HighQuality" />
<Image Source="avares://SharpEmu.GUI/Assets/logo.png" Width="20" Height="20" />
<TextBlock Text="SharpEmu" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center" />
<Border Classes="pill">
<TextBlock x:Name="VersionText" Text="v0.0.1" FontSize="11" Foreground="{StaticResource MutedBrush}" />
@@ -56,372 +30,123 @@ SPDX-License-Identifier: GPL-2.0-or-later
</Grid>
<!-- Main content -->
<Grid Grid.Row="1" Margin="32,24,32,20" RowDefinitions="Auto,*,Auto,Auto">
<Grid Grid.Row="1" Margin="14" ColumnDefinitions="360,14,*">
<!-- Library / Options page switcher, with the library toolbar sharing
the same row on the right. Plain buttons (not TabItem) so there is
no underline; LB/RB hint chips flank the pair and the gamepad's
shoulder buttons actually switch pages from anywhere. -->
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto" Margin="0,0,0,20">
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="14" VerticalAlignment="Center">
<Border Classes="padHint" VerticalAlignment="Center">
<TextBlock Text="LB" FontSize="11" FontWeight="Bold" Foreground="{StaticResource MutedBrush}" />
</Border>
<Button x:Name="LibraryTabButton" Classes="segment active" Content="Library" />
<Button x:Name="OptionsTabButton" Classes="segment" Content="Options" />
<Border Classes="padHint" VerticalAlignment="Center">
<TextBlock Text="RB" FontSize="11" FontWeight="Bold" Foreground="{StaticResource MutedBrush}" />
</Border>
</StackPanel>
<StackPanel Grid.Column="2" x:Name="LibraryToolbar" Orientation="Horizontal" Spacing="8"
VerticalAlignment="Center">
<TextBox x:Name="SearchBox" Watermark="Search library…" Width="240" VerticalAlignment="Center" />
<Button x:Name="AddFolderButton" Classes="ghost" Content=" Add folder" VerticalAlignment="Center" />
<Button x:Name="RescanButton" Classes="ghost" Content="⟳ Rescan" VerticalAlignment="Center" />
<Button x:Name="OpenFileButton" Classes="ghost" Content="Open file…" VerticalAlignment="Center" />
</StackPanel>
</Grid>
<Panel Grid.Row="1">
<!-- Library page. The tile row gets extra top margin so it sits
closer to eye level (PS5 home-screen style) instead of hugging
the Library/Options switcher above it. -->
<Panel x:Name="LibraryPage" Margin="0,64,0,0">
<ListBox x:Name="GameList" Classes="tileGrid" Background="Transparent"
SelectionMode="Single" Padding="0">
<ListBox.ContextMenu>
<ContextMenu x:Name="GameContextMenu" Placement="Pointer">
<MenuItem x:Name="CtxLaunch" Header="Launch" FontWeight="SemiBold">
<MenuItem.Icon>
<TextBlock Text="▶" FontSize="11" Foreground="{StaticResource AccentHoverBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
<MenuItem x:Name="CtxOpenFolder" Header="Open game folder">
<MenuItem.Icon>
<TextBlock Text="📂" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem x:Name="CtxCopyPath" Header="Copy path">
<MenuItem.Icon>
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
<MenuItem x:Name="CtxCopyTitleId" Header="Copy title ID">
<MenuItem.Icon>
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem x:Name="CtxRemove" Header="Remove from library"
Foreground="{StaticResource DangerHoverBrush}">
<MenuItem.Icon>
<TextBlock Text="✕" FontSize="12" Foreground="{StaticResource DangerHoverBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
</ContextMenu>
</ListBox.ContextMenu>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Width="128" Height="172" Spacing="7">
<Border Classes="coverShadow" Width="128" Height="128">
<Border Classes="coverClip">
<Panel>
<Border Background="{Binding PlaceholderBrush}" IsVisible="{Binding !HasCover}">
<TextBlock Text="{Binding Initials}" FontSize="36" FontWeight="Bold"
Foreground="#E8ECF4" Opacity="0.85"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<Image Source="{Binding Cover}" Stretch="UniformToFill" IsVisible="{Binding HasCover}" />
</Panel>
</Border>
</Border>
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="SemiBold"
TextWrapping="Wrap" MaxLines="2" TextTrimming="CharacterEllipsis"
TextAlignment="Center" HorizontalAlignment="Center" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<!-- Empty state -->
<StackPanel x:Name="EmptyState" Spacing="10" HorizontalAlignment="Center" VerticalAlignment="Center"
IsVisible="False">
<TextBlock Text="🎮" FontSize="44" HorizontalAlignment="Center" Opacity="0.7" />
<TextBlock x:Name="EmptyStateTitle" Text="Your library is empty" FontSize="18" FontWeight="SemiBold"
HorizontalAlignment="Center" />
<TextBlock x:Name="EmptyStateHint" Text="Add a folder containing your games to get started."
FontSize="13" Foreground="{StaticResource MutedBrush}" HorizontalAlignment="Center" />
<Button x:Name="EmptyAddFolderButton" Classes="accent" Content=" Add game folder"
HorizontalAlignment="Center" Margin="0,8,0,0" />
</StackPanel>
<!-- Loading state: covers the grid while a scan (initial load,
rescan, or a just-added folder) is in flight, so the screen
never looks blank mid-scan. -->
<StackPanel x:Name="LoadingState" Spacing="14" HorizontalAlignment="Center" VerticalAlignment="Center"
IsVisible="False">
<ProgressBar IsIndeterminate="True" Width="180" Height="3" />
<TextBlock x:Name="LoadingStateText" Text="Loading library…" FontSize="13"
Foreground="{StaticResource MutedBrush}" HorizontalAlignment="Center" />
</StackPanel>
</Panel>
<!-- Options page: sub-tabs so future categories (Graphics, …) slot
in next to General. Card-grouped sections match the visual
language used by the console panel and launch bar below. -->
<Grid x:Name="OptionsPage" IsVisible="False">
<TabControl>
<TabItem x:Name="GeneralTabItem" Header="General" FontSize="15">
<ScrollViewer>
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
<Border Classes="card">
<StackPanel Spacing="14">
<TextBlock x:Name="EmulationSectionTitle" Classes="sectionTitle" Text="EMULATION" />
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="CpuEngineLabel" Text="CPU engine" FontSize="13" />
<TextBlock x:Name="CpuEngineDesc" Text="Execution engine used to run game code."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ComboBox Grid.Column="1" x:Name="CpuEngineBox" Width="160" SelectedIndex="0"
VerticalAlignment="Center" CornerRadius="8">
<ComboBoxItem x:Name="CpuEngineNativeItem" Content="Native" />
</ComboBox>
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="StrictLabel" Text="Strict dynlib resolution" FontSize="13" />
<TextBlock x:Name="StrictDesc" Text="Fail the launch when an imported symbol cannot be resolved."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="StrictToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
</StackPanel>
</Border>
<Border Classes="card">
<StackPanel Spacing="14">
<TextBlock x:Name="LoggingSectionTitle" Classes="sectionTitle" Text="LOGGING" />
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="LogLevelLabel" Text="Log level" FontSize="13" />
<TextBlock x:Name="LogLevelDesc" Text="Verbosity of the emulator console output."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ComboBox Grid.Column="1" x:Name="LogLevelBox" Width="160" SelectedIndex="2"
VerticalAlignment="Center" CornerRadius="8">
<ComboBoxItem x:Name="LogLevelTraceItem" Content="Trace" />
<ComboBoxItem x:Name="LogLevelDebugItem" Content="Debug" />
<ComboBoxItem x:Name="LogLevelInfoItem" Content="Info" />
<ComboBoxItem x:Name="LogLevelWarningItem" Content="Warning" />
<ComboBoxItem x:Name="LogLevelErrorItem" Content="Error" />
<ComboBoxItem x:Name="LogLevelCriticalItem" Content="Critical" />
</ComboBox>
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="TraceImportsLabel" Text="Import trace limit" FontSize="13" />
<TextBlock x:Name="TraceImportsDesc" Text="Trace the first N imports per module (0 = off)."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<NumericUpDown Grid.Column="1" x:Name="TraceImportsBox" Width="160" Minimum="0"
Maximum="4096" Increment="16" Value="0" FormatString="0"
VerticalAlignment="Center" CornerRadius="8" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="LogToFileLabel" Text="Log to file" FontSize="13" />
<TextBlock x:Name="LogToFileDesc" Text="Mirror emulator output to a log file."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="LogToFileToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="LogFilePathLabel" Text="Log file path" FontSize="13" />
<TextBlock x:Name="LogFilePathText" Text="No custom path"
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<Button Grid.Column="1" x:Name="SelectLogFilePathButton" Classes="ghost" Content="Select…"
VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="OverrideLogFileLabel" Text="Override log file" FontSize="13" />
<TextBlock x:Name="OverrideLogFileDesc"
Text="Use the exact file path instead of appending title ID and timestamp."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="OverrideLogFileToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
</StackPanel>
</Border>
<Border Classes="card">
<StackPanel Spacing="14">
<TextBlock x:Name="LauncherSectionTitle" Classes="sectionTitle" Text="LAUNCHER" />
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="LanguageLabel" Text="Emulator language" FontSize="13" />
<TextBlock x:Name="LanguageDesc"
Text="Language used throughout the launcher. Applies immediately."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ComboBox Grid.Column="1" x:Name="LanguageBox" Width="160"
VerticalAlignment="Center" CornerRadius="8"
DisplayMemberBinding="{Binding NativeName}" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="TitleMusicLabel" Text="Title music" FontSize="13" />
<TextBlock x:Name="TitleMusicDesc" Text="Loop the selected game's preview music in the library."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="TitleMusicToggle" OnContent="On" OffContent="Off"
IsChecked="True" VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="DiscordLabel" Text="Discord presence" FontSize="13" />
<TextBlock x:Name="DiscordDesc" Text="Show the running game on your Discord profile."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="DiscordToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</TabItem>
</TabControl>
</Grid>
</Panel>
<!-- Console (collapsible) -->
<Border Grid.Row="2" x:Name="ConsolePanel" Classes="card" Padding="0" Height="240"
Margin="0,12,0,0" IsVisible="False">
<Grid RowDefinitions="Auto,*">
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto,Auto,Auto" Margin="16,12,16,8">
<TextBlock x:Name="ConsoleSectionTitle" Classes="sectionTitle" Text="CONSOLE" VerticalAlignment="Center" />
<TextBox Grid.Column="1" FontSize="12" Margin="0,0,12,0" x:Name="ConsoleSearchBox"
Watermark="Search..." Width="320" />
<CheckBox Grid.Column="2" x:Name="AutoScrollCheck" Content="Auto-scroll" IsChecked="True"
FontSize="12" Margin="0,0,12,0" />
<Button Grid.Column="3" x:Name="DetachConsoleButton" Classes="ghost" Content="Split" FontSize="12"
Padding="10,4" Margin="0,0,8,0" />
<Button Grid.Column="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" />
<!-- Game library -->
<Border Grid.Column="0" Classes="card" Padding="0">
<Grid RowDefinitions="Auto,Auto,*,Auto">
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="16,16,16,10">
<TextBlock Classes="sectionTitle" Text="GAME LIBRARY" VerticalAlignment="Center" />
<Border Grid.Column="1" Classes="pill">
<TextBlock x:Name="GameCountText" Text="0" FontSize="11" Foreground="{StaticResource MutedBrush}" />
</Border>
</Grid>
<ListBox Grid.Row="1" x:Name="ConsoleList" Classes="console" BorderThickness="0,1,0,0"
BorderBrush="{StaticResource CardBorderBrush}" CornerRadius="0,0,12,12">
<TextBox Grid.Row="1" x:Name="SearchBox" Watermark="Search games…" Margin="12,0,12,8" CornerRadius="8" />
<ListBox Grid.Row="2" x:Name="GameList" Classes="library" Background="Transparent" Margin="8,0">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Text}" Foreground="{Binding Brush}" TextWrapping="NoWrap" />
<StackPanel Margin="4,4" Spacing="2">
<TextBlock Text="{Binding Name}" FontSize="14" FontWeight="SemiBold" TextTrimming="CharacterEllipsis" />
<TextBlock Text="{Binding Detail}" FontSize="11" Foreground="{StaticResource MutedBrush}" TextTrimming="CharacterEllipsis" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel Grid.Row="3" Orientation="Horizontal" Spacing="8" Margin="16,12,16,16">
<Button x:Name="AddFolderButton" Classes="ghost" Content=" Add folder" />
<Button x:Name="RescanButton" Classes="ghost" Content="⟳ Rescan" />
<Button x:Name="OpenFileButton" Classes="ghost" Content="Open file…" />
</StackPanel>
</Grid>
</Border>
<!-- Launch bar; capped so it does not sprawl on a maximized window. -->
<Border Grid.Row="3" Classes="card" Margin="0,12,0,0" Padding="14" MaxWidth="1280">
<StackPanel Spacing="14">
<Grid ColumnDefinitions="Auto,*,Auto">
<!-- Selected game cover thumbnail -->
<Border Grid.Column="0" Classes="coverClip" Width="56" Height="56" CornerRadius="8"
VerticalAlignment="Center">
<Panel x:Name="SelectedCoverPanel">
<Border Background="{Binding PlaceholderBrush, FallbackValue={x:Null}}"
IsVisible="{Binding !HasCover, FallbackValue=False}">
<TextBlock Text="{Binding Initials}" FontSize="20" FontWeight="Bold"
Foreground="#E8ECF4" Opacity="0.85"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<Image Source="{Binding Cover}" Stretch="UniformToFill"
IsVisible="{Binding HasCover, FallbackValue=False}" />
</Panel>
</Border>
<StackPanel Grid.Column="1" Spacing="4" VerticalAlignment="Center" Margin="14,0,14,0">
<Grid ColumnDefinitions="Auto,Auto,*">
<TextBlock Grid.Column="0" x:Name="SelectedGameTitle" Text="No game selected" FontSize="16"
FontWeight="Bold" TextTrimming="CharacterEllipsis" VerticalAlignment="Center"
MaxWidth="340" Margin="0,0,10,0" />
<!-- Title id / version / size badges, right next to the
title. The title's own MaxWidth (not a "*" column) is
what keeps them from drifting to the far right. -->
<StackPanel Grid.Column="1" x:Name="SelectedBadgesRow" Orientation="Horizontal" Spacing="6"
IsVisible="False" VerticalAlignment="Center">
<Border Classes="pill" IsVisible="{Binding HasTitleId, FallbackValue=False}">
<TextBlock Text="{Binding TitleId}" FontSize="10" FontWeight="SemiBold"
Foreground="{StaticResource MutedBrush}" />
</Border>
<Border Classes="pill" IsVisible="{Binding HasVersion, FallbackValue=False}">
<TextBlock Text="{Binding VersionText}" FontSize="10" FontWeight="SemiBold"
Foreground="{StaticResource MutedBrush}" />
</Border>
<Border Classes="pill">
<TextBlock Text="{Binding SizeText, FallbackValue=''}" FontSize="10" FontWeight="SemiBold"
Foreground="{StaticResource MutedBrush}" />
</Border>
</StackPanel>
</Grid>
<!-- Right column -->
<Grid Grid.Column="2" RowDefinitions="Auto,12,Auto,12,*">
<!-- Selected game / launch -->
<Border Grid.Row="0" Classes="card">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" Spacing="6" VerticalAlignment="Center">
<TextBlock x:Name="SelectedGameTitle" Text="No game selected" FontSize="20" FontWeight="Bold" TextTrimming="CharacterEllipsis" />
<TextBlock x:Name="SelectedGamePath" Text="Pick a game from the library, or open an eboot.bin directly."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextTrimming="CharacterEllipsis" />
<StackPanel Orientation="Horizontal" Spacing="7">
<Ellipse x:Name="StatusDot" Width="8" Height="8" Fill="{StaticResource FaintBrush}"
VerticalAlignment="Center" />
<TextBlock x:Name="StatusText" Text="Idle" FontSize="11" Foreground="{StaticResource MutedBrush}"
VerticalAlignment="Center" />
FontSize="12" Foreground="{StaticResource MutedBrush}" TextTrimming="CharacterEllipsis" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Ellipse x:Name="StatusDot" Width="9" Height="9" Fill="{StaticResource FaintBrush}" VerticalAlignment="Center" />
<TextBlock x:Name="StatusText" Text="Idle" FontSize="12" Foreground="{StaticResource MutedBrush}" VerticalAlignment="Center" />
</StackPanel>
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
<ToggleButton x:Name="ConsoleToggle" Classes="ghost" Content="≡ Console" />
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10" VerticalAlignment="Center" Margin="16,0,0,0">
<Button x:Name="LaunchButton" Classes="accent" Content="▶ Launch" IsEnabled="False" />
<Button x:Name="StopButton" Classes="danger" Content="■ Stop" IsEnabled="False" />
</StackPanel>
</Grid>
</StackPanel>
</Border>
</Border>
<!-- Launch options -->
<Border Grid.Row="2" Classes="card">
<StackPanel Spacing="12">
<TextBlock Classes="sectionTitle" Text="LAUNCH OPTIONS" />
<WrapPanel>
<StackPanel Margin="0,0,24,0">
<TextBlock Classes="fieldLabel" Text="CPU engine" />
<ComboBox x:Name="CpuEngineBox" Width="150" SelectedIndex="0" CornerRadius="8">
<ComboBoxItem Content="Native" />
</ComboBox>
</StackPanel>
<StackPanel Margin="0,0,24,0">
<TextBlock Classes="fieldLabel" Text="Log level" />
<ComboBox x:Name="LogLevelBox" Width="150" SelectedIndex="2" CornerRadius="8">
<ComboBoxItem Content="Trace" />
<ComboBoxItem Content="Debug" />
<ComboBoxItem Content="Info" />
<ComboBoxItem Content="Warning" />
<ComboBoxItem Content="Error" />
<ComboBoxItem Content="Critical" />
</ComboBox>
</StackPanel>
<StackPanel Margin="0,0,24,0">
<TextBlock Classes="fieldLabel" Text="Import trace limit (0 = off)" />
<NumericUpDown x:Name="TraceImportsBox" Width="170" Minimum="0" Maximum="4096" Increment="16"
Value="0" FormatString="0" CornerRadius="8" />
</StackPanel>
<StackPanel>
<TextBlock Classes="fieldLabel" Text="Strict dynlib resolution" />
<ToggleSwitch x:Name="StrictToggle" OnContent="On" OffContent="Off" />
</StackPanel>
</WrapPanel>
</StackPanel>
</Border>
<!-- Console -->
<Border Grid.Row="4" Classes="card" Padding="0">
<Grid RowDefinitions="Auto,*">
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto" Margin="16,14,16,10">
<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"
Padding="10,4" Margin="0,0,8,0" />
<Button Grid.Column="3" 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"
BorderBrush="{StaticResource CardBorderBrush}" CornerRadius="0,0,10,10">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Text}" Foreground="{Binding Brush}" TextWrapping="NoWrap" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Border>
</Grid>
</Grid>
<!-- Status bar -->
<Grid x:Name="StatusBar" Grid.Row="2" Height="32" Background="{StaticResource ChromeBrush}"
ColumnDefinitions="*,Auto">
<Grid Grid.Row="2" Background="{StaticResource ChromeBrush}" ColumnDefinitions="*,Auto">
<TextBlock x:Name="EmulatorPathText" Grid.Column="0" Text="Emulator: locating…" FontSize="11"
Foreground="{StaticResource FaintBrush}" VerticalAlignment="Center" Margin="16,0"
TextTrimming="CharacterEllipsis" />
File diff suppressed because it is too large Load Diff
+1 -25
View File
@@ -12,12 +12,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Version>0.0.1</Version>
</PropertyGroup>
<!-- Dependency-free; provides the BuildInfo provenance shown in the
title bar. -->
<ItemGroup>
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" />
<PackageReference Include="Avalonia.Desktop" />
@@ -27,26 +21,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
</ItemGroup>
<ItemGroup>
<AvaloniaResource Include="..\..\assets\images\logo.png" Link="Assets/logo.png" />
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Languages\*.json">
<LogicalName>Languages.%(Filename)%(Extension)</LogicalName>
</EmbeddedResource>
</ItemGroup>
<!-- The controller readers (DualSense raw HID + Xbox XInput) are shared
with the emulator's pad HLE. They are dependency-free, so they are
compiled in directly rather than pulling a reference to all of
SharpEmu.Libs into the launcher. -->
<ItemGroup>
<Compile Include="..\SharpEmu.Libs\Pad\PadState.cs" Link="Input/PadState.cs" />
<Compile Include="..\SharpEmu.Libs\Pad\HidNative.cs" Link="Input/HidNative.cs" />
<Compile Include="..\SharpEmu.Libs\Pad\DualSenseReader.cs" Link="Input/DualSenseReader.cs" />
<Compile Include="..\SharpEmu.Libs\Pad\XInputReader.cs" Link="Input/XInputReader.cs" />
</ItemGroup>
</Project>
-301
View File
@@ -1,301 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Runtime.InteropServices;
using System.Text;
using LibAtrac9;
namespace SharpEmu.GUI;
/// <summary>
/// Loops a game's sce_sys/snd0.at9 preview music while the game is selected
/// in the library, like the console home screen. The ATRAC9 stream is decoded
/// to WAV on a background task (vendored LibAtrac9); playback uses winmm's
/// PlaySound, so this is Windows-only and a no-op elsewhere.
/// </summary>
internal sealed class SndPreviewPlayer
{
private const uint SND_ASYNC = 0x0001;
private const uint SND_NODEFAULT = 0x0002;
private const uint SND_MEMORY = 0x0004;
private const uint SND_LOOP = 0x0008;
private readonly object _sync = new();
private int _generation;
private GCHandle _pinnedWav;
private bool _playing;
private bool _paused;
private string? _cachedPath;
private byte[]? _cachedWav;
/// <summary>Starts looping the given snd0.at9 after a short debounce.</summary>
public void Play(string at9Path)
{
if (!OperatingSystem.IsWindows())
{
return;
}
int generation;
lock (_sync)
{
generation = ++_generation;
}
_ = Task.Run(async () =>
{
// Debounce so skimming through the library does not decode (or
// start) a preview per tile.
await Task.Delay(120).ConfigureAwait(false);
byte[]? wav;
lock (_sync)
{
if (generation != _generation)
{
return;
}
wav = string.Equals(_cachedPath, at9Path, StringComparison.OrdinalIgnoreCase)
? _cachedWav
: null;
}
if (wav is null)
{
try
{
wav = DecodeAt9ToWav(File.ReadAllBytes(at9Path));
}
catch (Exception)
{
return; // corrupt or unsupported preview: stay silent
}
}
lock (_sync)
{
if (generation != _generation)
{
return;
}
_cachedPath = at9Path;
_cachedWav = wav;
StopLocked();
// The WAV image must stay pinned while winmm plays from it.
_pinnedWav = GCHandle.Alloc(wav, GCHandleType.Pinned);
_playing = PlaySound(_pinnedWav.AddrOfPinnedObject(), 0, SND_MEMORY | SND_ASYNC | SND_LOOP | SND_NODEFAULT);
if (!_playing)
{
_pinnedWav.Free();
}
}
});
}
public void Stop()
{
if (!OperatingSystem.IsWindows())
{
return;
}
lock (_sync)
{
_generation++;
StopLocked();
}
}
/// <summary>
/// Silences playback but keeps the decoded track ready, so
/// <see cref="Resume"/> can restart it (winmm cannot truly pause).
/// </summary>
public void Pause()
{
if (!OperatingSystem.IsWindows())
{
return;
}
lock (_sync)
{
if (!_playing)
{
return;
}
_ = PlaySound(0, 0, 0);
_playing = false;
_paused = true;
}
}
/// <summary>Restarts the track silenced by <see cref="Pause"/>.</summary>
public void Resume()
{
if (!OperatingSystem.IsWindows())
{
return;
}
lock (_sync)
{
if (!_paused || !_pinnedWav.IsAllocated)
{
return;
}
_paused = false;
_playing = PlaySound(_pinnedWav.AddrOfPinnedObject(), 0, SND_MEMORY | SND_ASYNC | SND_LOOP | SND_NODEFAULT);
}
}
private void StopLocked()
{
_ = PlaySound(0, 0, 0);
_playing = false;
_paused = false;
if (_pinnedWav.IsAllocated)
{
_pinnedWav.Free();
}
}
private static readonly Guid Atrac9SubFormat = new("47E142D2-36BA-4D8D-88FC-61654F8C836C");
/// <summary>
/// Decodes a Sony AT9 (RIFF-wrapped ATRAC9) file to a PCM16 WAV image.
/// Layout per Sony's container: an extensible fmt chunk whose extension
/// carries the 4-byte codec config, a fact chunk with the sample count
/// and encoder delay, and superframes in the data chunk.
/// </summary>
private static byte[] DecodeAt9ToWav(byte[] file)
{
using var reader = new BinaryReader(new MemoryStream(file), Encoding.ASCII);
if (Encoding.ASCII.GetString(reader.ReadBytes(4)) != "RIFF")
{
throw new InvalidDataException("Not a RIFF file.");
}
reader.BaseStream.Position += 4; // riff size
if (Encoding.ASCII.GetString(reader.ReadBytes(4)) != "WAVE")
{
throw new InvalidDataException("Not a WAVE file.");
}
byte[]? configData = null;
var sampleCount = 0;
var encoderDelay = 0;
var dataOffset = -1;
var dataSize = 0;
while (reader.BaseStream.Position + 8 <= reader.BaseStream.Length)
{
var chunkId = Encoding.ASCII.GetString(reader.ReadBytes(4));
var chunkSize = reader.ReadInt32();
var chunkStart = reader.BaseStream.Position;
switch (chunkId)
{
case "fmt ":
var formatTag = reader.ReadUInt16();
reader.BaseStream.Position = chunkStart + 24; // to SubFormat GUID
var subFormat = new Guid(reader.ReadBytes(16));
if (formatTag != 0xFFFE || subFormat != Atrac9SubFormat)
{
throw new InvalidDataException("Not an ATRAC9 stream.");
}
reader.BaseStream.Position += 4; // version info
configData = reader.ReadBytes(4);
break;
case "fact":
sampleCount = reader.ReadInt32();
reader.BaseStream.Position += 4; // input overlap delay
encoderDelay = reader.ReadInt32();
break;
case "data":
dataOffset = (int)chunkStart;
dataSize = chunkSize;
break;
}
reader.BaseStream.Position = chunkStart + chunkSize + (chunkSize & 1);
}
if (configData is null || sampleCount <= 0 || dataOffset < 0)
{
throw new InvalidDataException("Missing fmt, fact, or data chunk.");
}
var decoder = new Atrac9Decoder();
decoder.Initialize(configData);
var config = decoder.Config;
var superframeCount = (sampleCount + encoderDelay + config.SuperframeSamples - 1) / config.SuperframeSamples;
superframeCount = Math.Min(superframeCount, dataSize / config.SuperframeBytes);
var channels = config.ChannelCount;
var pcmBuffer = new short[channels][];
for (var i = 0; i < channels; i++)
{
pcmBuffer[i] = new short[config.SuperframeSamples];
}
var wav = new byte[44 + (sampleCount * channels * 2)];
WriteWavHeader(wav, channels, config.SampleRate, sampleCount);
var superframe = new byte[config.SuperframeBytes];
var decodedIndex = 0L; // per-channel, includes the encoder delay
var written = 0;
for (var f = 0; f < superframeCount && written < sampleCount; f++)
{
Buffer.BlockCopy(file, dataOffset + (f * config.SuperframeBytes), superframe, 0, config.SuperframeBytes);
decoder.Decode(superframe, pcmBuffer);
for (var s = 0; s < config.SuperframeSamples && written < sampleCount; s++)
{
if (decodedIndex++ < encoderDelay)
{
continue;
}
var sampleOffset = 44 + ((long)written * channels * 2);
for (var ch = 0; ch < channels; ch++)
{
BinaryPrimitives.WriteInt16LittleEndian(
wav.AsSpan((int)(sampleOffset + (ch * 2))),
pcmBuffer[ch][s]);
}
written++;
}
}
return wav;
}
private static void WriteWavHeader(byte[] wav, int channels, int sampleRate, int sampleCount)
{
var span = wav.AsSpan();
Encoding.ASCII.GetBytes("RIFF").CopyTo(span);
BinaryPrimitives.WriteInt32LittleEndian(span[4..], wav.Length - 8);
Encoding.ASCII.GetBytes("WAVE").CopyTo(span[8..]);
Encoding.ASCII.GetBytes("fmt ").CopyTo(span[12..]);
BinaryPrimitives.WriteInt32LittleEndian(span[16..], 16);
BinaryPrimitives.WriteInt16LittleEndian(span[20..], 1); // PCM
BinaryPrimitives.WriteInt16LittleEndian(span[22..], (short)channels);
BinaryPrimitives.WriteInt32LittleEndian(span[24..], sampleRate);
BinaryPrimitives.WriteInt32LittleEndian(span[28..], sampleRate * channels * 2);
BinaryPrimitives.WriteInt16LittleEndian(span[32..], (short)(channels * 2));
BinaryPrimitives.WriteInt16LittleEndian(span[34..], 16); // bits per sample
Encoding.ASCII.GetBytes("data").CopyTo(span[36..]);
BinaryPrimitives.WriteInt32LittleEndian(span[40..], sampleCount * channels * 2);
}
[DllImport("winmm.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool PlaySound(nint sound, nint module, uint flags);
}
-3
View File
@@ -180,9 +180,6 @@
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
},
"sharpemu.logging": {
"type": "Project"
}
}
}
+4 -6
View File
@@ -3,13 +3,11 @@
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);
@@ -110,14 +108,14 @@ public sealed class Aerolib : ISymbolCatalog
if (resourceName == null)
{
Log.Error("Embedded resource 'aerolib.bin' not found");
Console.Error.WriteLine("[AEROLIB] Embedded resource 'aerolib.bin' not found");
return;
}
using var stream = assembly.GetManifestResourceStream(resourceName);
if (stream == null)
{
Log.Error("Failed to open embedded resource stream");
Console.Error.WriteLine("[AEROLIB] Failed to open embedded resource stream");
return;
}
@@ -147,11 +145,11 @@ public sealed class Aerolib : ISymbolCatalog
_byExportName[name] = symbol;
}
Log.Info($"Loaded {_byNid.Count} NID entries from binary resource");
Console.Error.WriteLine($"[AEROLIB] Loaded {_byNid.Count} NID entries from binary resource");
}
catch (Exception ex)
{
Log.Error($"Failed to load embedded aerolib.bin: {ex.Message}", ex);
Console.Error.WriteLine($"[AEROLIB] Failed to load embedded aerolib.bin: {ex.Message}");
}
}
Binary file not shown.
-195
View File
@@ -3,7 +3,6 @@
using System.Collections.Concurrent;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace SharpEmu.HLE;
@@ -13,7 +12,6 @@ public sealed class ModuleManager : IModuleManager
private readonly ConcurrentDictionary<string, ExportedFunction> _exportTable = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, ExportedFunction> _exportNameTable = new(StringComparer.Ordinal);
private readonly object _registrationGate = new();
private readonly HashSet<(Assembly Assembly, Generation Generation)> _scannedAssemblies = new();
private bool _isFrozen;
public int RegisterFromAssembly(Assembly assembly, Generation generation, ISymbolCatalog? symbolCatalog = null)
@@ -27,12 +25,6 @@ public sealed class ModuleManager : IModuleManager
throw new InvalidOperationException("Module registration is frozen.");
}
// Deduplicated: one assembly is reached through many types.
if (!_scannedAssemblies.Add((assembly, generation)))
{
return 0;
}
var registeredCount = 0;
var instances = new Dictionary<Type, object>();
@@ -81,195 +73,8 @@ public sealed class ModuleManager : IModuleManager
{
_isFrozen = true;
}
WarmHleTypeInitializers();
}
// A .cctor or first JIT running on a guest thread's hijacked stack fail-fasts the CLR.
// Run every HLE type's initializer and JIT its methods here first, on a host thread.
private void WarmHleTypeInitializers()
{
Assembly[] assemblies;
lock (_registrationGate)
{
assemblies = _scannedAssemblies.Select(entry => entry.Assembly).Distinct().ToArray();
}
assemblies = WithGuestReachableDependencies(assemblies);
var bclWarmed = WarmFrameworkTypeInitializers();
var warmed = 0;
var failed = 0;
var jitted = 0;
var jitFailed = 0;
foreach (var assembly in assemblies)
{
Type[] types;
try
{
types = assembly.GetTypes();
}
catch (ReflectionTypeLoadException ex)
{
types = ex.Types.Where(t => t is not null).ToArray()!;
}
const BindingFlags allMembers = BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
foreach (var type in types)
{
if (type is null || type.ContainsGenericParameters)
{
continue;
}
try
{
RuntimeHelpers.RunClassConstructor(type.TypeHandle);
warmed++;
}
catch
{
// A throw here beats a guest-thread fail-fast later; swallow and continue.
failed++;
}
// Force-JIT (not execute) every method so no guest thread compiles one first.
MethodBase[] members;
try
{
members = type.GetConstructors(allMembers)
.Concat<MethodBase>(type.GetMethods(allMembers))
.ToArray();
}
catch
{
continue;
}
foreach (var member in members)
{
if (member.ContainsGenericParameters || member.IsAbstract || member.MethodImplementationFlags.HasFlag(MethodImplAttributes.InternalCall))
{
continue;
}
try
{
RuntimeHelpers.PrepareMethod(member.MethodHandle);
jitted++;
}
catch
{
jitFailed++;
}
}
}
}
Console.Error.WriteLine(
$"[HLE] Warmed {warmed} type initializers ({failed} threw) + JIT-compiled {jitted} methods ({jitFailed} skipped) across {assemblies.Length} HLE assemblies, plus {bclWarmed} framework type initializers.");
}
// Framework .cctors too (but not JIT — the BCL is too large).
private static int WarmFrameworkTypeInitializers()
{
var warmed = 0;
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
var name = assembly.GetName().Name;
if (name is null || !IsFrameworkAssembly(name))
{
continue;
}
Type[] types;
try
{
types = assembly.GetTypes();
}
catch (ReflectionTypeLoadException ex)
{
types = ex.Types.Where(t => t is not null).ToArray()!;
}
catch
{
continue;
}
foreach (var type in types)
{
if (type is null || type.ContainsGenericParameters)
{
continue;
}
try
{
RuntimeHelpers.RunClassConstructor(type.TypeHandle);
warmed++;
}
catch
{
}
}
}
return warmed;
}
private static bool IsFrameworkAssembly(string assemblyName) =>
assemblyName.StartsWith("System", StringComparison.Ordinal) ||
string.Equals(assemblyName, "netstandard", StringComparison.Ordinal);
// Warm the interop assemblies guest threads reach (e.g. Silk.NET via the flip path).
private static Assembly[] WithGuestReachableDependencies(Assembly[] scanned)
{
var result = new Dictionary<string, Assembly>(StringComparer.Ordinal);
foreach (var assembly in scanned)
{
result[assembly.FullName ?? assembly.GetName().Name ?? string.Empty] = assembly;
}
foreach (var assembly in scanned)
{
AssemblyName[] references;
try
{
references = assembly.GetReferencedAssemblies();
}
catch
{
continue;
}
foreach (var reference in references)
{
var name = reference.Name;
if (name is null || !IsGuestReachableInterop(name))
{
continue;
}
try
{
var loaded = Assembly.Load(reference);
result[loaded.FullName ?? name] = loaded;
}
catch
{
}
}
}
return result.Values.ToArray();
}
private static bool IsGuestReachableInterop(string assemblyName) =>
assemblyName.StartsWith("Silk.NET", StringComparison.Ordinal) ||
assemblyName.StartsWith("SharpEmu", StringComparison.Ordinal);
public bool TryGetFunction(string nid, out Delegate function)
{
ArgumentException.ThrowIfNullOrWhiteSpace(nid);
-14
View File
@@ -40,13 +40,6 @@ public enum OrbisGen2Result : int
/// </summary>
ORBIS_GEN2_ERROR_DEADLOCK = unchecked((int)0x8002000B),
/// <summary>
/// Indicates that the waited-on object was deleted while the caller was
/// blocked on it. Matches the SCE kernel EACCES code that waiters of a
/// deleted semaphore observe.
/// </summary>
ORBIS_GEN2_ERROR_DELETED = unchecked((int)0x8002000D),
/// <summary>
/// Indicates that the target resource is busy.
/// </summary>
@@ -57,13 +50,6 @@ public enum OrbisGen2Result : int
/// </summary>
ORBIS_GEN2_ERROR_TRY_AGAIN = unchecked((int)0x80020023),
/// <summary>
/// Indicates that a blocked wait was canceled (e.g. sceKernelCancelSema).
/// Matches the SCE kernel ECANCELED code that canceled semaphore waiters
/// observe.
/// </summary>
ORBIS_GEN2_ERROR_CANCELED = unchecked((int)0x80020055),
/// <summary>
/// Indicates that behavior is recognized but not implemented yet.
/// </summary>
-4
View File
@@ -12,10 +12,6 @@ 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 -5
View File
@@ -1,10 +1,6 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"sharpemu.logging": {
"type": "Project"
}
}
"net10.0": {}
}
}
+119 -593
View File
@@ -120,9 +120,6 @@ public static class AgcExports
private const uint RegisterDefaultsVersion13 = 13;
private const int RegisterDefaultsSize = 0x40;
private const int RegisterDefaultBlockSize = 16 * 8;
private const ulong ResourceRegistrationBytesPerResource = 0x118;
private const ulong ResourceRegistrationBytesPerOwner = 0x1E0;
private const int ResourceRegistrationMaxNameLength = 256;
private const ulong ShaderUserDataOffset = 0x08;
private const ulong ShaderCodeOffset = 0x10;
@@ -182,6 +179,7 @@ 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;
@@ -391,26 +389,9 @@ public static class AgcExports
public SubmittedDcbState Graphics { get; } = new();
public Dictionary<uint, SubmittedDcbState> ComputeQueues { get; } = new();
public Dictionary<ulong, ComputeImageWriter> ComputeImageWriters { get; } = new();
public Dictionary<uint, string> ResourceOwners { get; } = new();
public Dictionary<uint, RegisteredAgcResource> RegisteredResources { get; } = new();
public bool ResourceRegistrationInitialized { get; set; }
public ulong ResourceRegistrationMemory { get; set; }
public ulong ResourceRegistrationMemorySize { get; set; }
public uint ResourceRegistrationMaxOwners { get; set; }
public uint DefaultOwner { get; set; } = DefaultAgcOwner;
public uint NextOwner { get; set; } = 1;
public uint NextResource { get; set; } = 1;
public ulong WorkSequence { get; set; }
}
private readonly record struct RegisteredAgcResource(
uint Owner,
ulong Address,
ulong Size,
string Name,
uint Type,
uint Flags);
private readonly record struct RegisterDefaultValue(uint Offset, uint Value);
private readonly record struct RegisterDefaultGroup(
@@ -2020,57 +2001,6 @@ 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;
@@ -2119,7 +2049,6 @@ public static class AgcExports
}
ParseSubmittedDcb(ctx, gpuState, queueState, commandAddress, dwordCount, tracePackets);
DrainResumableDcbs(ctx, gpuState, tracePackets);
}
ctx[CpuRegister.Rax] = 0;
@@ -2140,87 +2069,12 @@ public static class AgcExports
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (!ctx.TryWriteUInt32(outAddress, ResourceRegistrationMaxNameLength))
if (!ctx.TryWriteUInt32(outAddress, 256))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceAgc(
$"agc.driver_get_resource_registration_max_name_length " +
$"out=0x{outAddress:X16} value={ResourceRegistrationMaxNameLength}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "AOLcoIkQDgM",
ExportName = "sceAgcDriverQueryResourceRegistrationUserMemoryRequirements",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int DriverQueryResourceRegistrationUserMemoryRequirements(CpuContext ctx)
{
var sizeAddress = ctx[CpuRegister.Rdi];
var resourceCount = ctx[CpuRegister.Rsi];
var ownerCount = ctx[CpuRegister.Rdx];
if (sizeAddress == 0 || resourceCount == 0 || ownerCount == 0)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
ulong requiredSize;
try
{
requiredSize = checked(
resourceCount * ResourceRegistrationBytesPerResource +
ownerCount * ResourceRegistrationBytesPerOwner);
}
catch (OverflowException)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (!ctx.TryWriteUInt64(sizeAddress, requiredSize))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceAgc(
$"agc.driver_query_resource_registration_memory resources={resourceCount} " +
$"owners={ownerCount} bytes=0x{requiredSize:X}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "F0Y42t-3e18",
ExportName = "sceAgcDriverInitResourceRegistration",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int DriverInitResourceRegistration(CpuContext ctx)
{
var memoryAddress = ctx[CpuRegister.Rdi];
var memorySize = ctx[CpuRegister.Rsi];
var ownerCount = ctx[CpuRegister.Rdx];
if (memoryAddress == 0 || memorySize == 0 || ownerCount == 0 || ownerCount > uint.MaxValue)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
lock (state.Gate)
{
state.ResourceRegistrationInitialized = true;
state.ResourceRegistrationMemory = memoryAddress;
state.ResourceRegistrationMemorySize = memorySize;
state.ResourceRegistrationMaxOwners = (uint)ownerCount;
state.ResourceOwners.Clear();
state.RegisteredResources.Clear();
state.DefaultOwner = DefaultAgcOwner;
state.NextOwner = 1;
state.NextResource = 1;
}
TraceAgc(
$"agc.driver_init_resource_registration memory=0x{memoryAddress:X16} " +
$"bytes=0x{memorySize:X} owners={ownerCount}");
TraceAgc($"agc.driver_get_resource_registration_max_name_length out=0x{outAddress:X16} value=256");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
@@ -2239,97 +2093,12 @@ public static class AgcExports
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
uint owner;
lock (state.Gate)
{
owner = state.DefaultOwner;
}
if (!ctx.TryWriteUInt32(ownerAddress, owner))
if (!ctx.TryWriteUInt32(ownerAddress, DefaultAgcOwner))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceAgc($"agc.driver_get_default_owner out=0x{ownerAddress:X16} owner={owner}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "U9ueyEhSkF4",
ExportName = "sceAgcDriverRegisterDefaultOwner",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int DriverRegisterDefaultOwner(CpuContext ctx)
{
var owner = (uint)ctx[CpuRegister.Rdi];
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
lock (state.Gate)
{
state.DefaultOwner = owner;
}
TraceAgc($"agc.driver_register_default_owner owner={owner}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "X-Nm5KLREeg",
ExportName = "sceAgcDriverRegisterOwner",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int DriverRegisterOwner(CpuContext ctx)
{
var ownerAddress = ctx[CpuRegister.Rdi];
var nameAddress = ctx[CpuRegister.Rsi];
if (ownerAddress == 0 || nameAddress == 0 ||
!TryReadGuestCString(
ctx,
nameAddress,
ResourceRegistrationMaxNameLength,
out var nameBytes))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
uint owner;
lock (state.Gate)
{
if (!state.ResourceRegistrationInitialized ||
state.ResourceRegistrationMaxOwners != 0 &&
state.ResourceOwners.Count >= state.ResourceRegistrationMaxOwners)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
owner = state.NextOwner;
while (owner == state.DefaultOwner || state.ResourceOwners.ContainsKey(owner))
{
owner++;
if (owner == 0)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
}
state.NextOwner = owner + 1;
state.ResourceOwners.Add(owner, System.Text.Encoding.UTF8.GetString(nameBytes));
}
if (!ctx.TryWriteUInt32(ownerAddress, owner))
{
lock (state.Gate)
{
state.ResourceOwners.Remove(owner);
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceAgc(
$"agc.driver_register_owner out=0x{ownerAddress:X16} owner={owner} " +
$"name={System.Text.Encoding.UTF8.GetString(nameBytes)}");
TraceAgc($"agc.driver_get_default_owner out=0x{ownerAddress:X16} owner={DefaultAgcOwner}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
@@ -2340,90 +2109,19 @@ public static class AgcExports
LibraryName = "libSceAgc")]
public static int DriverRegisterResource(CpuContext ctx)
{
var resourceHandleAddress = ctx[CpuRegister.Rdi];
var resourceAddress = ctx[CpuRegister.Rdi];
var owner = (uint)ctx[CpuRegister.Rsi];
var resourceAddress = ctx[CpuRegister.Rdx];
var resourceSize = ctx[CpuRegister.Rcx];
var nameAddress = ctx[CpuRegister.R8];
var type = (uint)ctx[CpuRegister.R9];
if (resourceHandleAddress == 0 || resourceAddress == 0 || resourceSize == 0 ||
!ctx.TryReadUInt32(ctx[CpuRegister.Rsp] + sizeof(ulong), out var flags) ||
!TryReadGuestCString(
ctx,
nameAddress,
ResourceRegistrationMaxNameLength,
out var nameBytes))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
uint resourceHandle;
lock (state.Gate)
{
if (!state.ResourceRegistrationInitialized ||
owner != state.DefaultOwner &&
!state.ResourceOwners.ContainsKey(owner))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
resourceHandle = state.NextResource++;
if (resourceHandle == 0)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
state.RegisteredResources.Add(
resourceHandle,
new RegisteredAgcResource(
owner,
resourceAddress,
resourceSize,
System.Text.Encoding.UTF8.GetString(nameBytes),
type,
flags));
}
if (!ctx.TryWriteUInt32(resourceHandleAddress, resourceHandle))
{
lock (state.Gate)
{
state.RegisteredResources.Remove(resourceHandle);
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var nameAddress = ctx[CpuRegister.Rdx];
var type = (uint)ctx[CpuRegister.R8];
var flags = (uint)ctx[CpuRegister.R9];
TraceAgc(
$"agc.driver_register_resource handle={resourceHandle} owner={owner} " +
$"resource=0x{resourceAddress:X16} bytes=0x{resourceSize:X} " +
$"name={System.Text.Encoding.UTF8.GetString(nameBytes)} type={type} flags={flags}");
$"agc.driver_register_resource resource=0x{resourceAddress:X16} owner={owner} " +
$"name=0x{nameAddress:X16} type={type} flags={flags}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "pWLG7WOpVcw",
ExportName = "sceAgcDriverUnregisterResource",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int DriverUnregisterResource(CpuContext ctx)
{
var resourceHandle = (uint)ctx[CpuRegister.Rdi];
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
lock (state.Gate)
{
if (!state.RegisteredResources.Remove(resourceHandle))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
}
TraceAgc($"agc.driver_unregister_resource handle={resourceHandle}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "-KRzWekV120",
ExportName = "sceAgcDriverUnknown_KRzWekV120",
@@ -2472,63 +2170,6 @@ 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,
@@ -2560,6 +2201,7 @@ public static class AgcExports
$"agc.dcb.packet dw={offset} addr=0x{currentAddress:X16} " +
$"header=0x{header:X8} len=1 type=2");
}
offset++;
continue;
}
@@ -2669,91 +2311,16 @@ public static class AgcExports
state.InstanceCount = Math.Max(instanceCount, 1);
}
// 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 &&
if (op == ItNop &&
register is RWaitMem32 or RWaitMem64 &&
length >= (register == RWaitMem32 ? 6u : 9u))
{
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
}
}
ObserveSubmittedWaitRegMem(ctx, currentAddress, register == RWaitMem64, tracePackets);
}
if (op == ItWaitRegMem && length >= 7)
{
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;
}
}
ObserveSubmittedStandardWaitRegMem(ctx, currentAddress, tracePackets);
}
if (TryReadSubmittedDrawCount(
@@ -3074,6 +2641,104 @@ 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,
@@ -3091,11 +2756,10 @@ 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 => ctx.TryWriteUInt32(destinationAddress, dataLo),
2 or 3 => ctx.TryWriteUInt64(destinationAddress, data),
1 or 2 => ctx.TryWriteUInt32(destinationAddress, dataLo),
3 => ctx.TryWriteUInt64(destinationAddress, data),
_ => false,
};
@@ -3191,11 +2855,6 @@ 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))
{
@@ -3547,60 +3206,10 @@ public static class AgcExports
globalMemoryBindings,
vertexInputs,
renderTargets,
ApplyTransparentPremultipliedFillClear(
CreateRenderState(state.CxRegisters, renderTargets.FirstOrDefault()),
textures,
vertexInputs,
pixelEvaluation.InitialScalarRegisters));
CreateRenderState(state.CxRegisters, renderTargets.FirstOrDefault()));
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,
@@ -4501,7 +4110,7 @@ public static class AgcExports
var localSizeZ = GetComputeLocalSize(state.ShRegisters, ComputeNumThreadZ);
var gpuDispatch = false;
var computeError = string.Empty;
if ((hasStorageBinding || evaluation.GlobalMemoryBindings.Count != 0) &&
if (hasStorageBinding &&
(ulong)localSizeX * localSizeY * localSizeZ <= 1024)
{
var shaderKey = (
@@ -4571,8 +4180,7 @@ public static class AgcExports
$"groups={dispatch.GroupCountX}x{dispatch.GroupCountY}x{dispatch.GroupCountZ} " +
$"local={localSizeX}x{localSizeY}x{localSizeZ} " +
$"sys={DescribeComputeSystemRegisters(computeSystemRegisters)} " +
$"gpu={gpuDispatch} blits={blitCount} " +
$"globals={evaluation.GlobalMemoryBindings.Count}" +
$"gpu={gpuDispatch} blits={blitCount}" +
(computeError.Length == 0 ? string.Empty : $" error={computeError}") +
$" bindings=[{string.Join(',', descriptions)}]");
}
@@ -6027,86 +5635,4 @@ 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;
}
}
+4 -10
View File
@@ -621,8 +621,6 @@ internal static class Gen5ShaderTranslator
0x10 => "SSendmsg",
0x16 => "STtraceData",
0x20 => "SInstPrefetch",
0x21 => "SClause",
0x23 => "SWaitcntDepctr",
_ => string.Empty,
};
@@ -720,7 +718,7 @@ internal static class Gen5ShaderTranslator
}
var src0 = word & 0x1FF;
sizeDwords = opcode is 0x20 or 0x21 or 0x2C or 0x2D || src0 is 0xF9 or 0xFA or 0xFF ? 2u : 1u;
sizeDwords = opcode is 0x20 or 0x21 || src0 is 0xF9 or 0xFA or 0xFF ? 2u : 1u;
error = string.Empty;
name = opcode switch
{
@@ -759,9 +757,7 @@ internal static class Gen5ShaderTranslator
0x28 => "VAddcU32",
0x29 => "VSubbU32",
0x2A => "VSubbrevU32",
0x2B => "VFmacF32",
0x2C => "VFmamkF32",
0x2D => "VFmaakF32",
0x2B => "VLdexpF32",
0x2F => "VCvtPkrtzF16F32",
0x30 => "VCvtPkU16U32",
0x31 => "VCvtPkI16I32",
@@ -874,7 +870,6 @@ internal static class Gen5ShaderTranslator
0x10F => "VMinF32",
0x110 => "VMaxF32",
0x11F => "VMacF32",
0x12B => "VFmacF32",
0x12F => "VCvtPkrtzF16F32",
0x141 => "VMadF32",
0x143 => "VMadU32U24",
@@ -902,7 +897,6 @@ internal static class Gen5ShaderTranslator
0x169 => "VMulLoU32",
0x16A => "VMulHiU32",
0x16B => "VMulLoI32",
0x16C => "VMulHiI32",
0x360 => "VMadU32U16",
0x361 => "VMulLoU32",
0x362 => "VLdexpF32",
@@ -1385,7 +1379,7 @@ internal static class Gen5ShaderTranslator
Gen5Operand.Source(word & 0x1FF, literal),
Gen5Operand.Vector((word >> 9) & 0xFF),
];
if (opcode is "VMadMkF32" or "VFmamkF32" && literal.HasValue)
if (opcode == "VMadMkF32" && literal.HasValue)
{
sources =
[
@@ -1394,7 +1388,7 @@ internal static class Gen5ShaderTranslator
sources[1],
];
}
else if (opcode is "VMadAkF32" or "VFmaakF32" && literal.HasValue)
else if (opcode == "VMadAkF32" && literal.HasValue)
{
sources =
[
@@ -255,8 +255,6 @@ internal static partial class Gen5SpirvTranslator
case "VFmaF32":
case "VMadMkF32":
case "VMadAkF32":
case "VFmamkF32":
case "VFmaakF32":
result = EmitFloatResult(
instruction,
Ext(
@@ -267,7 +265,6 @@ internal static partial class Gen5SpirvTranslator
GetFloatSource(instruction, 2)));
break;
case "VMacF32":
case "VFmacF32":
{
var addend = Bitcast(_floatType, LoadV(destination));
result = EmitFloatResult(
@@ -337,7 +334,6 @@ internal static partial class Gen5SpirvTranslator
result = EmitSubtractWithBorrow(instruction, reverse: true);
break;
case "VMulLoU32":
case "VMulLoI32":
case "VMulU32U24":
result = EmitIntegerBinary(instruction, SpirvOp.IMul);
break;
@@ -373,37 +369,6 @@ internal static partial class Gen5SpirvTranslator
_module.Constant64(_ulongType, 32)));
break;
}
case "VMulHiI32":
{
var wideLeft = _module.AddInstruction(
SpirvOp.SConvert,
_longType,
Bitcast(_intType, GetRawSource(instruction, 0)));
var wideRight = _module.AddInstruction(
SpirvOp.SConvert,
_longType,
Bitcast(_intType, GetRawSource(instruction, 1)));
var product = _module.AddInstruction(
SpirvOp.IMul,
_longType,
wideLeft,
wideRight);
result = _module.AddInstruction(
SpirvOp.UConvert,
_uintType,
ShiftRightLogical64(
Bitcast(_ulongType, product),
_module.Constant64(_ulongType, 32)));
break;
}
case "VBcntU32B32":
result = IAdd(
_module.AddInstruction(
SpirvOp.BitCount,
_uintType,
GetRawSource(instruction, 0)),
GetRawSource(instruction, 1));
break;
case "VMadU32U24":
{
var left = BitwiseAnd(
@@ -601,18 +566,6 @@ internal static partial class Gen5SpirvTranslator
Ext(38, _uintType, high, right));
break;
}
case "VSadU32":
{
var left = GetRawSource(instruction, 0);
var right = GetRawSource(instruction, 1);
var difference = _module.AddInstruction(
SpirvOp.ISub,
_uintType,
Ext(41, _uintType, left, right),
Ext(38, _uintType, left, right));
result = IAdd(difference, GetRawSource(instruction, 2));
break;
}
case "VMed3I32":
{
var left = Bitcast(_intType, GetRawSource(instruction, 0));
@@ -722,14 +675,6 @@ internal static partial class Gen5SpirvTranslator
result = Ext(58, _uintType, vector);
break;
}
case "VCvtPkU16U32":
case "VCvtPkI16I32":
result = BitwiseOr(
BitwiseAnd(GetRawSource(instruction, 0), UInt(0xFFFF)),
ShiftLeftLogical(
BitwiseAnd(GetRawSource(instruction, 1), UInt(0xFFFF)),
UInt(16)));
break;
default:
error = $"unsupported vector opcode {instruction.Opcode}";
return false;
@@ -1164,52 +1109,6 @@ internal static partial class Gen5SpirvTranslator
left,
right);
break;
case "SMulHiU32":
{
var wideLeft = _module.AddInstruction(
SpirvOp.UConvert,
_ulongType,
left);
var wideRight = _module.AddInstruction(
SpirvOp.UConvert,
_ulongType,
right);
var product = _module.AddInstruction(
SpirvOp.IMul,
_ulongType,
wideLeft,
wideRight);
result = _module.AddInstruction(
SpirvOp.UConvert,
_uintType,
ShiftRightLogical64(
product,
_module.Constant64(_ulongType, 32)));
break;
}
case "SMulHiI32":
{
var wideLeft = _module.AddInstruction(
SpirvOp.SConvert,
_longType,
Bitcast(_intType, left));
var wideRight = _module.AddInstruction(
SpirvOp.SConvert,
_longType,
Bitcast(_intType, right));
var product = _module.AddInstruction(
SpirvOp.IMul,
_longType,
wideLeft,
wideRight);
result = _module.AddInstruction(
SpirvOp.UConvert,
_uintType,
ShiftRightLogical64(
Bitcast(_ulongType, product),
_module.Constant64(_ulongType, 32)));
break;
}
case "SAndB32":
result = BitwiseAnd(left, right);
Store(_scc, IsNotZero(result));
@@ -1331,43 +1230,6 @@ internal static partial class Gen5SpirvTranslator
Store(_scc, IsNotZero(result));
break;
}
case "SAbsdiffI32":
{
var wideLeft = _module.AddInstruction(
SpirvOp.SConvert,
_longType,
Bitcast(_intType, left));
var wideRight = _module.AddInstruction(
SpirvOp.SConvert,
_longType,
Bitcast(_intType, right));
var difference = _module.AddInstruction(
SpirvOp.ISub,
_longType,
wideLeft,
wideRight);
result = _module.AddInstruction(
SpirvOp.UConvert,
_uintType,
Ext(5, _longType, difference));
Store(_scc, IsNotZero(result));
break;
}
case "SPackLlB32B16":
result = BitwiseOr(
BitwiseAnd(left, UInt(0xFFFF)),
ShiftLeftLogical(right, UInt(16)));
break;
case "SPackLhB32B16":
result = BitwiseOr(
BitwiseAnd(left, UInt(0xFFFF)),
BitwiseAnd(right, UInt(0xFFFF0000)));
break;
case "SPackHhB32B16":
result = BitwiseOr(
ShiftRightLogical(left, UInt(16)),
BitwiseAnd(right, UInt(0xFFFF0000)));
break;
case "SCselectB32":
result = _module.AddInstruction(
SpirvOp.Select,
-102
View File
@@ -1,102 +0,0 @@
// 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,
};
}
}
-37
View File
@@ -1,37 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Ajm;
public static class AjmExports
{
private const int OrbisAjmErrorInvalidParameter = unchecked((int)0x80930005);
private static int _nextContextId;
[SysAbiExport(
Nid = "dl+4eHSzUu4",
ExportName = "sceAjmInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAjm")]
public static int Initialize(CpuContext ctx)
{
var reserved = ctx[CpuRegister.Rdi];
var outContextAddress = ctx[CpuRegister.Rsi];
// AJM requires a zero reserved argument.
if (reserved != 0 || outContextAddress == 0)
{
return ctx.SetReturn(OrbisAjmErrorInvalidParameter);
}
var contextId = unchecked((uint)Interlocked.Increment(ref _nextContextId));
if (!ctx.TryWriteUInt32(outContextAddress, contextId))
{
return ctx.SetReturn(OrbisAjmErrorInvalidParameter);
}
return ctx.SetReturn(0);
}
}
+4 -38
View File
@@ -267,53 +267,19 @@ public static class AmprExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
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))
if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath))
{
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;
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;
}
// 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);
var result = TryReadFileToGuestMemory(ctx, hostPath, fileOffset, destination, size, out var 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;
+1 -1
View File
@@ -21,7 +21,7 @@ internal static class AmprFileRegistry
return _hostPathsById.TryGetValue(id, out hostPath!);
}
internal static uint ComputeFileId(string guestPath)
private static uint ComputeFileId(string guestPath)
{
var bytes = System.Text.Encoding.UTF8.GetBytes(guestPath);
@@ -1,156 +0,0 @@
// 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;
}
}
+14 -12
View File
@@ -8,14 +8,9 @@ namespace SharpEmu.Libs.Audio;
public static class AudioOut2Exports
{
// 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 AudioOut2ContextParamSize = 0x80;
private const int AudioOut2ContextMemorySize = 0x10000;
private const int AudioOut2ContextMemoryAlignment = 0x10000;
private static long _nextContextHandle = 1;
private static long _nextUserHandle = 1;
private static int _nextPortId;
@@ -64,13 +59,20 @@ public static class AudioOut2Exports
public static int AudioOut2ContextQueryMemory(CpuContext ctx)
{
var paramAddress = ctx[CpuRegister.Rdi];
var outMemorySizeAddress = ctx[CpuRegister.Rsi];
if (paramAddress == 0 || outMemorySizeAddress == 0)
var memoryInfoAddress = ctx[CpuRegister.Rsi];
if (paramAddress == 0 || memoryInfoAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
return ctx.TryWriteUInt64(outMemorySizeAddress, AudioOut2ContextMemorySize)
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)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
@@ -202,7 +204,7 @@ public static class AudioOut2Exports
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var outUserAddress = ctx[CpuRegister.Rsi];
if ((userId != 0 && userId != 1 && userId != 1000 && userId != 255) || outUserAddress == 0)
if ((userId != 0 && userId != 1 && userId != 255) || outUserAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
@@ -35,42 +35,6 @@ 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,6 +8,7 @@ namespace SharpEmu.Libs.CommonDialog;
public static class CommonDialogExports
{
private const int AlreadySystemInitialized = unchecked((int)0x80B80002);
private static int _initialized;
[SysAbiExport(
@@ -17,12 +18,11 @@ public static class CommonDialogExports
LibraryName = "libSceCommonDialog")]
public static int CommonDialogInitialize(CpuContext ctx)
{
// 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;
var result = Interlocked.Exchange(ref _initialized, 1) == 0
? 0
: AlreadySystemInitialized;
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
[SysAbiExport(
@@ -8,13 +8,8 @@ namespace SharpEmu.Libs.CommonDialog;
public static class MsgDialogExports
{
private const int StatusNone = 0;
private const int StatusInitialized = 1;
private const int StatusFinished = 3;
private const int ResultSize = 0x20;
private const int AlreadyInitialized = unchecked((int)0x80B80002);
private static int _initialized;
private static int _status;
[SysAbiExport(
Nid = "lDqxaY1UbEo",
@@ -23,162 +18,9 @@ public static class MsgDialogExports
LibraryName = "libSceMsgDialog")]
public static int MsgDialogInitialize(CpuContext ctx)
{
// 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)
{
var result = Interlocked.Exchange(ref _initialized, 1) == 0
? 0
: AlreadyInitialized;
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
+2 -2
View File
@@ -93,7 +93,7 @@ public static class CxaGuardExports
}
[SysAbiExport(
Nid = "9rAeANT2tyE",
Nid = "",
ExportName = "__cxa_guard_release",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
@@ -142,7 +142,7 @@ public static class CxaGuardExports
}
[SysAbiExport(
Nid = "2emaaluWzUw",
Nid = "",
ExportName = "__cxa_guard_abort",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
-104
View File
@@ -1,104 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.DiscMap;
/// <summary>
/// HLE implementation of libSceDiscMap, ported from Kyty's LibDiscMap.cpp
/// (https://github.com/InoriRus/Kyty, MIT). Titles installed to internal
/// storage query these entry points on nearly every file access to decide
/// whether a read must be redirected to the Blu-ray drive. Reporting every
/// request as already resident on HDD lets the regular file system path
/// service the read.
/// </summary>
public static class DiscMapExports
{
private const int DiscMapErrorInvalidArgument = unchecked((int)0x81100001);
private const int DiscMapErrorLocationNotMapped = unchecked((int)0x81100002);
private const int DiscMapErrorFileNotFound = unchecked((int)0x81100003);
private const int DiscMapErrorNoBitmapInfo = unchecked((int)0x81100004);
[SysAbiExport(
Nid = "lbQKqsERhtE",
ExportName = "sceDiscMapIsRequestOnHDD",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceDiscMap")]
public static int DiscMapIsRequestOnHDD(CpuContext ctx)
{
var pathAddress = ctx[CpuRegister.Rdi];
var offset = ctx[CpuRegister.Rsi];
var size = ctx[CpuRegister.Rdx];
var resultAddress = ctx[CpuRegister.Rcx];
if (pathAddress == 0 || resultAddress == 0)
{
return DiscMapErrorInvalidArgument;
}
if (!ctx.TryWriteInt32(resultAddress, 1))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceDiscMap(ctx, "sceDiscMapIsRequestOnHDD", pathAddress, offset, size);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "fJgP+wqifno",
ExportName = "sceDiscMapUnknownFJgP",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceDiscMap")]
public static int DiscMapUnknownFJgP(CpuContext ctx) => WriteMappingTriple(ctx, "sceDiscMapUnknownFJgP");
[SysAbiExport(
Nid = "ioKMruft1ek",
ExportName = "sceDiscMapUnknownIoKM",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceDiscMap")]
public static int DiscMapUnknownIoKM(CpuContext ctx) => WriteMappingTriple(ctx, "sceDiscMapUnknownIoKM");
private static int WriteMappingTriple(CpuContext ctx, string exportName)
{
var pathAddress = ctx[CpuRegister.Rdi];
var offset = ctx[CpuRegister.Rsi];
var size = ctx[CpuRegister.Rdx];
var flagsAddress = ctx[CpuRegister.Rcx];
var outAddress1 = ctx[CpuRegister.R8];
var outAddress2 = ctx[CpuRegister.R9];
if (pathAddress == 0 || flagsAddress == 0 || outAddress1 == 0 || outAddress2 == 0)
{
return DiscMapErrorInvalidArgument;
}
if (!ctx.TryWriteUInt64(flagsAddress, 0) ||
!ctx.TryWriteUInt64(outAddress1, 0) ||
!ctx.TryWriteUInt64(outAddress2, 0))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceDiscMap(ctx, exportName, pathAddress, offset, size);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static void TraceDiscMap(CpuContext ctx, string exportName, ulong pathAddress, ulong offset, ulong size)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_DISCMAP"), "1", StringComparison.Ordinal))
{
return;
}
var path = ctx.TryReadNullTerminatedUtf8(pathAddress, 1024, out var value)
? value
: $"<unreadable 0x{pathAddress:X16}>";
Console.Error.WriteLine($"[HLE][DISCMAP] {exportName} path={path} offset=0x{offset:X} size=0x{size:X}");
}
}
-46
View File
@@ -1,46 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace SharpEmu.Libs;
public static class HostTimerResolution
{
private const uint TargetPeriodMilliseconds = 1;
private static int _requested;
public static void Request()
{
if (Interlocked.Exchange(ref _requested, 1) != 0)
{
return;
}
if (!OperatingSystem.IsWindows())
{
return;
}
try
{
if (TimeBeginPeriod(TargetPeriodMilliseconds) != 0)
{
Console.Error.WriteLine(
"[LOADER][WARN] Host timer resolution request rejected; " +
"timed waits keep the default ~15.6 ms granularity.");
}
}
catch (DllNotFoundException exception)
{
Console.Error.WriteLine(
$"[LOADER][WARN] Host timer resolution unavailable: {exception.Message}");
}
}
[SupportedOSPlatform("windows")]
[DllImport("winmm.dll", EntryPoint = "timeBeginPeriod", ExactSpelling = true)]
private static extern uint TimeBeginPeriod(uint uPeriod);
}
-46
View File
@@ -1,46 +0,0 @@
// 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;
}
}
-52
View File
@@ -78,58 +78,6 @@ public static class JsonExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "WSOuge5IsCg",
ExportName = "_ZN3sce4Json14InitParameter2C1Ev",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson2")]
public static int InitParameter2Constructor(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
TraceJson("InitParameter2.ctor", thisAddress, ctx[CpuRegister.Rsi]);
ctx[CpuRegister.Rax] = thisAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "I2QC8PYhJWY",
ExportName = "_ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson2")]
public static int InitParameter2SetAllocator(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
TraceJson("InitParameter2.setAllocator", thisAddress, ctx[CpuRegister.Rsi]);
ctx[CpuRegister.Rax] = thisAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "Eu95jmqn5Rw",
ExportName = "_ZN3sce4Json14InitParameter217setFileBufferSizeEm",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson2")]
public static int InitParameter2SetFileBufferSize(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
TraceJson("InitParameter2.setFileBufferSize", thisAddress, ctx[CpuRegister.Rsi]);
ctx[CpuRegister.Rax] = thisAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "IXW-z8pggfg",
ExportName = "_ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson2")]
public static int Initializer2Constructor(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
TraceJson("Initializer2.ctor", thisAddress, ctx[CpuRegister.Rsi]);
ctx[CpuRegister.Rax] = thisAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static void TraceJson(string operation, ulong thisAddress, ulong argument)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_JSON"), "1", StringComparison.Ordinal))
@@ -80,8 +80,14 @@ public static class KernelAprCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
// Completion output was written when the command was submitted.
TraceApr(ctx, "wait", submissionId, submission.CommandBuffer, waitArg1, waitArg2);
var resultAddress = ResolveWaitResultAddress(waitArg1, waitArg2, submission.ResultAddress);
if (resultAddress != 0 && !TryWriteAprResult(ctx, resultAddress))
{
TraceAprWaitFailure(ctx, "wait_result_fault", submissionId, submission.CommandBuffer, waitArg1, waitArg2);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceApr(ctx, "wait", submissionId, submission.CommandBuffer, waitArg1, resultAddress);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -143,19 +149,6 @@ 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)];
@@ -163,6 +156,27 @@ public static class KernelAprCompatExports
return ctx.Memory.TryWrite(resultAddress, result);
}
private static ulong ResolveWaitResultAddress(ulong waitArg1, ulong waitArg2, ulong submittedResultAddress)
{
if (waitArg2 == 0)
{
return submittedResultAddress;
}
if (IsAmprCompletionToken(waitArg1) && waitArg2 <= 0xFFFF)
{
return submittedResultAddress;
}
return waitArg2;
}
private static bool IsAmprCompletionToken(ulong value)
{
var tag = value >> 56;
return tag is 0x0C or 0x10;
}
private static void TraceApr(
CpuContext ctx,
string operation,
@@ -11,7 +11,6 @@ public static class KernelEventQueueCompatExports
{
private const int KernelEventSize = 0x20;
public const short KernelEventFilterGraphics = -14;
public const short KernelEventFilterUser = -11;
public const short KernelEventFilterAmpr = -16;
public const short KernelEventFilterAmprSystem = -17;
@@ -90,16 +89,8 @@ public static class KernelEventQueueCompatExports
LibraryName = "libKernel")]
public static int KernelAddUserEventEdge(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var registered = RegisterEvent(
handle,
ctx[CpuRegister.Rsi],
KernelEventFilterUser,
0);
TraceEventQueue(ctx, "add_user_edge", handle);
return registered
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
TraceEventQueue(ctx, "add_user_edge", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
@@ -109,16 +100,8 @@ public static class KernelEventQueueCompatExports
LibraryName = "libKernel")]
public static int KernelAddUserEvent(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var registered = RegisterEvent(
handle,
ctx[CpuRegister.Rsi],
KernelEventFilterUser,
0);
TraceEventQueue(ctx, "add_user", handle);
return registered
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
TraceEventQueue(ctx, "add_user", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
@@ -128,15 +111,8 @@ public static class KernelEventQueueCompatExports
LibraryName = "libKernel")]
public static int KernelDeleteUserEvent(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var deleted = DeleteRegisteredEvent(
handle,
ctx[CpuRegister.Rsi],
KernelEventFilterUser);
TraceEventQueue(ctx, "delete_user", handle);
return deleted
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
TraceEventQueue(ctx, "delete_user", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
@@ -146,18 +122,8 @@ public static class KernelEventQueueCompatExports
LibraryName = "libKernel")]
public static int KernelTriggerUserEvent(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var triggered = TriggerRegisteredEvent(
handle,
ctx[CpuRegister.Rsi],
KernelEventFilterUser,
flags: 0x21,
fflags: 0,
data: ctx[CpuRegister.Rdx]);
TraceEventQueue(ctx, "trigger_user", handle);
return triggered
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
TraceEventQueue(ctx, "trigger_user", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
@@ -509,43 +475,6 @@ public static class KernelEventQueueCompatExports
return triggeredCount;
}
private static bool TriggerRegisteredEvent(
ulong handle,
ulong ident,
short filter,
ushort flags,
uint fflags,
ulong data)
{
lock (_eventQueueGate)
{
if (!_registeredEvents.TryGetValue(handle, out var registrations) ||
!registrations.TryGetValue((ident, filter), out var registration))
{
return false;
}
if (!_pendingEvents.TryGetValue(handle, out var queue))
{
queue = new LinkedList<KernelQueuedEvent>();
_pendingEvents[handle] = queue;
}
QueueOrUpdateEvent(
queue,
new KernelQueuedEvent(
registration.Ident,
registration.Filter,
flags,
fflags,
data,
registration.UserData));
}
WakeEventQueue(handle);
return true;
}
public static bool TriggerDisplayEvent(
ulong handle,
ulong ident,
@@ -727,6 +656,6 @@ public static class KernelEventQueueCompatExports
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out ulong returnRip);
Console.Error.WriteLine(
$"[LOADER][TRACE] equeue.{operation}: thread=0x{KernelPthreadState.GetCurrentThreadHandle():X16} handle=0x{handle:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} ret=0x{returnRip:X16}");
$"[LOADER][TRACE] equeue.{operation}: handle=0x{handle:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} ret=0x{returnRip:X16}");
}
}
+23 -53
View File
@@ -13,8 +13,6 @@ public static class KernelExports
private static readonly object _coredumpGate = new();
private static ulong _coredumpHandler;
private static ulong _coredumpHandlerContext;
private const uint Gen4CompiledSdkVersion = 0x05000000;
private const uint Gen5CompiledSdkVersion = 0x09000000;
private readonly record struct CxaDestructorEntry(
ulong Function,
@@ -28,24 +26,7 @@ public static class KernelExports
LibraryName = "libKernel")]
public static int KernelGetCompiledSdkVersion(CpuContext ctx)
{
var versionAddress = ctx[CpuRegister.Rdi];
if (versionAddress == 0)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var sdkVersion = ctx.TargetGeneration == Generation.Gen5
? Gen5CompiledSdkVersion
: Gen4CompiledSdkVersion;
if (!ctx.TryWriteUInt32(versionAddress, sdkVersion))
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
_ = ctx;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -71,20 +52,12 @@ public static class KernelExports
ExportName = "exit",
Target = Generation.Gen4 | Generation.Gen5,
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)
public static int Exit(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));
var status = unchecked((int)ctx[CpuRegister.Rdi]);
Console.Error.WriteLine($"[LOADER][INFO] exit(status={status})");
GuestThreadExecution.RequestCurrentEntryExit("exit", status);
ctx[CpuRegister.Rax] = unchecked((ulong)status);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -199,7 +172,11 @@ public static class KernelExports
ExportName = "_exit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int UnderscoreExit(CpuContext ctx) => RequestProcessExit(ctx, "_exit");
public static int UnderscoreExit(CpuContext ctx)
{
_ = ctx;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "Ac86z8q7T8A",
@@ -278,14 +255,20 @@ public static class KernelExports
ExportName = "pthread_create",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadCreate(CpuContext ctx) => PthreadCreate(ctx);
public static int PosixPthreadCreate(CpuContext ctx)
{
return PthreadCreate(ctx);
}
[SysAbiExport(
Nid = "Jmi+9w9u0E4",
ExportName = "pthread_create_name_np",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadCreateNameNp(CpuContext ctx) => PthreadCreate(ctx);
public static int PosixPthreadCreateNameNp(CpuContext ctx)
{
return PthreadCreate(ctx);
}
[SysAbiExport(
Nid = "3kg7rT0NQIs",
@@ -362,7 +345,10 @@ public static class KernelExports
ExportName = "pthread_join",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadJoin(CpuContext ctx) => PthreadJoin(ctx);
public static int PosixPthreadJoin(CpuContext ctx)
{
return PthreadJoin(ctx);
}
[SysAbiExport(
Nid = "wuCroIGjt2g",
@@ -451,20 +437,4 @@ public static class KernelExports
{
return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal);
}
private static int RequestProcessExit(CpuContext ctx, string syscallName)
{
var status = unchecked((int)ctx[CpuRegister.Rdi]);
Console.Error.WriteLine($"[LOADER][INFO] {syscallName}(status={status})");
GuestThreadExecution.RequestCurrentEntryExit(syscallName, status);
ctx[CpuRegister.Rax] = unchecked((ulong)status);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// 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.
}
File diff suppressed because it is too large Load Diff
@@ -3,7 +3,6 @@
using System.Collections.Concurrent;
using SharpEmu.HLE;
using SharpEmu.Libs.Fiber;
using System.Diagnostics.CodeAnalysis;
namespace SharpEmu.Libs.Kernel;
@@ -25,8 +24,6 @@ public static class KernelPthreadCompatExports
private static readonly object _stateGate = new();
private static readonly ConcurrentDictionary<ulong, PthreadMutexState> _mutexStates = new();
private static readonly ConcurrentDictionary<ulong, string> _mutexWakeKeys = new();
private static readonly ConcurrentDictionary<ulong, string> _condWakeKeys = new();
private static readonly Dictionary<ulong, PthreadMutexAttrState> _mutexAttrStates = new();
private static readonly Dictionary<ulong, PthreadCondState> _condStates = new();
private static readonly Dictionary<ulong, object> _onceGates = new();
@@ -38,15 +35,6 @@ public static class KernelPthreadCompatExports
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREAD_CONDS"), "1", StringComparison.Ordinal);
private static readonly HashSet<ulong>? _tracePthreadMutexFilter = ParseTraceAddressFilter(
Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREAD_MUTEX_FILTER"));
private static readonly bool _enableMutexLockBlocking =
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_MUTEX_LOCK_BLOCKING"), "1", StringComparison.Ordinal);
// On the outer class deliberately: a static on the nested state classes gives them a
// type initializer that first runs on a guest thread and fail-fasts the CLR.
private static long _nextMutexWakeId;
private static long _nextCondWakeId;
private static readonly ConcurrentDictionary<ulong, long> _posixTimedWaitCounts = new();
private static readonly ConcurrentDictionary<ulong, byte> _reportedContendedMutexes = new();
private sealed class PthreadMutexState
{
@@ -55,10 +43,6 @@ public static class KernelPthreadCompatExports
public int RecursionCount { get; set; }
public int Type { get; set; } = MutexTypeErrorCheck;
public int Protocol { get; set; }
// Keyed on state identity, not the resolved address (which can differ between a
// lock and its unlock for the same state, stranding the waiter).
public string WakeKey { get; } = "pthread_mutex#" + Interlocked.Increment(ref _nextMutexWakeId).ToString("X");
}
private sealed class PthreadMutexWaiter
@@ -72,23 +56,6 @@ public static class KernelPthreadCompatExports
public object SyncRoot { get; } = new();
public ulong SignalEpoch { get; set; }
public int Waiters { get; set; }
// See PthreadMutexState.WakeKey.
public string WakeKey { get; } = "pthread_cond#" + Interlocked.Increment(ref _nextCondWakeId).ToString("X");
// A signal with no waiter stays pending; the guest often signals before the wait.
public int PendingSignals { get; set; }
public bool TryConsumePendingSignal()
{
if (PendingSignals <= 0)
{
return false;
}
PendingSignals--;
return true;
}
}
private readonly record struct PthreadMutexAttrState(int Type, int Protocol);
@@ -140,32 +107,11 @@ public static class KernelPthreadCompatExports
LibraryName = "libKernel")]
public static int PthreadYield(CpuContext ctx)
{
GuestThreadExecution.Scheduler?.Pump(ctx, "scePthreadYield");
_ = ctx;
Thread.Yield();
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "GBUY7ywdULE",
ExportName = "scePthreadRename",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadRename(CpuContext ctx)
{
if (_tracePthreads)
{
var nameAddress = ctx[CpuRegister.Rsi];
var name = nameAddress != 0 &&
KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, nameAddress, 64, out var value)
? value
: "<unreadable>";
Console.Error.WriteLine(
$"[LOADER][TRACE] pthread.rename thread=0x{ctx[CpuRegister.Rdi]:X16} name=\"{name}\"");
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "EI-5-jlq2dE",
ExportName = "scePthreadGetthreadid",
@@ -347,104 +293,6 @@ public static class KernelPthreadCompatExports
LibraryName = "libKernel")]
public static int PthreadCondTimedwait(CpuContext ctx) => PthreadCondWaitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], timed: true, timeoutUsec: unchecked((uint)ctx[CpuRegister.Rdx]));
[SysAbiExport(
Nid = "27bAgiJmOh0",
ExportName = "pthread_cond_timedwait",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadCondTimedwait(CpuContext ctx)
{
var absoluteTimeAddress = ctx[CpuRegister.Rdx];
if (absoluteTimeAddress == 0 ||
!ctx.TryReadUInt64(absoluteTimeAddress, out var secondsRaw) ||
!ctx.TryReadUInt64(absoluteTimeAddress + sizeof(ulong), out var nanosecondsRaw))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var deadlineSeconds = unchecked((long)secondsRaw);
var deadlineNanoseconds = unchecked((long)nanosecondsRaw);
if (deadlineSeconds < 0 || deadlineNanoseconds is < 0 or >= 1_000_000_000)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
long nowSeconds;
long nowNanoseconds;
if (deadlineSeconds >= 1_000_000_000)
{
var now = DateTimeOffset.UtcNow;
nowSeconds = now.ToUnixTimeSeconds();
nowNanoseconds = (now.Ticks % TimeSpan.TicksPerSecond) * 100;
}
else
{
// Engine condition variables use monotonic deadlines.
KernelRuntimeCompatExports.GetProcessMonotonicTime(out nowSeconds, out nowNanoseconds);
}
var remainingNanoseconds =
((Int128)deadlineSeconds * 1_000_000_000 + deadlineNanoseconds) -
((Int128)nowSeconds * 1_000_000_000 + nowNanoseconds);
var timeoutUsec = remainingNanoseconds <= 0
? 0u
: (uint)Int128.Min(uint.MaxValue, (remainingNanoseconds + 999) / 1_000);
TracePosixTimedWait(
ctx,
deadlineSeconds,
deadlineNanoseconds,
nowSeconds,
nowNanoseconds,
remainingNanoseconds,
timeoutUsec);
return PthreadCondWaitCore(
ctx,
ctx[CpuRegister.Rdi],
ctx[CpuRegister.Rsi],
timed: true,
timeoutUsec,
posixResult: true);
}
private static void TracePosixTimedWait(
CpuContext ctx,
long deadlineSeconds,
long deadlineNanoseconds,
long nowSeconds,
long nowNanoseconds,
Int128 remainingNanoseconds,
uint timeoutUsec)
{
var condAddress = ctx[CpuRegister.Rdi];
var count = _posixTimedWaitCounts.AddOrUpdate(condAddress, 1, static (_, current) => current + 1);
if (count != 1 && count % 1_000_000 != 0)
{
return;
}
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out var returnRip);
Console.Error.WriteLine(
$"[LOADER][DIAG] pthread_cond_timedwait#{count}: thread=0x{KernelPthreadState.GetCurrentThreadHandle():X16} " +
$"cond=0x{condAddress:X16} mutex=0x{ctx[CpuRegister.Rsi]:X16} abstime=0x{ctx[CpuRegister.Rdx]:X16} " +
$"deadline={deadlineSeconds}.{deadlineNanoseconds:D9} now={nowSeconds}.{nowNanoseconds:D9} " +
$"remaining_ns={remainingNanoseconds} timeout_us={timeoutUsec} ret=0x{returnRip:X16}");
if (GuestThreadExecution.Scheduler is not { } scheduler)
{
return;
}
foreach (var snapshot in scheduler.SnapshotThreads())
{
Console.Error.WriteLine(
$"[LOADER][DIAG] guest_thread handle=0x{snapshot.ThreadHandle:X16} name='{snapshot.Name}' " +
$"state={snapshot.State} imports={snapshot.ImportCount} nid={snapshot.LastImportNid ?? "none"} " +
$"ret=0x{snapshot.LastReturnRip:X16} block={snapshot.BlockReason ?? "none"}");
}
}
[SysAbiExport(
Nid = "kDh-NfxgMtE",
ExportName = "scePthreadCondSignal",
@@ -479,6 +327,12 @@ public static class KernelPthreadCompatExports
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadCondSignal(CpuContext ctx) => PthreadCondSignalCore(ctx, ctx[CpuRegister.Rdi], broadcast: false);
[SysAbiExport(
Nid = "27bAgiJmOh0",
ExportName = "pthread_cond_timedwait",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadCondTimedwait(CpuContext ctx) => PthreadCondWaitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], timed: true, timeoutUsec: unchecked((uint)ctx[CpuRegister.Rdx]));
[SysAbiExport(
Nid = "m5-2bsNfv7s",
@@ -705,43 +559,42 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
}
// Normal/adaptive mutexes do not report EDEADLK on self-lock.
// Under the current single-host-thread guest execution model,
// treating them as nested ownership keeps init paths moving
// without turning a would-block path into a hard error.
state.RecursionCount++;
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
else
{
var ownedResult = tryOnly
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, ownedResult);
return ownedResult;
}
var ownedResult = tryOnly
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, ownedResult);
return ownedResult;
}
}
var acquired = state.Semaphore.Wait(0);
if (!acquired)
{
TraceContendedMutex(ctx, mutexAddress, resolvedAddress, state, currentThreadId);
var waiter = new PthreadMutexWaiter { ThreadId = currentThreadId };
// Fibers retain the synchronous fallback to preserve switch state.
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
var canCooperativelyBlock = _enableMutexLockBlocking || currentFiber == 0;
if (canCooperativelyBlock &&
!tryOnly &&
// Guest-thread blocking for pthread_mutex_lock is currently disabled.
// Demon's Souls deadlocks during PS5SyncEvent initialization.
/* var waiter = new PthreadMutexWaiter { ThreadId = currentThreadId };
if (!tryOnly &&
GuestThreadExecution.IsGuestThread &&
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"pthread_mutex_lock",
state.WakeKey,
GetMutexWakeKey(resolvedAddress),
() => CompleteBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter),
() => TryReserveBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter)))
{
TracePthreadMutex(ctx, "lock-block", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
} */
if (!tryOnly)
{
@@ -758,7 +611,6 @@ public static class KernelPthreadCompatExports
lock (state)
{
DetectMutexDoubleOwner(resolvedAddress, state, currentThreadId, "lock");
state.OwnerThreadId = currentThreadId;
state.RecursionCount = 1;
}
@@ -767,53 +619,6 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static void TraceContendedMutex(
CpuContext ctx,
ulong mutexAddress,
ulong resolvedAddress,
PthreadMutexState state,
ulong currentThreadId)
{
if (!_reportedContendedMutexes.TryAdd(resolvedAddress, 0))
{
return;
}
ulong ownerThreadId;
int recursionCount;
int type;
lock (state)
{
ownerThreadId = state.OwnerThreadId;
recursionCount = state.RecursionCount;
type = state.Type;
}
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out var returnRip);
Console.Error.WriteLine(
$"[LOADER][DIAG] pthread_mutex_contended: waiter=0x{currentThreadId:X16} owner=0x{ownerThreadId:X16} " +
$"mutex=0x{mutexAddress:X16} resolved=0x{resolvedAddress:X16} recursion={recursionCount} " +
$"type={type} semaphore_count={state.Semaphore.CurrentCount} ret=0x{returnRip:X16}");
if (GuestThreadExecution.Scheduler is not { } scheduler)
{
return;
}
foreach (var snapshot in scheduler.SnapshotThreads())
{
if (snapshot.ThreadHandle != currentThreadId && snapshot.ThreadHandle != ownerThreadId)
{
continue;
}
Console.Error.WriteLine(
$"[LOADER][DIAG] pthread_mutex_party handle=0x{snapshot.ThreadHandle:X16} name='{snapshot.Name}' " +
$"state={snapshot.State} imports={snapshot.ImportCount} nid={snapshot.LastImportNid ?? "none"} " +
$"ret=0x{snapshot.LastReturnRip:X16} block={snapshot.BlockReason ?? "none"}");
}
}
private static int PthreadMutexUnlockCore(CpuContext ctx, ulong mutexAddress, bool requireOwner)
{
if (mutexAddress == 0)
@@ -828,24 +633,16 @@ public static class KernelPthreadCompatExports
}
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
var lenientOwnership = state.Type is MutexTypeNormal or MutexTypeAdaptiveNp;
var shouldRelease = false;
lock (state)
{
if (state.RecursionCount <= 0)
{
if (lenientOwnership)
{
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (requireOwner && !lenientOwnership && state.OwnerThreadId != currentThreadId)
if (requireOwner && state.OwnerThreadId != currentThreadId)
{
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
@@ -864,15 +661,12 @@ public static class KernelPthreadCompatExports
try
{
state.Semaphore.Release();
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(state.WakeKey, 1);
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetMutexWakeKey(resolvedAddress), 1);
}
catch (SemaphoreFullException)
{
if (!lenientOwnership)
{
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
}
@@ -1193,13 +987,6 @@ public static class KernelPthreadCompatExports
lock (_stateGate)
{
if (_condStates.TryGetValue(condAddress, out var raced))
{
resolvedAddress = condAddress;
state = raced;
return true;
}
_condStates[condAddress] = createdState;
_condStates[handle] = createdState;
}
@@ -1296,41 +1083,27 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static int PthreadCondWaitCore(
CpuContext ctx,
ulong condAddress,
ulong mutexAddress,
bool timed,
uint timeoutUsec = 0,
bool posixResult = false)
private static int PthreadCondWaitCore(CpuContext ctx, ulong condAddress, ulong mutexAddress, bool timed, uint timeoutUsec = 0)
{
if (condAddress == 0 || mutexAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryResolveCondState(ctx, condAddress, createIfZero: true, out var resolvedCondAddress, out var state))
if (!TryResolveCondState(ctx, condAddress, createIfZero: true, out _, out var state))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
var waitResult = (int)OrbisGen2Result.ORBIS_GEN2_OK;
var spuriousWake = false;
var releasedRecursion = 0;
lock (state.SyncRoot)
{
state.Waiters++;
var observedEpoch = state.SignalEpoch;
var consumedPendingSignal = state.TryConsumePendingSignal();
TracePthreadCond(
consumedPendingSignal ? "wait-enter-pending" : "wait-enter",
condAddress,
mutexAddress,
state,
timed,
waitResult);
TracePthreadCond("wait-enter", condAddress, mutexAddress, state, timed, waitResult);
var unlockResult = PthreadMutexFullUnlockForCondWait(ctx, mutexAddress, out releasedRecursion);
var unlockResult = PthreadMutexUnlockCore(ctx, mutexAddress, requireOwner: true);
if (unlockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
state.Waiters--;
@@ -1338,36 +1111,10 @@ public static class KernelPthreadCompatExports
return unlockResult;
}
if (consumedPendingSignal)
{
state.Waiters = Math.Max(0, state.Waiters - 1);
TracePthreadCond("wait-wake-pending", condAddress, mutexAddress, state, timed, waitResult);
// Relock outside SyncRoot to preserve lock ordering.
Monitor.Exit(state.SyncRoot);
try
{
var pendingLockResult = PthreadMutexRelockForCondWait(ctx, mutexAddress, releasedRecursion);
return pendingLockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK ? pendingLockResult : waitResult;
}
finally
{
// Balance the surrounding lock statement.
Monitor.Enter(state.SyncRoot);
}
}
var scheduler = GuestThreadExecution.Scheduler;
if (GuestThreadExecution.IsGuestThread &&
GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
timed ? "pthread_cond_timedwait" : "pthread_cond_wait",
state.WakeKey,
() => ResumePthreadCondWait(ctx, condAddress, mutexAddress, state, observedEpoch, timed, releasedRecursion, posixResult),
() => state.SignalEpoch != observedEpoch,
timed ? GuestThreadExecution.ComputeDeadlineTimestamp(GetCondWaitTimeout(timeoutUsec)) : 0))
if (!timed && GuestThreadExecution.RequestCurrentThreadBlock("pthread_cond_wait"))
{
TracePthreadCond(timed ? "wait-block-timed" : "wait-block", condAddress, mutexAddress, state, timed, waitResult);
TracePthreadCond("wait-block", condAddress, mutexAddress, state, timed, waitResult);
return waitResult;
}
@@ -1390,21 +1137,6 @@ public static class KernelPthreadCompatExports
{
if (!Monitor.Wait(state.SyncRoot, GetCondSpuriousWakeTimeout()))
{
if (scheduler is not null && !GuestThreadExecution.IsGuestThread)
{
Monitor.Exit(state.SyncRoot);
try
{
scheduler.Pump(ctx, "pthread_cond_wait");
}
finally
{
Monitor.Enter(state.SyncRoot);
}
continue;
}
spuriousWake = true;
break;
}
@@ -1431,7 +1163,7 @@ public static class KernelPthreadCompatExports
waitResult);
}
var lockResult = PthreadMutexRelockForCondWait(ctx, mutexAddress, releasedRecursion);
var lockResult = PthreadMutexLockCore(ctx, mutexAddress, tryOnly: false);
if (lockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
TracePthreadCond("wait-relock-fail", condAddress, mutexAddress, state, timed, lockResult);
@@ -1447,7 +1179,7 @@ public static class KernelPthreadCompatExports
state,
timed,
waitResult);
return TranslatePthreadResult(waitResult, posixResult);
return waitResult;
}
private static int PthreadCondSignalCore(CpuContext ctx, ulong condAddress, bool broadcast)
@@ -1457,19 +1189,16 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryResolveCondState(ctx, condAddress, createIfZero: true, out var resolvedCondAddress, out var state))
if (!TryResolveCondState(ctx, condAddress, createIfZero: true, out _, out var state))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
var shouldWakeScheduler = false;
lock (state.SyncRoot)
{
// Advance the epoch even with no waiter registered — it's the wait predicate.
state.SignalEpoch++;
if (state.Waiters > 0)
{
shouldWakeScheduler = true;
state.SignalEpoch++;
if (broadcast)
{
Monitor.PulseAll(state.SyncRoot);
@@ -1479,161 +1208,15 @@ public static class KernelPthreadCompatExports
Monitor.Pulse(state.SyncRoot);
}
}
else
{
state.PendingSignals++;
}
TracePthreadCond(broadcast ? "broadcast" : "signal", condAddress, mutexAddress: 0, state, timed: false, (int)OrbisGen2Result.ORBIS_GEN2_OK);
}
if (shouldWakeScheduler)
{
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(
state.WakeKey,
broadcast ? int.MaxValue : 1);
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static int ResumePthreadCondWait(
CpuContext ctx,
ulong condAddress,
ulong mutexAddress,
PthreadCondState state,
ulong observedEpoch,
bool timed,
int releasedRecursion,
bool posixResult)
{
var waitResult = (int)OrbisGen2Result.ORBIS_GEN2_OK;
lock (state.SyncRoot)
{
state.Waiters = Math.Max(0, state.Waiters - 1);
if (timed && state.SignalEpoch == observedEpoch)
{
waitResult = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
}
TracePthreadCond(
waitResult == (int)OrbisGen2Result.ORBIS_GEN2_OK ? "wait-resume" : "wait-resume-timeout",
condAddress,
mutexAddress,
state,
timed,
waitResult);
}
var lockResult = PthreadMutexRelockForCondWait(ctx, mutexAddress, releasedRecursion);
return TranslatePthreadResult(
lockResult == (int)OrbisGen2Result.ORBIS_GEN2_OK ? waitResult : lockResult,
posixResult);
}
private static int TranslatePthreadResult(int result, bool posixResult)
{
if (!posixResult || result == (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
return result;
}
return result switch
{
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED => 1, // EPERM
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND => 2, // ENOENT
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT => 22, // EINVAL
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK => 11, // EDEADLK
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED => 13, // EACCES
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY => 16, // EBUSY
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN => 35, // EAGAIN
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT => 60, // ETIMEDOUT
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_CANCELED => 85, // ECANCELED
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT => 14, // EFAULT
_ => result
};
}
private static string GetCondWakeKey(ulong resolvedCondAddress) =>
_condWakeKeys.GetOrAdd(resolvedCondAddress, static address => string.Create(
31,
address,
static (destination, value) =>
{
"pthread_cond:0x".AsSpan().CopyTo(destination);
_ = value.TryFormat(destination[15..], out _, "X16");
}));
private static int PthreadMutexFullUnlockForCondWait(CpuContext ctx, ulong mutexAddress, out int releasedRecursion)
{
releasedRecursion = 0;
if (!TryResolveMutexState(ctx, mutexAddress, createIfZero: false, out _, out var state))
{
return PthreadMutexUnlockCore(ctx, mutexAddress, requireOwner: true);
}
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
lock (state)
{
if (state.RecursionCount <= 0 || state.OwnerThreadId != currentThreadId)
{
return PthreadMutexUnlockCore(ctx, mutexAddress, requireOwner: true);
}
releasedRecursion = state.RecursionCount;
state.RecursionCount = 1;
}
return PthreadMutexUnlockCore(ctx, mutexAddress, requireOwner: true);
}
private static int PthreadMutexRelockForCondWait(CpuContext ctx, ulong mutexAddress, int releasedRecursion)
{
var result = PthreadMutexLockCore(ctx, mutexAddress, tryOnly: false);
if (result != (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
return result;
}
if (releasedRecursion > 1 &&
TryResolveMutexState(ctx, mutexAddress, createIfZero: false, out _, out var state))
{
lock (state)
{
if (state.OwnerThreadId == KernelPthreadState.GetCurrentThreadHandle())
{
state.RecursionCount = releasedRecursion;
}
}
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
public static string? DumpMutexStateForStall(ulong mutexAddress)
{
if (!_mutexStates.TryGetValue(mutexAddress, out var state))
{
return null;
}
var waiterThreads = string.Join(",", _mutexStates
.Where(pair => ReferenceEquals(pair.Value, state))
.Select(pair => $"0x{pair.Key:X}"));
return $"mutex=0x{mutexAddress:X16} owner=0x{state.OwnerThreadId:X} recursion={state.RecursionCount} " +
$"type={state.Type} semaphore_count={state.Semaphore.CurrentCount} aliases=[{waiterThreads}]";
}
private static string GetMutexWakeKey(ulong resolvedMutexAddress) =>
_mutexWakeKeys.GetOrAdd(resolvedMutexAddress, static address => string.Create(
32,
address,
static (destination, value) =>
{
"pthread_mutex:0x".AsSpan().CopyTo(destination);
_ = value.TryFormat(destination[16..], out _, "X16");
}));
$"pthread_mutex:0x{resolvedMutexAddress:X16}";
private static bool TryReserveBlockedMutexLock(
CpuContext ctx,
@@ -1650,13 +1233,6 @@ public static class KernelPthreadCompatExports
return false;
}
if (!state.Semaphore.Wait(0))
{
TracePthreadMutex(ctx, "lock-reserve-busy", mutexAddress, resolvedAddress, state, waiter.ThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
return false;
}
DetectMutexDoubleOwner(resolvedAddress, state, waiter.ThreadId, "reserve");
state.OwnerThreadId = waiter.ThreadId;
state.RecursionCount = 1;
Interlocked.Exchange(ref waiter.Reserved, 1);
@@ -1688,7 +1264,6 @@ public static class KernelPthreadCompatExports
lock (state)
{
DetectMutexDoubleOwner(resolvedAddress, state, currentThreadId, "resume");
state.OwnerThreadId = currentThreadId;
state.RecursionCount = 1;
}
@@ -1697,19 +1272,6 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// A nonzero foreign prior owner means two guest threads are in the same critical
// section. Call while holding lock(state).
private static void DetectMutexDoubleOwner(ulong resolvedAddress, PthreadMutexState state, ulong currentThreadId, string site)
{
var priorOwner = state.OwnerThreadId;
if (priorOwner != 0 && priorOwner != currentThreadId)
{
Console.Error.WriteLine(
$"[LOADER][ERROR] MUTEX DOUBLE-OWNER at {site}: resolved=0x{resolvedAddress:X} acquirer=0x{currentThreadId:X} " +
$"prior_owner=0x{priorOwner:X} recursion={state.RecursionCount} type={state.Type} sem_count={state.Semaphore.CurrentCount}");
}
}
private static TimeSpan GetCondWaitTimeout(uint timeoutUsec)
{
if (timeoutUsec == 0)
@@ -1717,10 +1279,7 @@ public static class KernelPthreadCompatExports
return TimeSpan.Zero;
}
// Round positive sub-millisecond waits to the host sleep quantum.
return timeoutUsec < 1_000
? TimeSpan.FromMilliseconds(1)
: TimeSpan.FromTicks((long)timeoutUsec * 10L);
return TimeSpan.FromTicks((long)timeoutUsec * 10L);
}
private static TimeSpan GetCondSpuriousWakeTimeout()
@@ -1850,8 +1409,7 @@ public static class KernelPthreadCompatExports
$"[LOADER][TRACE] pthread_{operation}: mutex=0x{mutexAddress:X16} resolved=0x{resolvedAddress:X16} " +
$"guest[0]=0x{guestWord0:X16} guest[8]=0x{guestWord1:X16} " +
$"current=0x{currentThreadId:X16} owner=0x{(state?.OwnerThreadId ?? 0):X16} " +
$"recursion={(state?.RecursionCount ?? 0)} type={(state?.Type ?? 0)} result=0x{unchecked((uint)result):X8} " +
$"guest_thread=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} managed={Environment.CurrentManagedThreadId}");
$"recursion={(state?.RecursionCount ?? 0)} type={(state?.Type ?? 0)} result=0x{unchecked((uint)result):X8}");
}
private static void TracePthreadCond(string operation, ulong condAddress, ulong mutexAddress, PthreadCondState? state, bool timed, int result)
@@ -1862,8 +1420,7 @@ public static class KernelPthreadCompatExports
}
Console.Error.WriteLine(
$"[LOADER][TRACE] pthread_cond_{operation}: thread=0x{KernelPthreadState.GetCurrentThreadHandle():X16} " +
$"cond=0x{condAddress:X16} key={state?.WakeKey ?? "none"} mutex=0x{mutexAddress:X16} " +
$"[LOADER][TRACE] pthread_cond_{operation}: cond=0x{condAddress:X16} mutex=0x{mutexAddress:X16} " +
$"waiters={(state?.Waiters ?? 0)} epoch=0x{(state?.SignalEpoch ?? 0):X} timed={timed} result=0x{unchecked((uint)result):X8}");
}
@@ -31,8 +31,6 @@ public static class KernelPthreadExtendedCompatExports
private static long _nextSyntheticRwlockHandleId = 1;
private static long _nextSyntheticPthreadAttrHandleId = 1;
private static long _nextSyntheticRwlockAttrHandleId = 1;
private static readonly bool _strictRwlockWriterPreference =
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_STRICT_RWLOCK_WRITER_PREFERENCE"), "1", StringComparison.Ordinal);
private static readonly ConcurrentDictionary<ulong, ConcurrentDictionary<int, ulong>> _threadLocalSpecific = new();
@@ -87,23 +85,14 @@ public static class KernelPthreadExtendedCompatExports
public PthreadAttrState Attributes { get; set; } = PthreadAttrState.Default;
}
// On the outer class deliberately: a static on the nested state class gives it a type
// initializer that first runs on a guest thread and fail-fasts the CLR.
private static long _nextRwlockWakeId;
private sealed class PthreadRwlockState
{
public object SyncRoot { get; } = new();
public Dictionary<ulong, int> ReaderCounts { get; } = new();
public Dictionary<ulong, int> CompatWriterCounts { get; } = new();
public int ReaderTotalCount { get; set; }
public int CompatWriterTotalCount { get; set; }
public ulong WriterThreadId { get; set; }
public int WaitingWriters { get; set; }
// See PthreadMutexState.WakeKey.
public string WakeKey { get; } = "pthread_rwlock#" + Interlocked.Increment(ref _nextRwlockWakeId).ToString("X");
public int GetReaderCount(ulong threadId)
{
return ReaderCounts.TryGetValue(threadId, out var count) ? count : 0;
@@ -116,33 +105,6 @@ public static class KernelPthreadExtendedCompatExports
ReaderTotalCount++;
}
public void AddCompatWriter(ulong threadId)
{
CompatWriterCounts.TryGetValue(threadId, out var currentCount);
CompatWriterCounts[threadId] = currentCount + 1;
CompatWriterTotalCount++;
}
public bool RemoveCompatWriter(ulong threadId)
{
if (!CompatWriterCounts.TryGetValue(threadId, out var currentCount) || currentCount <= 0)
{
return false;
}
if (currentCount == 1)
{
CompatWriterCounts.Remove(threadId);
}
else
{
CompatWriterCounts[threadId] = currentCount - 1;
}
CompatWriterTotalCount = Math.Max(0, CompatWriterTotalCount - 1);
return true;
}
public bool RemoveReader(ulong threadId)
{
if (!ReaderCounts.TryGetValue(threadId, out var currentCount) || currentCount <= 0)
@@ -973,7 +935,7 @@ public static class KernelPthreadExtendedCompatExports
lock (state.SyncRoot)
{
if (state.WriterThreadId != 0 || state.ReaderTotalCount != 0 || state.WaitingWriters != 0 || state.CompatWriterTotalCount != 0)
if (state.WriterThreadId != 0 || state.ReaderTotalCount != 0 || state.WaitingWriters != 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
}
@@ -1041,7 +1003,7 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: false, out var resolvedAddress, out var rwlock))
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: false, out _, out var rwlock))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
@@ -1052,11 +1014,7 @@ public static class KernelPthreadExtendedCompatExports
{
lock (rwlock.SyncRoot)
{
if (rwlock.RemoveCompatWriter(currentThreadId))
{
Monitor.PulseAll(rwlock.SyncRoot);
}
else if (rwlock.WriterThreadId == currentThreadId)
if (rwlock.WriterThreadId == currentThreadId)
{
rwlock.WriterThreadId = 0;
Monitor.PulseAll(rwlock.SyncRoot);
@@ -1079,7 +1037,6 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
}
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(rwlock.WakeKey);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -1270,7 +1227,7 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: true, out var resolvedAddress, out var rwlock))
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: true, out _, out var rwlock))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
@@ -1285,60 +1242,20 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
}
if (rwlock.CompatWriterCounts.GetValueOrDefault(currentThreadId) > 0)
{
rwlock.AddCompatWriter(currentThreadId);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (GuestThreadExecution.IsGuestThread &&
!_strictRwlockWriterPreference &&
rwlock.WriterThreadId == 0 &&
rwlock.ReaderTotalCount == 0 &&
rwlock.CompatWriterTotalCount == 0)
{
rwlock.AddCompatWriter(currentThreadId);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (rwlock.WriterThreadId == 0 && rwlock.ReaderTotalCount == 0 && rwlock.CompatWriterTotalCount == 0)
{
DetectRwlockWriterConflict(resolvedAddress, rwlock, currentThreadId, "wrlock");
rwlock.WriterThreadId = currentThreadId;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
rwlock.WaitingWriters++;
var transferredToScheduler = false;
try
{
if (GuestThreadExecution.IsGuestThread &&
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"pthread_rwlock_wrlock",
rwlock.WakeKey,
static () => (int)OrbisGen2Result.ORBIS_GEN2_OK,
() => TryAcquireBlockedRwlock(rwlock, currentThreadId, write: true)))
{
transferredToScheduler = true;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
while (rwlock.WriterThreadId != 0 || rwlock.ReaderTotalCount != 0 || rwlock.CompatWriterTotalCount != 0)
while (rwlock.WriterThreadId != 0 || rwlock.ReaderTotalCount != 0)
{
Monitor.Wait(rwlock.SyncRoot);
}
rwlock.WriterThreadId = currentThreadId;
}
finally
{
if (!transferredToScheduler)
{
rwlock.WaitingWriters = Math.Max(0, rwlock.WaitingWriters - 1);
}
rwlock.WaitingWriters--;
}
rwlock.WriterThreadId = currentThreadId;
}
else
{
@@ -1347,30 +1264,12 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
}
while (ReaderMustWaitForRwlock(rwlock, currentThreadId))
while (rwlock.WriterThreadId != 0 ||
(rwlock.WaitingWriters > 0 && rwlock.GetReaderCount(currentThreadId) == 0))
{
if (GuestThreadExecution.IsGuestThread &&
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"pthread_rwlock_rdlock",
rwlock.WakeKey,
static () => (int)OrbisGen2Result.ORBIS_GEN2_OK,
() => TryAcquireBlockedRwlock(rwlock, currentThreadId, write: false)))
{
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
Monitor.Wait(rwlock.SyncRoot);
}
if (rwlock.WriterThreadId != 0 ||
rwlock.CompatWriterTotalCount > rwlock.CompatWriterCounts.GetValueOrDefault(currentThreadId))
{
Console.Error.WriteLine(
$"[LOADER][ERROR] RWLOCK READER/WRITER COEXIST: resolved=0x{resolvedAddress:X} reader=0x{currentThreadId:X} " +
$"writer=0x{rwlock.WriterThreadId:X} compat_total={rwlock.CompatWriterTotalCount} readers_total={rwlock.ReaderTotalCount}");
}
rwlock.AddReader(currentThreadId);
}
}
@@ -1378,86 +1277,6 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool TryAcquireBlockedRwlock(PthreadRwlockState rwlock, ulong currentThreadId, bool write)
{
lock (rwlock.SyncRoot)
{
if (write)
{
if (rwlock.WriterThreadId != 0 || rwlock.ReaderTotalCount != 0 || rwlock.CompatWriterTotalCount != 0)
{
return false;
}
DetectRwlockWriterConflict(0, rwlock, currentThreadId, "wrlock-resume");
rwlock.WriterThreadId = currentThreadId;
rwlock.WaitingWriters = Math.Max(0, rwlock.WaitingWriters - 1);
return true;
}
if (ReaderMustWaitForRwlock(rwlock, currentThreadId))
{
return false;
}
rwlock.AddReader(currentThreadId);
return true;
}
}
// Call while holding lock(rwlock.SyncRoot): an existing reader/writer here means a
// writer would share the rwlock with another holder — a data race.
private static void DetectRwlockWriterConflict(ulong resolvedAddress, PthreadRwlockState rwlock, ulong currentThreadId, string site)
{
if (rwlock.WriterThreadId != 0 ||
rwlock.ReaderTotalCount != 0 ||
rwlock.CompatWriterTotalCount > rwlock.CompatWriterCounts.GetValueOrDefault(currentThreadId))
{
Console.Error.WriteLine(
$"[LOADER][ERROR] RWLOCK WRITER CONFLICT at {site}: resolved=0x{resolvedAddress:X} writer=0x{currentThreadId:X} " +
$"existing_writer=0x{rwlock.WriterThreadId:X} readers_total={rwlock.ReaderTotalCount} compat_total={rwlock.CompatWriterTotalCount}");
}
}
private static bool ReaderMustWaitForRwlock(PthreadRwlockState rwlock, ulong currentThreadId)
{
if (rwlock.WriterThreadId != 0)
{
return true;
}
if (rwlock.CompatWriterTotalCount > rwlock.CompatWriterCounts.GetValueOrDefault(currentThreadId))
{
return true;
}
return rwlock.WaitingWriters > 0 &&
rwlock.GetReaderCount(currentThreadId) == 0;
}
private static string GetRwlockWakeKey(ulong rwlockAddress) => $"pthread_rwlock:0x{rwlockAddress:X16}";
public static string? DumpRwlockStateForStall(ulong rwlockAddress)
{
PthreadRwlockState? rwlock;
lock (_stateGate)
{
if (!_rwlockStates.TryGetValue(rwlockAddress, out rwlock))
{
return null;
}
}
lock (rwlock.SyncRoot)
{
var readers = string.Join(",", rwlock.ReaderCounts.Select(pair => $"0x{pair.Key:X}x{pair.Value}"));
var compatWriters = string.Join(",", rwlock.CompatWriterCounts.Select(pair => $"0x{pair.Key:X}x{pair.Value}"));
return $"rwlock=0x{rwlockAddress:X16} writer=0x{rwlock.WriterThreadId:X} waiting_writers={rwlock.WaitingWriters} " +
$"readers_total={rwlock.ReaderTotalCount} readers=[{readers}] " +
$"compat_writers_total={rwlock.CompatWriterTotalCount} compat_writers=[{compatWriters}]";
}
}
private static ulong ResolveRwlockHandle(CpuContext ctx, ulong rwlockAddress)
{
if (rwlockAddress == 0)
@@ -14,22 +14,6 @@ namespace SharpEmu.Libs.Kernel;
public static class KernelRuntimeCompatExports
{
internal const int ClockRealtime = 0;
internal const int ClockVirtual = 1;
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 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;
@@ -80,98 +64,6 @@ 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) => NanosleepCore(ctx, posix: false);
[SysAbiExport(
Nid = "yS8U2TGCe1A",
ExportName = "nanosleep",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixNanosleep(CpuContext ctx) => NanosleepCore(ctx, posix: true);
private static int NanosleepCore(CpuContext ctx, bool posix)
{
var requestAddress = ctx[CpuRegister.Rdi];
var remainAddress = ctx[CpuRegister.Rsi];
if (requestAddress == 0)
{
return NanosleepFailure(ctx, posix, Einval, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> timespecBuffer = stackalloc byte[16];
if (!ctx.Memory.TryRead(requestAddress, timespecBuffer))
{
return NanosleepFailure(ctx, posix, Efault, 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)
{
return NanosleepFailure(ctx, posix, Einval, 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, posix ? "nanosleep" : "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 int NanosleepFailure(CpuContext ctx, bool posix, int errnoValue, OrbisGen2Result sceResult)
{
if (posix)
{
TrySetErrno(ctx, errnoValue);
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
}
else
{
ctx[CpuRegister.Rax] = unchecked((ulong)errnoValue);
}
return (int)sceResult;
}
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",
@@ -213,13 +105,6 @@ public static class KernelRuntimeCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "QcteRwbsnV0",
ExportName = "usleep",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixUsleep(CpuContext ctx) => KernelUsleep(ctx);
private static void TraceUsleepSpin(CpuContext ctx, ulong micros)
{
if (!_traceUsleep)
@@ -301,16 +186,21 @@ public static class KernelRuntimeCompatExports
long seconds;
long nanoseconds;
if (!ResolveClockTime(clockId, out seconds, out nanoseconds))
if (clockId == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
var now = DateTimeOffset.UtcNow;
seconds = now.ToUnixTimeSeconds();
nanoseconds = (now.Ticks % TimeSpan.TicksPerSecond) * 100;
}
else
{
var elapsedTicks = Stopwatch.GetTimestamp() - _processStartCounter;
seconds = elapsedTicks / Stopwatch.Frequency;
nanoseconds = (elapsedTicks % Stopwatch.Frequency) * 1_000_000_000L / Stopwatch.Frequency;
}
Span<byte> timespecBuffer = stackalloc byte[16];
BinaryPrimitives.WriteInt64LittleEndian(timespecBuffer, seconds);
BinaryPrimitives.WriteInt64LittleEndian(timespecBuffer[sizeof(long)..], nanoseconds);
if (!ctx.Memory.TryWrite(timeAddress, timespecBuffer))
if (!ctx.TryWriteUInt64(timeAddress, unchecked((ulong)seconds)) ||
!ctx.TryWriteUInt64(timeAddress + sizeof(long), unchecked((ulong)nanoseconds)))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -335,11 +225,8 @@ public static class KernelRuntimeCompatExports
var now = DateTimeOffset.UtcNow;
var seconds = now.ToUnixTimeSeconds();
var microseconds = (now.Ticks % TimeSpan.TicksPerSecond) / 10;
Span<byte> timevalBuffer = stackalloc byte[16];
BinaryPrimitives.WriteInt64LittleEndian(timevalBuffer, seconds);
BinaryPrimitives.WriteInt64LittleEndian(timevalBuffer[sizeof(long)..], microseconds);
if (!ctx.Memory.TryWrite(timeAddress, timevalBuffer))
if (!ctx.TryWriteUInt64(timeAddress, unchecked((ulong)seconds)) ||
!ctx.TryWriteUInt64(timeAddress + sizeof(long), unchecked((ulong)microseconds)))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -361,28 +248,18 @@ public static class KernelRuntimeCompatExports
var seconds = now.ToUnixTimeSeconds();
var microseconds = (now.Ticks % TimeSpan.TicksPerSecond) / 10;
if (timeAddress != 0)
if (timeAddress != 0 &&
(!ctx.TryWriteUInt64(timeAddress, unchecked((ulong)seconds)) ||
!ctx.TryWriteUInt64(timeAddress + sizeof(long), unchecked((ulong)microseconds))))
{
Span<byte> timevalBuffer = stackalloc byte[16];
BinaryPrimitives.WriteInt64LittleEndian(timevalBuffer, seconds);
BinaryPrimitives.WriteInt64LittleEndian(timevalBuffer[sizeof(long)..], microseconds);
if (!ctx.Memory.TryWrite(timeAddress, timevalBuffer))
{
TrySetErrno(ctx, Efault);
return -1;
}
return -1;
}
if (timezoneAddress != 0)
if (timezoneAddress != 0 &&
(!ctx.TryWriteInt32(timezoneAddress, 0) ||
!ctx.TryWriteInt32(timezoneAddress + sizeof(int), 0)))
{
Span<byte> timezoneBuffer = stackalloc byte[8];
BinaryPrimitives.WriteInt32LittleEndian(timezoneBuffer, 0);
BinaryPrimitives.WriteInt32LittleEndian(timezoneBuffer[sizeof(int)..], 0);
if (!ctx.Memory.TryWrite(timezoneAddress, timezoneBuffer))
{
TrySetErrno(ctx, Efault);
return -1;
}
return -1;
}
ctx[CpuRegister.Rax] = 0;
@@ -431,18 +308,6 @@ public static class KernelRuntimeCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "HoLVWNanBBc",
ExportName = "getpid",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int GetProcessId(CpuContext ctx)
{
var processId = Environment.ProcessId;
ctx[CpuRegister.Rax] = unchecked((uint)processId);
return processId;
}
[SysAbiExport(
Nid = "fgxnMeTNUtY",
ExportName = "sceKernelGetProcessTimeCounter",
@@ -752,57 +617,6 @@ public static class KernelRuntimeCompatExports
return address != 0 && ctx.TryWriteInt32(address, value);
}
internal static bool ResolveClockTime(int clockId, out long seconds, out long nanoseconds)
{
switch (clockId)
{
case ClockRealtime:
case ClockRealtimePrecise:
case ClockRealtimeFast:
case ClockVirtual:
case ClockProf:
{
var now = DateTimeOffset.UtcNow;
seconds = now.ToUnixTimeSeconds();
nanoseconds = (now.Ticks % TimeSpan.TicksPerSecond) * 100;
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;
default:
seconds = 0;
nanoseconds = 0;
return false;
}
}
internal static void GetProcessMonotonicTime(out long seconds, out long nanoseconds)
{
var elapsedTicks = Stopwatch.GetTimestamp() - _processStartCounter;
seconds = elapsedTicks / Stopwatch.Frequency;
nanoseconds = (elapsedTicks % Stopwatch.Frequency) * 1_000_000_000L / Stopwatch.Frequency;
}
[SysAbiExport(
Nid = "bnZxYgAFeA0",
ExportName = "sceKernelGetSanitizerNewReplaceExternal",
@@ -1655,42 +1469,26 @@ public static class KernelRuntimeCompatExports
return false;
}
const int pageSize = 4096;
const int inlineChunkSize = 64;
Span<byte> buffer = stackalloc byte[Math.Min(maxLength, 512)];
var length = 0;
for (var offset = 0; offset < maxLength && length < buffer.Length;)
var bytes = new List<byte>(Math.Min(maxLength, 64));
Span<byte> one = stackalloc byte[1];
for (var i = 0; i < maxLength; i++)
{
var current = address + (ulong)offset;
var pageRemaining = pageSize - (int)(current & (pageSize - 1));
var chunkSize = Math.Min(
buffer.Length - length,
Math.Min(maxLength - offset, Math.Min(inlineChunkSize, pageRemaining)));
if (chunkSize <= 0)
if (!ctx.Memory.TryRead(address + (ulong)i, one))
{
return false;
}
var chunk = buffer.Slice(length, chunkSize);
if (!ctx.Memory.TryRead(current, chunk))
if (one[0] == 0)
{
return false;
}
var nulIndex = chunk.IndexOf((byte)0);
if (nulIndex >= 0)
{
value = Encoding.UTF8.GetString(buffer[..(length + nulIndex)]);
value = Encoding.UTF8.GetString(bytes.ToArray());
return true;
}
length += chunkSize;
offset += chunkSize;
bytes.Add(one[0]);
}
return false;
value = Encoding.UTF8.GetString(bytes.ToArray());
return true;
}
private static ulong GetTlsScratchAddress(CpuContext ctx, ulong offset)
@@ -1899,7 +1697,7 @@ public static class KernelRuntimeCompatExports
return null;
}
ReadOnlySpan<byte> stub = stackalloc byte[]
Span<byte> stub = stackalloc byte[]
{
0x0F, 0x31,
0x48, 0xC1, 0xE2, 0x20,
@@ -1907,14 +1705,7 @@ public static class KernelRuntimeCompatExports
0xC3,
};
unsafe
{
fixed (byte* src = stub)
{
Buffer.MemoryCopy(src, (void*)stubAddress, stub.Length, stub.Length);
}
}
Marshal.Copy(stub.ToArray(), 0, stubAddress, stub.Length);
return Marshal.GetDelegateForFunctionPointer<RdtscDelegate>(stubAddress);
}
catch
@@ -2,7 +2,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
using System.Diagnostics;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Kernel;
@@ -13,9 +12,6 @@ public static class KernelSemaphoreCompatExports
private static readonly ConcurrentDictionary<uint, KernelSemaphoreState> _semaphores = new();
private static int _nextSemaphoreHandle = 1;
[ThreadStatic]
private static int _semaPollBackoffCount;
private sealed class KernelSemaphoreState
{
public required string Name { get; init; }
@@ -23,23 +19,9 @@ public static class KernelSemaphoreCompatExports
public required int MaxCount { get; init; }
public int Count { get; set; }
public int WaitingThreads { get; set; }
public int CancelEpoch { get; set; }
public bool Deleted { get; set; }
public object Gate { get; } = new();
}
private sealed class SemaphoreWaiter
{
public required int NeedCount { get; init; }
public required int CancelEpochAtBlock { get; init; }
public bool Timed { get; init; }
// Written and read only under the owning semaphore's Gate.
public int? Result { get; set; }
}
private static string GetSemaphoreWakeKey(uint handle) => $"kernel_sema:0x{handle:X8}";
[SysAbiExport(
Nid = "188x57JYp0g",
ExportName = "sceKernelCreateSema",
@@ -76,27 +58,17 @@ public static class KernelSemaphoreCompatExports
handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
}
var state = new KernelSemaphoreState
_semaphores[handle] = new KernelSemaphoreState
{
Name = name,
InitialCount = initialCount,
MaxCount = maxCount,
Count = initialCount,
};
_semaphores[handle] = state;
if (!ctx.TryWriteUInt32(semaphoreAddress, handle))
{
_semaphores.TryRemove(handle, out _);
// Handles are sequential and guest-predictable, so a hostile guest can
// race a WaitSema onto the handle between publication above and this
// rollback. Strand-proof that waiter exactly like DeleteSema does.
lock (state.Gate)
{
state.Deleted = true;
}
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
@@ -125,7 +97,6 @@ public static class KernelSemaphoreCompatExports
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var pollTimedOut = false;
lock (semaphore.Gate)
{
if (semaphore.Count >= needCount)
@@ -137,82 +108,26 @@ public static class KernelSemaphoreCompatExports
if (timeoutAddress != 0)
{
if (!ctx.TryReadUInt32(timeoutAddress, out var timeoutMicros))
if (!ctx.TryReadUInt32(timeoutAddress, out _))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (timeoutMicros == 0)
{
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
pollTimedOut = true;
}
else
{
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromTicks((long)timeoutMicros * 10L));
var timedWaiter = new SemaphoreWaiter
{
NeedCount = needCount,
CancelEpochAtBlock = semaphore.CancelEpoch,
Timed = true,
};
if (GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitSema",
GetSemaphoreWakeKey(handle),
resumeHandler: () => CompleteBlockedTimedSemaWait(ctx, semaphore, timedWaiter, timeoutAddress, deadline),
wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, timedWaiter),
blockDeadlineTimestamp: deadline))
{
semaphore.WaitingThreads++;
TraceSemaphore($"wait-block-timed handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout_us={timeoutMicros} waiters={semaphore.WaitingThreads}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
// Host-owned threads cannot park in the guest scheduler; degrade to the
// immediate-timeout poll the callers already tolerate.
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
pollTimedOut = true;
}
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
if (!pollTimedOut)
if (!GuestThreadExecution.RequestCurrentThreadBlock(ctx, "sceKernelWaitSema"))
{
var waiter = new SemaphoreWaiter
{
NeedCount = needCount,
CancelEpochAtBlock = semaphore.CancelEpoch,
};
if (!GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitSema",
GetSemaphoreWakeKey(handle),
resumeHandler: () => CompleteBlockedSemaWait(semaphore, waiter),
wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, waiter)))
{
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
}
semaphore.WaitingThreads++;
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
}
}
GuestThreadExecution.Scheduler?.Pump(ctx, "sceKernelWaitSema");
if ((++_semaPollBackoffCount & 255) == 0)
{
Thread.Sleep(0);
semaphore.WaitingThreads++;
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
else
{
Thread.Yield();
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
[SysAbiExport(
@@ -278,14 +193,8 @@ public static class KernelSemaphoreCompatExports
semaphore.Count += signalCount;
TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
// Wake after releasing the gate (lock order: scheduler gate -> semaphore gate).
// Wake everyone; the wake handler consumes the count per waiter, so a waiter
// whose needCount exceeds the remaining count stays parked while a smaller
// waiter can proceed.
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -317,16 +226,10 @@ public static class KernelSemaphoreCompatExports
}
semaphore.Count = setCount < 0 ? semaphore.InitialCount : setCount;
semaphore.CancelEpoch++;
// WaitingThreads is NOT zeroed here: each canceled waiter decrements it
// exactly once in its wake handler. Zeroing here as well would double-count
// and silently absorb the increment of a waiter that parks between this
// gate release and the wake-all below.
semaphore.WaitingThreads = 0;
TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -342,120 +245,10 @@ public static class KernelSemaphoreCompatExports
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
// Delete succeeds even with blocked waiters; they wake with the deleted
// result (the SCE kernel wakes waiters with the EACCES-class code).
lock (semaphore.Gate)
{
semaphore.Deleted = true;
}
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
// Wake handler: runs under the scheduler's guest-thread gate (lock order:
// scheduler gate -> semaphore gate). Returns true iff the waiter has a final
// result and should be re-readied; false leaves it parked.
private static bool TryConsumeBlockedSemaWait(KernelSemaphoreState semaphore, SemaphoreWaiter waiter)
{
lock (semaphore.Gate)
{
return TryConsumeBlockedSemaWaitLocked(semaphore, waiter);
}
}
private static bool TryConsumeBlockedSemaWaitLocked(KernelSemaphoreState semaphore, SemaphoreWaiter waiter)
{
if (waiter.Result is not null)
{
return true;
}
if (semaphore.Deleted)
{
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
TraceSemaphore($"wake-deleted name='{semaphore.Name}' need={waiter.NeedCount}");
return true;
}
if (semaphore.CancelEpoch != waiter.CancelEpochAtBlock)
{
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_CANCELED;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
TraceSemaphore($"wake-canceled name='{semaphore.Name}' need={waiter.NeedCount}");
return true;
}
if (semaphore.Count >= waiter.NeedCount)
{
semaphore.Count -= waiter.NeedCount;
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_OK;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
TraceSemaphore($"wake-consume name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
return true;
}
return false;
}
// Resume handler: runs on the woken guest thread outside the scheduler gate;
// its return value becomes the guest's RAX for the resumed sceKernelWaitSema.
private static int CompleteBlockedSemaWait(KernelSemaphoreState semaphore, SemaphoreWaiter waiter)
{
lock (semaphore.Gate)
{
if (waiter.Result is null && !TryConsumeBlockedSemaWaitLocked(semaphore, waiter))
{
// Nothing readies a parked semaphore waiter without the wake handler
// resolving it, so reaching here means the scheduler contract changed.
Console.Error.WriteLine(
$"[LOADER][GAP] sema.resume-no-outcome name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count}");
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
}
return waiter.Result!.Value;
}
}
private static int CompleteBlockedTimedSemaWait(
CpuContext ctx,
KernelSemaphoreState semaphore,
SemaphoreWaiter waiter,
ulong timeoutAddress,
long deadlineTimestamp)
{
int result;
lock (semaphore.Gate)
{
if (waiter.Result is null && !TryConsumeBlockedSemaWaitLocked(semaphore, waiter))
{
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
TraceSemaphore($"wake-timeout name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
}
result = waiter.Result!.Value;
}
if (result == (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
var remainingTicks = deadlineTimestamp - Stopwatch.GetTimestamp();
var remainingMicros = remainingTicks <= 0
? 0u
: (uint)Math.Min(uint.MaxValue, remainingTicks / (double)Stopwatch.Frequency * 1_000_000d);
_ = ctx.TryWriteUInt32(timeoutAddress, remainingMicros);
}
else
{
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
}
return result;
}
private static void TraceSemaphore(string message)
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal))
@@ -1,558 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Net;
using System.Net.Sockets;
using System.Text;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Kernel;
internal static class KernelSocketCompatExports
{
private sealed class EmulatedSocketState
{
public TcpClient? Client;
public NetworkStream? Stream;
public IPAddress BoundAddress = IPAddress.Any;
public int BoundPort;
public bool Bound;
public bool Connected;
}
private static readonly object Gate = new();
private static readonly Dictionary<int, EmulatedSocketState> Sockets = new();
internal static bool IsEmulatedSocketFd(int fd)
{
lock (Gate)
{
return Sockets.ContainsKey(fd);
}
}
internal static bool TryCloseSocketFd(int fd)
{
lock (Gate)
{
if (!Sockets.Remove(fd, out var state))
{
return false;
}
DisposeEmulatedSocket(state);
return true;
}
}
internal static bool TryReadSocketFd(
CpuContext ctx,
int fd,
ulong bufferAddress,
int requested,
out ulong bytesRead,
out OrbisGen2Result error)
{
bytesRead = 0;
error = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
if (!TryGetEmulatedSocketState(fd, out var state) ||
state is null ||
!state.Connected ||
state.Stream is null)
{
return false;
}
var socketBuffer = GC.AllocateUninitializedArray<byte>(requested);
int socketRead;
try
{
socketRead = state.Stream.Read(socketBuffer, 0, requested);
}
catch (IOException)
{
return false;
}
if (socketRead > 0 && !ctx.Memory.TryWrite(bufferAddress, socketBuffer.AsSpan(0, socketRead)))
{
error = OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
return true;
}
bytesRead = unchecked((ulong)socketRead);
error = OrbisGen2Result.ORBIS_GEN2_OK;
return true;
}
internal static bool TryWriteSocketFd(
CpuContext ctx,
int fd,
byte[] payload,
out OrbisGen2Result error)
{
error = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
if (!TryGetEmulatedSocketState(fd, out var state) ||
state is null ||
!state.Connected ||
state.Stream is null)
{
return false;
}
try
{
state.Stream.Write(payload, 0, payload.Length);
state.Stream.Flush();
}
catch (IOException)
{
return false;
}
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NET"), "1", StringComparison.Ordinal))
{
Console.Out.Write(Encoding.UTF8.GetString(payload));
Console.Out.Flush();
}
error = OrbisGen2Result.ORBIS_GEN2_OK;
return true;
}
[SysAbiExport(
Nid = "TU-d9PfIHPM",
ExportName = "socket",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int Socket(CpuContext ctx)
{
var fd = KernelMemoryCompatExports.AllocateGuestFileDescriptor();
lock (Gate)
{
Sockets[fd] = new EmulatedSocketState();
}
ctx[CpuRegister.Rax] = unchecked((ulong)fd);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "XVL8So3QJUk",
ExportName = "connect",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int Connect(CpuContext ctx)
{
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
var sockaddrAddress = ctx[CpuRegister.Rsi];
var addrlen = unchecked((int)ctx[CpuRegister.Rdx]);
if (!TryGetEmulatedSocketState(fd, out _))
{
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (!TryParseGuestSockaddrIn(sockaddrAddress, addrlen, ctx, out var ipAddress, out var port))
{
LogNet($"connect sockaddr parse failed: fd={fd} addr=0x{sockaddrAddress:X} len={addrlen}");
RemoveEmulatedSocketFd(fd);
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
var redirectApplied = TryApplyNetRedirect(ref ipAddress);
if (redirectApplied)
{
LogNet($"connect redirect: fd={fd} ip={ipAddress} port={port}");
}
if (!IsGuestTcpOutboundAllowed(ipAddress, redirectApplied))
{
LogNet($"connect denied by outbound policy: fd={fd} ip={ipAddress} port={port}");
RemoveEmulatedSocketFd(fd);
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (!TryEstablishHostTcpConnection(ipAddress, port, out var client, out var stream))
{
LogNet($"connect failed: fd={fd} ip={ipAddress} port={port}");
RemoveEmulatedSocketFd(fd);
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
LogNet($"connect ok: fd={fd} ip={ipAddress} port={port}");
lock (Gate)
{
if (!Sockets.TryGetValue(fd, out var state) || state is null)
{
try { stream.Dispose(); } catch (IOException) { }
try { client.Dispose(); } catch (IOException) { }
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
DisposeEmulatedSocket(state);
state.Client = client;
state.Stream = stream;
state.Connected = true;
state.BoundAddress = ipAddress;
state.BoundPort = port;
state.Bound = true;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "KuOmgKoqCdY",
ExportName = "bind",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int Bind(CpuContext ctx)
{
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
var sockaddrAddress = ctx[CpuRegister.Rsi];
var addrlen = unchecked((int)ctx[CpuRegister.Rdx]);
if (!TryGetEmulatedSocketState(fd, out var state) || state is null)
{
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (!TryParseGuestSockaddrIn(sockaddrAddress, addrlen, ctx, out var ipAddress, out var port))
{
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
state.BoundAddress = ipAddress;
state.BoundPort = port;
state.Bound = true;
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "RenI1lL1WFk",
ExportName = "getsockname",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int Getsockname(CpuContext ctx)
{
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
var sockaddrAddress = ctx[CpuRegister.Rsi];
var addrlenAddress = ctx[CpuRegister.Rdx];
if (!TryGetEmulatedSocketState(fd, out var state) || state is null || !state.Bound)
{
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
Span<byte> addrlenBuffer = stackalloc byte[4];
if (!ctx.Memory.TryRead(addrlenAddress, addrlenBuffer))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var addrlen = BinaryPrimitives.ReadInt32LittleEndian(addrlenBuffer);
if (addrlen < 8)
{
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
Span<byte> sockaddr = stackalloc byte[16];
sockaddr[0] = 16;
sockaddr[1] = 2;
BinaryPrimitives.WriteUInt16BigEndian(sockaddr.Slice(2, 2), (ushort)state.BoundPort);
var addressBytes = state.BoundAddress.GetAddressBytes();
if (addressBytes.Length == 4)
{
addressBytes.CopyTo(sockaddr.Slice(4, 4));
}
var writeLength = Math.Min(addrlen, 16);
if (!ctx.Memory.TryWrite(sockaddrAddress, sockaddr.Slice(0, writeLength)))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
BinaryPrimitives.WriteInt32LittleEndian(addrlenBuffer, writeLength);
if (!ctx.Memory.TryWrite(addrlenAddress, addrlenBuffer))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "9oiX1kyeedA",
ExportName = "bzero",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int Bzero(CpuContext ctx)
{
var address = ctx[CpuRegister.Rdi];
var length = unchecked((int)ctx[CpuRegister.Rsi]);
if (length > 0 && address != 0)
{
var zeros = new byte[length];
if (!ctx.Memory.TryWrite(address, zeros))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "4n51s0zEf0c",
ExportName = "inet_pton",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int InetPton(CpuContext ctx)
{
var af = unchecked((int)ctx[CpuRegister.Rdi]);
var srcAddress = ctx[CpuRegister.Rsi];
var dstAddress = ctx[CpuRegister.Rdx];
if (af != 2 || srcAddress == 0 || dstAddress == 0)
{
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryReadCString(srcAddress, ctx, out var text) ||
!TryParseIpv4Address(text, out var octets))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
Span<byte> packed = stackalloc byte[4];
packed[0] = octets[0];
packed[1] = octets[1];
packed[2] = octets[2];
packed[3] = octets[3];
if (!ctx.Memory.TryWrite(dstAddress, packed))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 1;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "jogUIsOV3-U",
ExportName = "htons",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int Htons(CpuContext ctx)
{
var value = unchecked((ushort)ctx[CpuRegister.Rdi]);
var swapped = (ushort)(((value & 0x00FF) << 8) | ((value >> 8) & 0x00FF));
ctx[CpuRegister.Rax] = swapped;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool TryGetEmulatedSocketState(int fd, out EmulatedSocketState? state)
{
lock (Gate)
{
return Sockets.TryGetValue(fd, out state);
}
}
private static bool TryParseGuestSockaddrIn(
ulong address,
int addrlen,
CpuContext ctx,
out IPAddress ipAddress,
out int port)
{
ipAddress = IPAddress.None;
port = 0;
if (address == 0 || addrlen < 8)
{
return false;
}
Span<byte> buffer = stackalloc byte[16];
var readLength = Math.Min(addrlen, buffer.Length);
if (!ctx.Memory.TryRead(address, buffer.Slice(0, readLength)))
{
return false;
}
if (buffer[1] != 2)
{
return false;
}
port = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(2, 2));
ipAddress = new IPAddress(buffer.Slice(4, 4).ToArray());
return true;
}
private static void DisposeEmulatedSocket(EmulatedSocketState state)
{
try { state.Stream?.Dispose(); } catch (IOException) { }
try { state.Client?.Dispose(); } catch (IOException) { }
state.Stream = null;
state.Client = null;
state.Connected = false;
}
private static void RemoveEmulatedSocketFd(int fd)
{
lock (Gate)
{
if (Sockets.Remove(fd, out var socketState))
{
DisposeEmulatedSocket(socketState);
}
}
}
private static void LogNet(string message)
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NET"), "1", StringComparison.Ordinal))
{
Console.Error.WriteLine($"[LOADER][DEBUG] {message}");
}
}
private static bool TryApplyNetRedirect(ref IPAddress ipAddress)
{
var redirect = Environment.GetEnvironmentVariable("SHARPEMU_NET_REDIRECT");
if (string.IsNullOrWhiteSpace(redirect))
{
return false;
}
if (!IPAddress.TryParse(redirect.Trim(), out var redirectAddress))
{
return false;
}
ipAddress = redirectAddress;
return true;
}
private static bool IsNetRedirectConfigured()
{
var redirect = Environment.GetEnvironmentVariable("SHARPEMU_NET_REDIRECT");
return !string.IsNullOrWhiteSpace(redirect);
}
private static bool IsGuestTcpOutboundAllowed(IPAddress ipAddress, bool redirectApplied)
{
return redirectApplied || IsNetRedirectConfigured() || IPAddress.IsLoopback(ipAddress);
}
private static bool TryEstablishHostTcpConnection(
IPAddress ipAddress,
int port,
out TcpClient client,
out NetworkStream stream)
{
client = null!;
stream = null!;
if (!TryConnectTcpClient(ipAddress, port, out client))
{
return false;
}
stream = client.GetStream();
return true;
}
private static bool TryConnectTcpClient(IPAddress ipAddress, int port, out TcpClient client)
{
client = new TcpClient();
try
{
var connectTask = client.ConnectAsync(ipAddress, port);
if (!connectTask.Wait(TimeSpan.FromMilliseconds(500)))
{
client.Dispose();
client = null!;
return false;
}
return true;
}
catch (SocketException)
{
client.Dispose();
client = null!;
return false;
}
catch (IOException)
{
client.Dispose();
client = null!;
return false;
}
}
private static bool TryReadCString(ulong address, CpuContext ctx, out string text)
{
const int maxLength = 64;
var buffer = new byte[maxLength];
var length = 0;
for (; length < maxLength; length++)
{
if (!ctx.Memory.TryRead(address + (ulong)length, buffer.AsSpan(length, 1)))
{
text = string.Empty;
return false;
}
if (buffer[length] == 0)
{
break;
}
}
text = Encoding.ASCII.GetString(buffer, 0, length);
return true;
}
private static bool TryParseIpv4Address(string text, out byte[] octets)
{
octets = Array.Empty<byte>();
var parts = text.Split('.', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length != 4)
{
return false;
}
var parsed = new byte[4];
for (var i = 0; i < 4; i++)
{
if (!byte.TryParse(parts[i], out parsed[i]))
{
return false;
}
}
octets = parsed;
return true;
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ public static class LibcInternalExports
[SysAbiExport(
Nid = "NWtTN10cJzE",
ExportName = "sceLibcHeapGetTraceInfo",
ExportName = "LibcHeapGetTraceInfo",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "LibcInternalExt")]
public static int LibcHeapGetTraceInfo(CpuContext ctx)
-820
View File
@@ -1,820 +0,0 @@
// 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 = "8Q60JLJ6Rv4",
ExportName = "getc",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Getc(CpuContext ctx) => FgetcCore(ctx);
[SysAbiExport(
Nid = "AEuF3F2f8TA",
ExportName = "fgetc",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Fgetc(CpuContext ctx) => FgetcCore(ctx);
private static int FgetcCore(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
if (!_fileHandles.TryGetValue(handle, out var stream))
{
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
try
{
var value = stream.ReadByte();
ctx[CpuRegister.Rax] = value < 0 ? unchecked((ulong)(-1L)) : (ulong)value;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
catch (IOException)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
}
[SysAbiExport(
Nid = "LxcEU+ICu8U",
ExportName = "feof",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Feof(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var atEnd = _fileHandles.TryGetValue(handle, out var stream) &&
stream.CanRead && stream.Position >= stream.Length;
ctx[CpuRegister.Rax] = atEnd ? 1UL : 0UL;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "AHxyhN96dy4",
ExportName = "ferror",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Ferror(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "3QIPIh-GDjw",
ExportName = "rewind",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Rewind(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
if (_fileHandles.TryGetValue(handle, out var stream))
{
try
{
stream.Seek(0, SeekOrigin.Begin);
}
catch (IOException)
{
}
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "aZK8lNei-Qw",
ExportName = "fputc",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Fputc(CpuContext ctx)
{
var character = unchecked((byte)ctx[CpuRegister.Rdi]);
var handle = ctx[CpuRegister.Rsi];
if (_fileHandles.TryGetValue(handle, out var stream))
{
try
{
stream.WriteByte(character);
}
catch (IOException)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
}
else
{
// Unknown streams are the bundled libc's real stdout/stderr FILE objects;
// mirror the fputs HLE and forward them to the host console.
Console.Out.Write((char)character);
}
ctx[CpuRegister.Rax] = character;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "MpxhMh8QFro",
ExportName = "fwrite",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Fwrite(CpuContext ctx)
{
var source = 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;
}
var knownHandle = _fileHandles.TryGetValue(handle, out var stream);
if (source == 0)
{
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 totalWritten = 0;
try
{
while (totalWritten < totalRequested)
{
var request = (int)Math.Min((ulong)buffer.Length, totalRequested - totalWritten);
if (!ctx.Memory.TryRead(source + totalWritten, buffer.AsSpan(0, request)))
{
ctx[CpuRegister.Rax] = totalWritten / elementSize;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (knownHandle)
{
stream!.Write(buffer, 0, request);
}
else
{
Console.Out.Write(System.Text.Encoding.UTF8.GetString(buffer, 0, request));
}
totalWritten += (ulong)request;
}
}
catch (IOException)
{
ctx[CpuRegister.Rax] = totalWritten / elementSize;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
ctx[CpuRegister.Rax] = totalWritten / elementSize;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "MUjC4lbHrK4",
ExportName = "fflush",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Fflush(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
try
{
if (handle == 0)
{
foreach (var stream in _fileHandles.Values)
{
stream.Flush();
}
}
else if (_fileHandles.TryGetValue(handle, out var stream))
{
stream.Flush();
}
}
catch (IOException)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "fffwELXNVFA",
ExportName = "fprintf",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Fprintf(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var formatAddress = ctx[CpuRegister.Rsi];
if (!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, formatAddress, MaxPathLength, out var format))
{
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var rendered = KernelMemoryCompatExports.FormatStringFromVarArgs(ctx, format, firstGpArgIndex: 2);
return WriteRenderedText(ctx, handle, rendered);
}
[SysAbiExport(
Nid = "pDBDcY6uLSA",
ExportName = "vfprintf",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Vfprintf(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var formatAddress = ctx[CpuRegister.Rsi];
var vaListAddress = ctx[CpuRegister.Rdx];
if (!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, formatAddress, MaxPathLength, out var format))
{
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
KernelMemoryCompatExports.TryFormatStringFromVaList(ctx, format, vaListAddress, out var rendered);
return WriteRenderedText(ctx, handle, rendered);
}
private static int WriteRenderedText(CpuContext ctx, ulong handle, string rendered)
{
var payload = System.Text.Encoding.UTF8.GetBytes(rendered);
if (_fileHandles.TryGetValue(handle, out var stream))
{
try
{
stream.Write(payload, 0, payload.Length);
}
catch (IOException)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
}
else
{
Console.Out.Write(rendered);
}
ctx[CpuRegister.Rax] = (ulong)payload.Length;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "gkWgn0p1AfU",
ExportName = "freopen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Freopen(CpuContext ctx)
{
var pathAddress = ctx[CpuRegister.Rdi];
var modeAddress = ctx[CpuRegister.Rsi];
var handle = ctx[CpuRegister.Rdx];
if (pathAddress == 0 || modeAddress == 0 || handle == 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;
}
if (_fileHandles.TryRemove(handle, out var previousStream))
{
previousStream.Dispose();
}
var hostPath = KernelMemoryCompatExports.ResolveGuestPath(guestPath);
if (fileAccess != FileAccess.Read && KernelMemoryCompatExports.IsReadOnlyGuestMutationPath(guestPath))
{
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);
// freopen keeps the caller's FILE* identity, so rebind the same handle value.
_fileHandles[handle] = stream;
if (_traceStdio)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] freopen: guest='{guestPath}' host='{hostPath}' mode='{mode}' -> OK handle=0x{handle:X}");
}
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] freopen: 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 = "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;
}
}
}
-33
View File
@@ -1,33 +0,0 @@
// 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;
}
}

Some files were not shown because too many files have changed in this diff Show More