mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 373100a6b0 | |||
| f23161be9a | |||
| 081760be3f | |||
| e604fb606d | |||
| df53ff59d9 | |||
| 4c35831cb8 | |||
| 90fdd20f9a | |||
| ae5ef0abe7 | |||
| 5e2c21edf1 | |||
| 3fb9d4db1c | |||
| de13735972 | |||
| fc0efca297 | |||
| 2a9a261913 | |||
| 290f5fd3d7 | |||
| c06c70cad7 | |||
| 5e54250752 | |||
| be6a6a5935 | |||
| caf859cc52 | |||
| d2f3511002 | |||
| 90a5d5176f | |||
| 28a43e09c7 | |||
| 093cfa1f3e | |||
| d8397b022e | |||
| 85cc2b9892 | |||
| 293194c40b | |||
| 1f09de8896 | |||
| ddc452b4fc | |||
| 61a97baf85 | |||
| e80f96ecf5 | |||
| d49c0f1f10 | |||
| 1d33ef90fc | |||
| 26a570633c | |||
| e4f89445b9 | |||
| 1254cc1564 | |||
| 503b3f4d6b | |||
| 6b37ab54f2 | |||
| a84d2344fb | |||
| 787d3a1efb | |||
| 884584da67 | |||
| d43edc865a | |||
| cf6964710a | |||
| 4f028d0483 | |||
| 4db98bd8fe | |||
| 4600a2ed1f | |||
| c5c5ee1f36 | |||
| b48b1a5e09 | |||
| 511a01e03a | |||
| aaebfe017b | |||
| 9cdc8550ec | |||
| d6fccedab8 | |||
| fed7a6d062 | |||
| 50f78f3713 | |||
| 63b440efcd | |||
| 0565d01744 | |||
| 56bf00e9a5 | |||
| 46b729c5b4 | |||
| 4b7df8623a | |||
| 3c2134474d | |||
| 298ef01809 | |||
| 2060dacaf1 | |||
| f73c9e8c3f | |||
| 3d2c30b151 | |||
| 61d28e9e08 | |||
| 4bd42795c7 | |||
| c03ca32a02 | |||
| 6e2878f2ff | |||
| 2e09b015bc | |||
| 7b9efd1539 | |||
| fbf2c2d00a | |||
| 8c1507777c | |||
| 8ebc43e758 | |||
| 5aadb7495a | |||
| cdee77521e | |||
| d2fca37716 | |||
| 5bee81df70 | |||
| d4bb282c49 |
@@ -88,7 +88,7 @@ body:
|
||||
attributes:
|
||||
label: Last Milestone / First Error
|
||||
description: Paste the **last useful milestone line** or the **first `WARNING`, `ERROR`, or `CRITICAL` line connected to the stop** from the log. `DEBUG`/`TRACE` lines do not count unless a real error follows them.
|
||||
placeholder: e.g. CpuEngine: native-only OR [ERROR] Native backend FAILED: ...
|
||||
placeholder: "e.g. CpuEngine: native-only OR [ERROR] Native backend FAILED: ..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 101 KiB |
@@ -100,6 +100,9 @@ jobs:
|
||||
- name: Build solution
|
||||
run: dotnet build SharpEmu.slnx -c Release --no-restore
|
||||
|
||||
- name: Validate synthetic shaders
|
||||
run: dotnet run --project tools/SharpEmu.Tools.ShaderDump/SharpEmu.Tools.ShaderDump.csproj -c Release -- artifacts/shader-dump
|
||||
|
||||
- name: Publish win-x64 CLI
|
||||
run: dotnet publish src/SharpEmu.CLI/SharpEmu.CLI.csproj -c Release -r win-x64 --self-contained true --no-restore -p:PublishDir="${env:PUBLISH_DIR}"
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
# Contributing
|
||||
|
||||
Contributions are always welcome!
|
||||
|
||||
Before opening a pull request, please keep the following in mind:
|
||||
|
||||
- Keep PRs small and focused on a single topic.
|
||||
- Discuss large architectural changes before implementing them.
|
||||
- Follow the project's existing coding style.
|
||||
- Do not submit generated code unless you fully understand and can maintain it.
|
||||
- Do not introduce Sony proprietary code, firmware, keys, decrypted assets, or other copyrighted PlayStation materials.
|
||||
- All reverse engineering should be based on publicly available information, clean-room techniques, or your own original research.
|
||||
- Game-specific hacks should be avoided whenever possible. Prefer generic implementations that improve overall compatibility.
|
||||
- New features should not break existing behavior.
|
||||
- Ensure the project builds successfully before submitting a PR.
|
||||
|
||||
If you're unsure about a design decision, feel free to open a discussion or draft PR first.
|
||||
|
||||
## AI-Assisted Contributions
|
||||
|
||||
AI-assisted development is welcome and may be used for research, reverse engineering, code generation, or documentation.
|
||||
|
||||
However, contributors are expected to fully understand every line of code they submit. By opening a pull request, you confirm that you are able to explain, modify, debug, and maintain the submitted code without relying on the AI that generated it.
|
||||
|
||||
When submitting an AI-assisted PR:
|
||||
|
||||
- Clearly explain **what the change does**, **why it is needed**, and **what problem it solves**, using your own words.
|
||||
- Describe **how you verified the change**, including the games, applications, or test cases used.
|
||||
- Avoid excessive product-level logging. Use logging only when it provides meaningful diagnostic value.
|
||||
- Comments should document design decisions or implementation details in your own words. Avoid generic AI-generated comments that merely restate what the code already does.
|
||||
- Be prepared to answer review questions about the implementation. "The AI generated it" is not considered a sufficient explanation.
|
||||
- Large AI-generated changes without a clear understanding of the implementation are unlikely to be accepted.
|
||||
- If the implementation cannot be reasonably explained during code review, the pull request may be rejected regardless of whether it works.
|
||||
|
||||
The quality, correctness, maintainability, and long-term ownership of the submitted code remain the responsibility of the contributor.
|
||||
|
||||
## Coding Style
|
||||
|
||||
SharpEmu follows a consistent coding style across the project. Please ensure your contributions match the existing style.
|
||||
|
||||
- Use **4 spaces** for indentation (no tabs).
|
||||
- Use **2 spaces** for XML-based files (e.g. `.csproj`, `.props`, `.targets`, `.xml`, `yml`, GitHub workflow files where applicable).
|
||||
- Respect the project's `.editorconfig`.
|
||||
- Ensure every text file ends with a **single trailing newline**
|
||||
- Avoid formatting-only commits unless they are the purpose of the PR.
|
||||
- Keep naming, formatting, and file organization consistent with the surrounding code.
|
||||
- Prefer small, focused changes over large refactors.
|
||||
|
||||
### REUSE Compliance
|
||||
|
||||
This repository follows the REUSE Specification.
|
||||
|
||||
Every new file must contain the appropriate SPDX license header. Pull requests that do not comply with the project's REUSE requirements will fail CI and will not be merged.
|
||||
|
||||
### Recommended Development Environment
|
||||
|
||||
The repository includes an `.editorconfig` and Visual Studio solution files.
|
||||
|
||||
For the best experience, we recommend using:
|
||||
|
||||
- Visual Studio Code with;
|
||||
- C#
|
||||
- C# Dev Kit
|
||||
|
||||
Most editors that support `.editorconfig` will automatically apply the project's formatting rules.
|
||||
@@ -12,11 +12,14 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.3.18" />
|
||||
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.3.18" />
|
||||
<PackageVersion Include="Iced" Version="1.21.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan" Version="2.23.0" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.23.0" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.23.0" />
|
||||
<PackageVersion Include="Silk.NET.Windowing" Version="2.23.0" />
|
||||
<!-- Transitive of Avalonia.Desktop; pinned to fix GHSA-xrw6-gwf8-vvr9 -->
|
||||
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.21.3" />
|
||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -122,3 +122,16 @@ Provided valuable references for filesystem handling and low-level C# implementa
|
||||
# License
|
||||
|
||||
- [**GPL-2.0 license**](https://github.com/par274/sharpemu/blob/main/LICENSE)
|
||||
|
||||
## Contributing
|
||||
|
||||
Before opening an issue or pull request, please read our contribution guidelines:
|
||||
|
||||
**[CONTRIBUTING.md](./CONTRIBUTING.md)**
|
||||
|
||||
The guide covers:
|
||||
- Coding style and formatting
|
||||
- AI-assisted contributions
|
||||
- Pull request expectations
|
||||
- Testing guidelines
|
||||
- Legal and reverse engineering policy
|
||||
|
||||
@@ -8,6 +8,7 @@ path = [
|
||||
"**/packages.lock.json",
|
||||
"scripts/ps5_names.txt",
|
||||
"src/SharpEmu.HLE/Aerolib/aerolib.bin",
|
||||
"src/SharpEmu.GUI/Languages/**",
|
||||
"_logs/**",
|
||||
".github/images/**",
|
||||
"assets/images/**"
|
||||
|
||||
@@ -12,4 +12,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<Project Path="src/SharpEmu.Libs/SharpEmu.Libs.csproj" />
|
||||
<Project Path="src/SharpEmu.Logging/SharpEmu.Logging.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tests/">
|
||||
<Project Path="tests/SharpEmu.Libs.Tests/SharpEmu.Libs.Tests.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 698 B |
Binary file not shown.
|
After Width: | Height: | Size: 802 B |
@@ -0,0 +1,172 @@
|
||||
# Copyright (C) 2026 SharpEmu Emulator Project
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""Offline check: SysAbiExport ExportName must hash to its Nid (name2nid).
|
||||
|
||||
NIDs absent from aerolib.bin are skipped (unknown/unresolved symbols).
|
||||
Known historic mislabels may be allowlisted with a one-line reason.
|
||||
|
||||
Run from the repository root:
|
||||
python scripts/check_sysabi_aerolib.py
|
||||
python scripts/check_sysabi_aerolib.py --strict
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
from base64 import b64encode as base64enc
|
||||
from binascii import unhexlify as uhx
|
||||
from pathlib import Path
|
||||
|
||||
SRC_ROOT = Path("src")
|
||||
AEROLIB_BIN = Path("src/SharpEmu.HLE/Aerolib/aerolib.bin")
|
||||
SYSABI_EXPORT_RE = re.compile(r"\[SysAbiExport\((.*?)\)\]", re.DOTALL)
|
||||
NID_RE = re.compile(r'Nid\s*=\s*"([^"]+)"')
|
||||
EXPORT_NAME_RE = re.compile(r'ExportName\s*=\s*"([^"]+)"')
|
||||
|
||||
# NID -> reason. Keep minimal; fix ExportName when safe instead of growing this list.
|
||||
ALLOWLISTED_NIDS: dict[str, str] = {
|
||||
"KMcEa+rHsIo": "Historic kernel MapMemory stub bound to sceAvPlayerAddSource NID; API rewrite deferred.",
|
||||
"WV1GwM32NgY": "Historic WebApi2 init alias for PushEventCreateHandle NID; ABI rewrite deferred.",
|
||||
}
|
||||
|
||||
|
||||
def name2nid(name: str) -> str:
|
||||
symbol = hashlib.sha1(name.encode() + uhx("518D64A635DED8C1E6B039B1C3E55230")).digest()
|
||||
id_val = struct.unpack("<Q", symbol[:8])[0]
|
||||
nid = base64enc(uhx("%016x" % id_val), b"+-").rstrip(b"=")
|
||||
return nid.decode("utf-8")
|
||||
|
||||
|
||||
def find_repo_root() -> Path:
|
||||
cwd = Path.cwd()
|
||||
if (cwd / SRC_ROOT).is_dir() and (cwd / "scripts").is_dir():
|
||||
return cwd
|
||||
script_root = Path(__file__).resolve().parent.parent
|
||||
if (script_root / SRC_ROOT).is_dir():
|
||||
return script_root
|
||||
raise SystemExit("Run from the repository root (src/ and scripts/ expected).")
|
||||
|
||||
|
||||
def load_aerolib_nids(aerolib_path: Path) -> set[str]:
|
||||
data = aerolib_path.read_bytes()
|
||||
if len(data) < 4:
|
||||
raise SystemExit(f"Aerolib binary too small: {aerolib_path}")
|
||||
|
||||
count = struct.unpack_from("<I", data, 0)[0]
|
||||
offset = 4
|
||||
nids: set[str] = set()
|
||||
for _ in range(count):
|
||||
if offset >= len(data):
|
||||
raise SystemExit(f"Truncated aerolib.bin while reading NIDs: {aerolib_path}")
|
||||
nid_len = data[offset]
|
||||
offset += 1
|
||||
nid = data[offset : offset + nid_len].decode("utf-8")
|
||||
offset += nid_len
|
||||
if offset + 2 > len(data):
|
||||
raise SystemExit(f"Truncated aerolib.bin name length: {aerolib_path}")
|
||||
name_len = struct.unpack_from("<H", data, offset)[0]
|
||||
offset += 2 + name_len
|
||||
nids.add(nid)
|
||||
return nids
|
||||
|
||||
|
||||
def iter_sysabi_exports(cs_path: Path, text: str):
|
||||
for match in SYSABI_EXPORT_RE.finditer(text):
|
||||
block = match.group(1)
|
||||
nid_match = NID_RE.search(block)
|
||||
export_match = EXPORT_NAME_RE.search(block)
|
||||
if nid_match is None or export_match is None:
|
||||
continue
|
||||
|
||||
nid = nid_match.group(1)
|
||||
export_name = export_match.group(1)
|
||||
nid_attr = f'Nid = "{nid}"'
|
||||
abs_pos = text.find(nid_attr, match.start(), match.end())
|
||||
if abs_pos < 0:
|
||||
abs_pos = match.start()
|
||||
line = text.count("\n", 0, abs_pos) + 1
|
||||
yield cs_path, line, nid, export_name
|
||||
|
||||
|
||||
def scan(src_root: Path, catalog_nids: set[str]):
|
||||
checked = 0
|
||||
mismatches = []
|
||||
skipped_no_catalog = 0
|
||||
allowlisted = 0
|
||||
|
||||
for cs_path in sorted(src_root.rglob("*.cs")):
|
||||
text = cs_path.read_text(encoding="utf-8")
|
||||
for path, line, nid, export_name in iter_sysabi_exports(cs_path, text):
|
||||
checked += 1
|
||||
computed = name2nid(export_name)
|
||||
if computed == nid:
|
||||
continue
|
||||
|
||||
if nid not in catalog_nids:
|
||||
skipped_no_catalog += 1
|
||||
continue
|
||||
|
||||
if nid in ALLOWLISTED_NIDS:
|
||||
allowlisted += 1
|
||||
continue
|
||||
|
||||
mismatches.append((path, line, nid, export_name, computed))
|
||||
|
||||
return checked, mismatches, skipped_no_catalog, allowlisted
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Check that SysAbiExport ExportName values hash to their Nid via name2nid."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--strict",
|
||||
action="store_true",
|
||||
help="Exit 1 when any non-skipped/non-allowlisted ExportName does not hash to its Nid.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--quiet",
|
||||
action="store_true",
|
||||
help="Print only the summary line.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = find_repo_root()
|
||||
aerolib_path = repo_root / AEROLIB_BIN
|
||||
if not aerolib_path.is_file():
|
||||
raise SystemExit(f"Missing Aerolib catalog: {aerolib_path.as_posix()}")
|
||||
|
||||
catalog_nids = load_aerolib_nids(aerolib_path)
|
||||
checked, mismatches, skipped_no_catalog, allowlisted = scan(
|
||||
repo_root / SRC_ROOT, catalog_nids
|
||||
)
|
||||
ok = checked - len(mismatches) - skipped_no_catalog - allowlisted
|
||||
|
||||
if not args.quiet:
|
||||
for path, line, nid, export_name, computed in mismatches:
|
||||
rel = path.relative_to(repo_root).as_posix()
|
||||
print(
|
||||
f"{rel}:{line}: NID={nid} ExportName={export_name!r} "
|
||||
f"computed={computed}"
|
||||
)
|
||||
|
||||
print(
|
||||
f"checked={checked} ok={ok} fail={len(mismatches)} "
|
||||
f"skipped_no_catalog={skipped_no_catalog} allowlisted={allowlisted} "
|
||||
f"allowlist_size={len(ALLOWLISTED_NIDS)}"
|
||||
)
|
||||
|
||||
if args.strict and mismatches:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import struct
|
||||
import hashlib
|
||||
import struct
|
||||
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,10 +42,11 @@ 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
+389
-13
@@ -5,15 +5,19 @@ using SharpEmu.Core.Runtime;
|
||||
using SharpEmu.Core.Cpu;
|
||||
using SharpEmu.GUI;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
using SharpEmu.Logging;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SharpEmu.CLI;
|
||||
|
||||
internal static partial class Program
|
||||
{
|
||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("SharpEmu.CLI");
|
||||
private static readonly object ConsoleMirrorSync = new();
|
||||
private static StreamWriter? _consoleMirrorFile;
|
||||
private const int DefaultImportTraceLimit = 32;
|
||||
private const string MitigatedChildFlag = "--sharpemu-mitigated-child";
|
||||
private const uint EXTENDED_STARTUPINFO_PRESENT = 0x00080000;
|
||||
@@ -21,11 +25,15 @@ internal static partial class Program
|
||||
private const int PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY = 0x00020007;
|
||||
private const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000;
|
||||
private const int JobObjectExtendedLimitInformation = 9;
|
||||
private const int STARTF_USESTDHANDLES = 0x00000100;
|
||||
private const uint HANDLE_FLAG_INHERIT = 0x00000001;
|
||||
private const string MitigatedChildEnvironment = "SHARPEMU_MITIGATED_CHILD";
|
||||
private const ulong PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
|
||||
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF = 0x00000002UL << 28;
|
||||
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF = 0x00000002UL << 32;
|
||||
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
|
||||
private const int ATTACH_PARENT_PROCESS = -1;
|
||||
private const int STD_INPUT_HANDLE = -10;
|
||||
private const int STD_OUTPUT_HANDLE = -11;
|
||||
private const int STD_ERROR_HANDLE = -12;
|
||||
private const uint GENERIC_READ = 0x80000000;
|
||||
@@ -43,6 +51,7 @@ internal static partial class Program
|
||||
}
|
||||
finally
|
||||
{
|
||||
DropConsoleFileMirror();
|
||||
SharpEmuLog.Shutdown();
|
||||
}
|
||||
}
|
||||
@@ -61,6 +70,10 @@ internal static partial class Program
|
||||
// itself to a console before the first write.
|
||||
EnsureCliConsole();
|
||||
UseUtf8ConsoleOutput();
|
||||
if (isMitigatedChild && TryGetLogFileArgument(args, out var earlyLogFilePath))
|
||||
{
|
||||
TryEnableConsoleFileMirror(earlyLogFilePath);
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[DEBUG] SharpEmu starting with {args.Length} args");
|
||||
|
||||
@@ -69,15 +82,21 @@ internal static partial class Program
|
||||
return childExitCode;
|
||||
}
|
||||
|
||||
if (!TryParseArguments(args, out var ebootPath, out var runtimeOptions, out var logLevel))
|
||||
if (!TryParseArguments(args, out var ebootPath, out var runtimeOptions, out var logLevel, out var logFilePath))
|
||||
{
|
||||
PrintUsage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!isMitigatedChild && !string.IsNullOrWhiteSpace(logFilePath))
|
||||
{
|
||||
TryEnableConsoleFileMirror(logFilePath);
|
||||
}
|
||||
|
||||
SharpEmuLog.MinimumLevel = logLevel;
|
||||
|
||||
Log.Info(BuildInfo.Banner);
|
||||
Log.Info(HostSystemInfo.Summary);
|
||||
|
||||
ebootPath = Path.GetFullPath(ebootPath);
|
||||
Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}");
|
||||
@@ -93,8 +112,16 @@ internal static partial class Program
|
||||
using var runtime = SharpEmuRuntime.CreateDefault(runtimeOptions);
|
||||
|
||||
OrbisGen2Result result;
|
||||
ConsoleCancelEventHandler? cancelHandler = null;
|
||||
try
|
||||
{
|
||||
cancelHandler = (_, eventArgs) =>
|
||||
{
|
||||
eventArgs.Cancel = true;
|
||||
VideoOutExports.NotifyHostInterrupt();
|
||||
};
|
||||
Console.CancelKeyPress += cancelHandler;
|
||||
|
||||
Console.Error.WriteLine($"[DEBUG] Running: {ebootPath}");
|
||||
result = runtime.Run(ebootPath);
|
||||
Console.Error.WriteLine($"[DEBUG] Result: {result}");
|
||||
@@ -105,6 +132,13 @@ internal static partial class Program
|
||||
Log.Error("SharpEmu failed to run.", ex);
|
||||
return 3;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (cancelHandler is not null)
|
||||
{
|
||||
Console.CancelKeyPress -= cancelHandler;
|
||||
}
|
||||
}
|
||||
|
||||
Log.Info($"SharpEmu execution completed. Result={result} (0x{(int)result:X8})");
|
||||
if (!string.IsNullOrWhiteSpace(runtime.LastSessionSummary))
|
||||
@@ -234,6 +268,10 @@ internal static partial class Program
|
||||
private static string[] NormalizeInternalArguments(string[] args, out bool isMitigatedChild)
|
||||
{
|
||||
isMitigatedChild = false;
|
||||
var trustedMitigatedChild = string.Equals(
|
||||
Environment.GetEnvironmentVariable(MitigatedChildEnvironment),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
if (args.Length == 0)
|
||||
{
|
||||
return args;
|
||||
@@ -244,7 +282,7 @@ internal static partial class Program
|
||||
{
|
||||
if (string.Equals(arg, MitigatedChildFlag, StringComparison.Ordinal))
|
||||
{
|
||||
isMitigatedChild = true;
|
||||
isMitigatedChild = trustedMitigatedChild;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -283,9 +321,11 @@ internal static partial class Program
|
||||
var commandLine = BuildCommandLine(processPath, childArgs);
|
||||
var startupInfoEx = new STARTUPINFOEX();
|
||||
startupInfoEx.StartupInfo.cb = Marshal.SizeOf<STARTUPINFOEX>();
|
||||
ConfigureInheritedStdHandles(ref startupInfoEx.StartupInfo);
|
||||
|
||||
nint attributeList = 0;
|
||||
nint mitigationPolicies = 0;
|
||||
var previousChildEnvironment = Environment.GetEnvironmentVariable(MitigatedChildEnvironment);
|
||||
try
|
||||
{
|
||||
nuint attributeListSize = 0;
|
||||
@@ -293,7 +333,9 @@ internal static partial class Program
|
||||
attributeList = Marshal.AllocHGlobal((nint)attributeListSize);
|
||||
if (!InitializeProcThreadAttributeList(attributeList, 1, 0, ref attributeListSize))
|
||||
{
|
||||
return false;
|
||||
childExitCode = 5;
|
||||
Console.Error.WriteLine($"[ERROR] Failed to initialize mitigation attributes: {Marshal.GetLastWin32Error()}");
|
||||
return true;
|
||||
}
|
||||
|
||||
startupInfoEx.lpAttributeList = attributeList;
|
||||
@@ -301,8 +343,7 @@ internal static partial class Program
|
||||
var policy1 = PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF;
|
||||
var policy2 =
|
||||
PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF |
|
||||
PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF |
|
||||
PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF;
|
||||
PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF;
|
||||
|
||||
mitigationPolicies = Marshal.AllocHGlobal(sizeof(ulong) * 2);
|
||||
Marshal.WriteInt64(mitigationPolicies, unchecked((long)policy1));
|
||||
@@ -317,24 +358,31 @@ internal static partial class Program
|
||||
0,
|
||||
0))
|
||||
{
|
||||
return false;
|
||||
childExitCode = 5;
|
||||
Console.Error.WriteLine($"[ERROR] Failed to apply mitigation attributes: {Marshal.GetLastWin32Error()}");
|
||||
return true;
|
||||
}
|
||||
|
||||
var cmdLineBuilder = new StringBuilder(commandLine);
|
||||
nint jobHandle = 0;
|
||||
if (!CreateProcessW(
|
||||
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, "1");
|
||||
var created = CreateProcessW(
|
||||
processPath,
|
||||
cmdLineBuilder,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
true,
|
||||
EXTENDED_STARTUPINFO_PRESENT,
|
||||
0,
|
||||
Environment.CurrentDirectory,
|
||||
ref startupInfoEx,
|
||||
out var processInfo))
|
||||
out var processInfo);
|
||||
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, previousChildEnvironment);
|
||||
if (!created)
|
||||
{
|
||||
return false;
|
||||
childExitCode = 5;
|
||||
Console.Error.WriteLine($"[ERROR] Failed to launch mitigated child process: {Marshal.GetLastWin32Error()}");
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
@@ -388,6 +436,8 @@ internal static partial class Program
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, previousChildEnvironment);
|
||||
|
||||
if (attributeList != 0)
|
||||
{
|
||||
DeleteProcThreadAttributeList(attributeList);
|
||||
@@ -401,6 +451,238 @@ internal static partial class Program
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetLogFileArgument(IReadOnlyList<string> args, out string path)
|
||||
{
|
||||
for (var i = 0; i < args.Count; i++)
|
||||
{
|
||||
var argument = args[i];
|
||||
if (string.Equals(argument, "--log-file", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (i + 1 < args.Count &&
|
||||
!string.IsNullOrWhiteSpace(args[i + 1]) &&
|
||||
!args[i + 1].StartsWith("--", StringComparison.Ordinal) &&
|
||||
ShouldConsumeLogFilePath(args, i + 1))
|
||||
{
|
||||
path = args[i + 1];
|
||||
return true;
|
||||
}
|
||||
|
||||
path = BuildDefaultLogFilePath(TryFindEbootPathToken(args));
|
||||
return true;
|
||||
}
|
||||
|
||||
const string logFilePrefix = "--log-file=";
|
||||
if (argument.StartsWith(logFilePrefix, StringComparison.OrdinalIgnoreCase) &&
|
||||
!string.IsNullOrWhiteSpace(argument[logFilePrefix.Length..]))
|
||||
{
|
||||
path = argument[logFilePrefix.Length..];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
path = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string BuildDefaultLogFilePath(string? ebootPath)
|
||||
{
|
||||
var baseDirectory = AppContext.BaseDirectory;
|
||||
var logsDirectory = Path.Combine(baseDirectory, "user", "logs");
|
||||
var name = TryReadTitleId(ebootPath) ?? "UNKNOWN";
|
||||
|
||||
foreach (var invalid in Path.GetInvalidFileNameChars())
|
||||
{
|
||||
name = name.Replace(invalid, '_');
|
||||
}
|
||||
|
||||
return Path.Combine(logsDirectory, $"{name}-{DateTime.Now:yyyyMMdd-HHmmss}.log");
|
||||
}
|
||||
|
||||
private static string? TryReadTitleId(string? ebootPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ebootPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var directory = Path.GetDirectoryName(Path.GetFullPath(ebootPath));
|
||||
if (string.IsNullOrEmpty(directory))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var paramPath in new[]
|
||||
{
|
||||
Path.Combine(directory, "sce_sys", "param.json"),
|
||||
Path.Combine(directory, "param.json"),
|
||||
})
|
||||
{
|
||||
if (!File.Exists(paramPath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
using var stream = File.OpenRead(paramPath);
|
||||
using var document = JsonDocument.Parse(stream);
|
||||
if (document.RootElement.TryGetProperty("titleId", out var titleIdElement) &&
|
||||
titleIdElement.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var titleId = titleIdElement.GetString();
|
||||
if (!string.IsNullOrWhiteSpace(titleId))
|
||||
{
|
||||
return titleId.Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Logging should never block launch; unknown title ids use a stable fallback.
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string? TryFindEbootPathToken(IReadOnlyList<string> args)
|
||||
{
|
||||
for (var i = args.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var argument = args[i];
|
||||
if (string.IsNullOrWhiteSpace(argument) ||
|
||||
argument.StartsWith("--", StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return argument;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool ShouldConsumeLogFilePath(IReadOnlyList<string> args, int candidateIndex)
|
||||
{
|
||||
var candidate = args[candidateIndex];
|
||||
if (LooksLikeLogFilePath(candidate))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
for (var i = candidateIndex + 1; i < args.Count; i++)
|
||||
{
|
||||
var argument = args[i];
|
||||
if (!string.IsNullOrWhiteSpace(argument) &&
|
||||
!argument.StartsWith("--", StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool LooksLikeLogFilePath(string path)
|
||||
{
|
||||
var extension = Path.GetExtension(path);
|
||||
return string.Equals(extension, ".log", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(extension, ".txt", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static void TryEnableConsoleFileMirror(string path)
|
||||
{
|
||||
lock (ConsoleMirrorSync)
|
||||
{
|
||||
if (_consoleMirrorFile is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var directory = Path.GetDirectoryName(path);
|
||||
if (!string.IsNullOrEmpty(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
var stream = new FileStream(
|
||||
path,
|
||||
FileMode.Create,
|
||||
FileAccess.Write,
|
||||
FileShare.ReadWrite,
|
||||
bufferSize: 4096,
|
||||
FileOptions.SequentialScan);
|
||||
_consoleMirrorFile = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false))
|
||||
{
|
||||
AutoFlush = true,
|
||||
};
|
||||
|
||||
Console.SetOut(new TeeTextWriter(Console.Out, _consoleMirrorFile));
|
||||
Console.SetError(new TeeTextWriter(Console.Error, _consoleMirrorFile));
|
||||
Console.Error.WriteLine($"[DEBUG] Log file: {Path.GetFullPath(path)}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[WARN] Could not open log file '{path}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DropConsoleFileMirror()
|
||||
{
|
||||
lock (ConsoleMirrorSync)
|
||||
{
|
||||
try
|
||||
{
|
||||
_consoleMirrorFile?.Flush();
|
||||
_consoleMirrorFile?.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
_consoleMirrorFile = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ConfigureInheritedStdHandles(ref STARTUPINFO startupInfo)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var input = GetStdHandle(STD_INPUT_HANDLE);
|
||||
var output = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
var error = GetStdHandle(STD_ERROR_HANDLE);
|
||||
if (!IsHandleValid(output) && !IsHandleValid(error))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsHandleValid(input))
|
||||
{
|
||||
_ = SetHandleInformation(input, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
|
||||
startupInfo.hStdInput = input;
|
||||
}
|
||||
|
||||
if (IsHandleValid(output))
|
||||
{
|
||||
_ = SetHandleInformation(output, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
|
||||
startupInfo.hStdOutput = output;
|
||||
}
|
||||
|
||||
if (IsHandleValid(error))
|
||||
{
|
||||
_ = SetHandleInformation(error, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
|
||||
startupInfo.hStdError = error;
|
||||
}
|
||||
|
||||
startupInfo.dwFlags |= STARTF_USESTDHANDLES;
|
||||
}
|
||||
|
||||
private static string BuildCommandLine(string processPath, IReadOnlyList<string> args)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
@@ -503,27 +785,30 @@ internal static partial class Program
|
||||
|
||||
private static void PrintUsage()
|
||||
{
|
||||
Log.Info("Usage: SharpEmu.CLI [--strict] [--trace-imports[=N]] [--cpu-engine=<native>] [--log-level=<level>] <path-to-eboot.bin>");
|
||||
Log.Info(@"Example: SharpEmu.CLI --cpu-engine=native --trace-imports=64 --log-level=debug ""E:\Games\...\eboot.bin""");
|
||||
Log.Info("Usage: SharpEmu.CLI [--strict] [--trace-imports[=N]] [--cpu-engine=<native>] [--log-level=<level>] [--log-file[=<path>]] <path-to-eboot.bin>");
|
||||
Log.Info(@"Example: SharpEmu.CLI --cpu-engine=native --trace-imports=64 --log-level=debug --log-file ""E:\Games\...\eboot.bin""");
|
||||
}
|
||||
|
||||
private static bool TryParseArguments(
|
||||
string[] args,
|
||||
out string ebootPath,
|
||||
out SharpEmuRuntimeOptions runtimeOptions,
|
||||
out LogLevel logLevel)
|
||||
out LogLevel logLevel,
|
||||
out string? logFilePath)
|
||||
{
|
||||
if (args.Length == 0)
|
||||
{
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
logLevel = SharpEmuLog.MinimumLevel;
|
||||
logFilePath = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var strictDynlibResolution = false;
|
||||
var importTraceLimit = 0;
|
||||
var cpuEngine = CpuExecutionEngine.NativeOnly;
|
||||
logFilePath = null;
|
||||
logLevel = SharpEmuLog.MinimumLevel;
|
||||
var pathTokens = new List<string>(args.Length);
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
@@ -553,6 +838,7 @@ internal static partial class Program
|
||||
{
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
logFilePath = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -566,6 +852,7 @@ internal static partial class Program
|
||||
{
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
logFilePath = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -573,6 +860,23 @@ internal static partial class Program
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.Equals(argument, "--log-file", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (i + 1 < args.Length &&
|
||||
!string.IsNullOrWhiteSpace(args[i + 1]) &&
|
||||
!args[i + 1].StartsWith("--", StringComparison.Ordinal) &&
|
||||
ShouldConsumeLogFilePath(args, i + 1))
|
||||
{
|
||||
logFilePath = args[++i];
|
||||
}
|
||||
else
|
||||
{
|
||||
logFilePath = BuildDefaultLogFilePath(TryFindEbootPathToken(args));
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const string logLevelPrefix = "--log-level=";
|
||||
if (argument.StartsWith(logLevelPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -596,6 +900,7 @@ internal static partial class Program
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
logLevel = SharpEmuLog.MinimumLevel;
|
||||
logFilePath = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -618,11 +923,27 @@ internal static partial class Program
|
||||
continue;
|
||||
}
|
||||
|
||||
const string logFilePrefix = "--log-file=";
|
||||
if (argument.StartsWith(logFilePrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
logFilePath = argument[logFilePrefix.Length..];
|
||||
if (string.IsNullOrWhiteSpace(logFilePath))
|
||||
{
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
logLevel = SharpEmuLog.MinimumLevel;
|
||||
return false;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argument.StartsWith("--", StringComparison.Ordinal))
|
||||
{
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
logLevel = SharpEmuLog.MinimumLevel;
|
||||
logFilePath = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -634,6 +955,7 @@ internal static partial class Program
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
logLevel = SharpEmuLog.MinimumLevel;
|
||||
logFilePath = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -735,6 +1057,56 @@ internal static partial class Program
|
||||
public nuint PeakJobMemoryUsed;
|
||||
}
|
||||
|
||||
private sealed class TeeTextWriter : TextWriter
|
||||
{
|
||||
private readonly TextWriter _primary;
|
||||
private readonly TextWriter _mirror;
|
||||
|
||||
public TeeTextWriter(TextWriter primary, TextWriter mirror)
|
||||
{
|
||||
_primary = primary;
|
||||
_mirror = mirror;
|
||||
}
|
||||
|
||||
public override Encoding Encoding => _primary.Encoding;
|
||||
|
||||
public override void Write(char value)
|
||||
{
|
||||
lock (ConsoleMirrorSync)
|
||||
{
|
||||
_primary.Write(value);
|
||||
_mirror.Write(value);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Write(string? value)
|
||||
{
|
||||
lock (ConsoleMirrorSync)
|
||||
{
|
||||
_primary.Write(value);
|
||||
_mirror.Write(value);
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteLine(string? value)
|
||||
{
|
||||
lock (ConsoleMirrorSync)
|
||||
{
|
||||
_primary.WriteLine(value);
|
||||
_mirror.WriteLine(value);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Flush()
|
||||
{
|
||||
lock (ConsoleMirrorSync)
|
||||
{
|
||||
_primary.Flush();
|
||||
_mirror.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool InitializeProcThreadAttributeList(
|
||||
@@ -819,6 +1191,10 @@ internal static partial class Program
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool SetStdHandle(int stdHandle, nint handle);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool SetHandleInformation(nint handle, uint mask, uint flags);
|
||||
|
||||
[DllImport("kernel32.dll", EntryPoint = "CreateFileW", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern nint CreateFileW(
|
||||
string fileName,
|
||||
|
||||
@@ -24,6 +24,9 @@ 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'">
|
||||
@@ -48,6 +51,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<TargetPath>LICENSE.txt</TargetPath>
|
||||
<Visible>False</Visible>
|
||||
</Content>
|
||||
<Content Include="..\SharpEmu.GUI\Languages\*.json" Condition="'$(Configuration)' != 'Release'">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<TargetPath>Languages\%(Filename)%(Extension)</TargetPath>
|
||||
<Visible>False</Visible>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Keep glfw as a loose file next to the executable; every other native
|
||||
|
||||
@@ -216,7 +216,10 @@
|
||||
}
|
||||
},
|
||||
"sharpemu.hle": {
|
||||
"type": "Project"
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"SharpEmu.Logging": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.libs": {
|
||||
"type": "Project",
|
||||
|
||||
@@ -7,6 +7,7 @@ using SharpEmu.Core.Cpu.Native;
|
||||
using SharpEmu.Core.Loader;
|
||||
using SharpEmu.Core.Memory;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.HLE.Host;
|
||||
using SharpEmu.Logging;
|
||||
|
||||
namespace SharpEmu.Core.Cpu;
|
||||
@@ -41,16 +42,19 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
];
|
||||
private readonly IVirtualMemory _virtualMemory;
|
||||
private readonly IModuleManager _moduleManager;
|
||||
private readonly IHostPlatform? _hostPlatform;
|
||||
private INativeCpuBackend? _nativeCpuBackend;
|
||||
|
||||
public CpuDispatcher(
|
||||
IVirtualMemory virtualMemory,
|
||||
IModuleManager moduleManager,
|
||||
INativeCpuBackend? nativeCpuBackend = null)
|
||||
INativeCpuBackend? nativeCpuBackend = null,
|
||||
IHostPlatform? hostPlatform = null)
|
||||
{
|
||||
_virtualMemory = virtualMemory ?? throw new ArgumentNullException(nameof(virtualMemory));
|
||||
_moduleManager = moduleManager ?? throw new ArgumentNullException(nameof(moduleManager));
|
||||
_nativeCpuBackend = nativeCpuBackend;
|
||||
_hostPlatform = hostPlatform;
|
||||
}
|
||||
|
||||
public ulong? LastEntryPoint { get; private set; }
|
||||
@@ -266,7 +270,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
entryFrameDiagnostic,
|
||||
Environment.NewLine,
|
||||
"CpuEngine: native-only");
|
||||
_nativeCpuBackend ??= new DirectExecutionBackend(_moduleManager);
|
||||
_nativeCpuBackend ??= new DirectExecutionBackend(_moduleManager, _hostPlatform);
|
||||
if (_nativeCpuBackend.TryExecute(
|
||||
context,
|
||||
entryPoint,
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.HLE.Host;
|
||||
using SharpEmu.Logging;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
@@ -38,6 +39,71 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
}
|
||||
|
||||
private void RecordDeferredBootstrapTrace(
|
||||
long dispatchIndex,
|
||||
ulong op,
|
||||
ulong symbolPointer,
|
||||
ulong outputPointer,
|
||||
ulong returnRip)
|
||||
{
|
||||
lock (_deferredBootstrapTraceGate)
|
||||
{
|
||||
_deferredBootstrapTrace[_deferredBootstrapTraceWriteIndex] = new DeferredBootstrapTraceEntry(
|
||||
dispatchIndex,
|
||||
op,
|
||||
symbolPointer,
|
||||
outputPointer,
|
||||
returnRip);
|
||||
_deferredBootstrapTraceWriteIndex =
|
||||
(_deferredBootstrapTraceWriteIndex + 1) % _deferredBootstrapTrace.Length;
|
||||
if (_deferredBootstrapTraceCount < _deferredBootstrapTrace.Length)
|
||||
{
|
||||
_deferredBootstrapTraceCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrainDeferredBootstrapTraces()
|
||||
{
|
||||
if (!_logBootstrap)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DeferredBootstrapTraceEntry[] pending;
|
||||
lock (_deferredBootstrapTraceGate)
|
||||
{
|
||||
if (_deferredBootstrapTraceCount == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
pending = new DeferredBootstrapTraceEntry[_deferredBootstrapTraceCount];
|
||||
var readIndex = (_deferredBootstrapTraceWriteIndex - _deferredBootstrapTraceCount +
|
||||
_deferredBootstrapTrace.Length) % _deferredBootstrapTrace.Length;
|
||||
for (var i = 0; i < _deferredBootstrapTraceCount; i++)
|
||||
{
|
||||
pending[i] = _deferredBootstrapTrace[(readIndex + i) % _deferredBootstrapTrace.Length];
|
||||
}
|
||||
|
||||
_deferredBootstrapTraceCount = 0;
|
||||
}
|
||||
|
||||
foreach (var entry in pending)
|
||||
{
|
||||
var symbolText = "<unreadable>";
|
||||
if (TryReadAsciiZ(entry.SymbolPointer, 256, out var sym))
|
||||
{
|
||||
symbolText = sym;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] bootstrap_call#{entry.DispatchIndex}: op=0x{entry.Op:X16} " +
|
||||
$"sym_ptr=0x{entry.SymbolPointer:X16} sym='{symbolText}' " +
|
||||
$"out_ptr=0x{entry.OutputPointer:X16} ret=0x{entry.ReturnRip:X16}");
|
||||
}
|
||||
}
|
||||
|
||||
private void DumpRecentImportTrace()
|
||||
{
|
||||
if (_recentImportTraceCount == 0)
|
||||
@@ -69,8 +135,9 @@ public sealed partial class DirectExecutionBackend
|
||||
int num2 = 0;
|
||||
List<ulong> list = new List<ulong>(16);
|
||||
ulong num3 = scanStart;
|
||||
MEMORY_BASIC_INFORMATION64 lpBuffer;
|
||||
while (num3 < scanEnd && VirtualQuery((void*)num3, out lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) != 0)
|
||||
var hostMemory = ResolveDiagnosticsHostMemory();
|
||||
HostRegionInfo lpBuffer;
|
||||
while (num3 < scanEnd && hostMemory.Query(num3, out lpBuffer))
|
||||
{
|
||||
ulong baseAddress = lpBuffer.BaseAddress;
|
||||
ulong num4 = baseAddress + lpBuffer.RegionSize;
|
||||
@@ -80,7 +147,7 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
ulong value = Math.Max(num3, baseAddress);
|
||||
ulong num5 = Math.Min(num4, scanEnd);
|
||||
if (lpBuffer.State == 4096 && IsReadableProtection(lpBuffer.Protect) && !IsExecutableProtection(lpBuffer.Protect))
|
||||
if (lpBuffer.State == HostRegionState.Committed && IsReadableProtection(lpBuffer.RawProtection) && !IsExecutableProtection(lpBuffer.RawProtection))
|
||||
{
|
||||
ulong num6 = AlignUp(value, 8uL);
|
||||
for (ulong num7 = num6; num7 + 8 <= num5; num7 += 8)
|
||||
@@ -170,14 +237,15 @@ public sealed partial class DirectExecutionBackend
|
||||
ulong callRip = returnRip + (ulong)i;
|
||||
ulong target = unchecked((ulong)((long)(callRip + 5) + rel32));
|
||||
Log.Debug($"Import#{dispatchIndex} near-call @{callRip:X16}: target=0x{target:X16}");
|
||||
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
|
||||
var importEntries = _importEntries;
|
||||
for (int importIndex = 0; importIndex < importEntries.Length; importIndex++)
|
||||
{
|
||||
if (_importEntries[importIndex].Address != target)
|
||||
if (importEntries[importIndex].Address != target)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string nid = _importEntries[importIndex].Nid;
|
||||
string nid = importEntries[importIndex].Nid;
|
||||
if (_moduleManager.TryGetExport(nid, out var export))
|
||||
{
|
||||
Log.Debug(
|
||||
@@ -204,14 +272,14 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
Log.Debug(
|
||||
$"Import#{dispatchIndex} near-call PLT slot: [0x{slot:X16}] = 0x{slotTarget:X16}");
|
||||
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
|
||||
for (int importIndex = 0; importIndex < importEntries.Length; importIndex++)
|
||||
{
|
||||
if (_importEntries[importIndex].Address != slotTarget)
|
||||
if (importEntries[importIndex].Address != slotTarget)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string nid = _importEntries[importIndex].Nid;
|
||||
string nid = importEntries[importIndex].Nid;
|
||||
if (_moduleManager.TryGetExport(nid, out var export))
|
||||
{
|
||||
Log.Debug(
|
||||
@@ -284,7 +352,7 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
||||
if (!ResolveDiagnosticsHostMemory().Query(address, out var lpBuffer))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -293,7 +361,7 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (lpBuffer.State != 4096 || !IsReadableProtection(lpBuffer.Protect))
|
||||
if (lpBuffer.State != HostRegionState.Committed || !IsReadableProtection(lpBuffer.RawProtection))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -325,12 +393,12 @@ public sealed partial class DirectExecutionBackend
|
||||
return true;
|
||||
}
|
||||
|
||||
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
||||
if (!ResolveDiagnosticsHostMemory().Query(address, out var lpBuffer))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var executable = lpBuffer.State == 4096 && IsExecutableProtection(lpBuffer.Protect);
|
||||
var executable = lpBuffer.State == HostRegionState.Committed && IsExecutableProtection(lpBuffer.RawProtection);
|
||||
if (executable)
|
||||
{
|
||||
_knownExecutablePages.TryAdd(pageAddress, 0);
|
||||
@@ -349,6 +417,14 @@ public sealed partial class DirectExecutionBackend
|
||||
return (value + num) & ~num;
|
||||
}
|
||||
|
||||
// Diagnostics helpers are static (reachable from static handler paths), so
|
||||
// they use the platform injected into the backend active on this thread and
|
||||
// fall back to the process-wide singleton only when no run is bound.
|
||||
private static IHostMemory ResolveDiagnosticsHostMemory()
|
||||
{
|
||||
return _activeExecutionBackend?._hostMemory ?? HostPlatform.Current.Memory;
|
||||
}
|
||||
|
||||
private static bool IsReadableProtection(uint protect)
|
||||
{
|
||||
if ((protect & 0x100) != 0 || (protect & 1) != 0)
|
||||
|
||||
@@ -9,7 +9,9 @@ using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using SharpEmu.Core.Cpu.Disasm;
|
||||
using SharpEmu.Core.Cpu.Native.Windows;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.HLE.Host;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
@@ -22,12 +24,12 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_RAW_HANDLER"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
_rawExceptionHandlerStub = CreateExceptionHandlerTrampoline(RawVectoredHandlerPtrManaged);
|
||||
_rawExceptionHandlerStub = _faultHandling.CreateHandlerThunk(RawVectoredHandlerPtrManaged, _hostRspSlotTlsIndex, _tlsGetValueAddress);
|
||||
if (_rawExceptionHandlerStub == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to create raw exception handler trampoline");
|
||||
}
|
||||
_rawExceptionHandler = (nint)AddVectoredExceptionHandler(1u, _rawExceptionHandlerStub);
|
||||
_rawExceptionHandler = _faultHandling.AddFirstChanceHandler(_rawExceptionHandlerStub);
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Raw exception handler installed: 0x{_rawExceptionHandler:X16}");
|
||||
}
|
||||
else
|
||||
@@ -37,22 +39,22 @@ public sealed partial class DirectExecutionBackend
|
||||
|
||||
_handlerDelegate = VectoredHandler;
|
||||
_handlerHandle = GCHandle.Alloc(_handlerDelegate);
|
||||
_exceptionHandlerStub = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_handlerDelegate));
|
||||
_exceptionHandlerStub = _faultHandling.CreateHandlerThunk(Marshal.GetFunctionPointerForDelegate(_handlerDelegate), _hostRspSlotTlsIndex, _tlsGetValueAddress);
|
||||
if (_exceptionHandlerStub == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to create exception handler trampoline");
|
||||
}
|
||||
_exceptionHandler = (nint)AddVectoredExceptionHandler(1u, _exceptionHandlerStub);
|
||||
_exceptionHandler = _faultHandling.AddFirstChanceHandler(_exceptionHandlerStub);
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Exception handler installed: 0x{_exceptionHandler:X16}");
|
||||
|
||||
_unhandledFilterDelegate = UnhandledExceptionFilter;
|
||||
_unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate);
|
||||
_unhandledFilterStub = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate));
|
||||
_unhandledFilterStub = _faultHandling.CreateHandlerThunk(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate), _hostRspSlotTlsIndex, _tlsGetValueAddress);
|
||||
if (_unhandledFilterStub == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to create unhandled exception filter trampoline");
|
||||
}
|
||||
SetUnhandledExceptionFilter(_unhandledFilterStub);
|
||||
_faultHandling.SetUnhandledFilter(_unhandledFilterStub);
|
||||
}
|
||||
|
||||
private unsafe int UnhandledExceptionFilter(void* exceptionInfo)
|
||||
@@ -60,8 +62,8 @@ public sealed partial class DirectExecutionBackend
|
||||
try
|
||||
{
|
||||
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
|
||||
ulong rip = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, 248);
|
||||
ulong rsp = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, 152);
|
||||
ulong rip = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, CTX_RIP);
|
||||
ulong rsp = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, CTX_RSP);
|
||||
Console.Error.WriteLine("[LOADER][FATAL] Unhandled exception filter fired.");
|
||||
Console.Error.WriteLine($"[LOADER][FATAL] Code: 0x{exceptionRecord->ExceptionCode:X8}");
|
||||
Console.Error.WriteLine($"[LOADER][FATAL] Exception Address: 0x{(ulong)(nint)exceptionRecord->ExceptionAddress:X16}");
|
||||
@@ -100,10 +102,19 @@ public sealed partial class DirectExecutionBackend
|
||||
return 0;
|
||||
}
|
||||
|
||||
ulong rip = ReadCtxU64(contextRecord, 248);
|
||||
ulong rsp = ReadCtxU64(contextRecord, 152);
|
||||
ulong rip = ReadCtxU64(contextRecord, CTX_RIP);
|
||||
ulong rsp = ReadCtxU64(contextRecord, CTX_RSP);
|
||||
|
||||
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
|
||||
// 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 == WindowsFaultCodes.AccessViolation && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
@@ -118,10 +129,10 @@ public sealed partial class DirectExecutionBackend
|
||||
|
||||
switch (exceptionCode)
|
||||
{
|
||||
case 3221225477u:
|
||||
case WindowsFaultCodes.AccessViolation:
|
||||
LogAccessViolationTrace(exceptionAddress, exceptionRecord);
|
||||
break;
|
||||
case 3221226505u:
|
||||
case WindowsFaultCodes.FastFail:
|
||||
{
|
||||
ulong p0 = exceptionRecord->NumberParameters >= 1 ? (*exceptionRecord->ExceptionInformation) : 0;
|
||||
ulong p1 = exceptionRecord->NumberParameters >= 2 ? exceptionRecord->ExceptionInformation[1] : 0;
|
||||
@@ -131,21 +142,21 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
}
|
||||
|
||||
ulong rax = ReadCtxU64(contextRecord, 120);
|
||||
ulong rbx = ReadCtxU64(contextRecord, 144);
|
||||
ulong rcx = ReadCtxU64(contextRecord, 128);
|
||||
ulong rdx = ReadCtxU64(contextRecord, 136);
|
||||
ulong rsi = ReadCtxU64(contextRecord, 168);
|
||||
ulong rdi = ReadCtxU64(contextRecord, 176);
|
||||
ulong rbp = ReadCtxU64(contextRecord, 160);
|
||||
ulong r8 = ReadCtxU64(contextRecord, 184);
|
||||
ulong r9 = ReadCtxU64(contextRecord, 192);
|
||||
ulong r10 = ReadCtxU64(contextRecord, 200);
|
||||
ulong r11 = ReadCtxU64(contextRecord, 208);
|
||||
ulong r12 = ReadCtxU64(contextRecord, 216);
|
||||
ulong r13 = ReadCtxU64(contextRecord, 224);
|
||||
ulong r14 = ReadCtxU64(contextRecord, 232);
|
||||
ulong r15 = ReadCtxU64(contextRecord, 240);
|
||||
ulong rax = ReadCtxU64(contextRecord, CTX_RAX);
|
||||
ulong rbx = ReadCtxU64(contextRecord, CTX_RBX);
|
||||
ulong rcx = ReadCtxU64(contextRecord, CTX_RCX);
|
||||
ulong rdx = ReadCtxU64(contextRecord, CTX_RDX);
|
||||
ulong rsi = ReadCtxU64(contextRecord, CTX_RSI);
|
||||
ulong rdi = ReadCtxU64(contextRecord, CTX_RDI);
|
||||
ulong rbp = ReadCtxU64(contextRecord, CTX_RBP);
|
||||
ulong r8 = ReadCtxU64(contextRecord, CTX_R8);
|
||||
ulong r9 = ReadCtxU64(contextRecord, CTX_R9);
|
||||
ulong r10 = ReadCtxU64(contextRecord, CTX_R10);
|
||||
ulong r11 = ReadCtxU64(contextRecord, CTX_R11);
|
||||
ulong r12 = ReadCtxU64(contextRecord, CTX_R12);
|
||||
ulong r13 = ReadCtxU64(contextRecord, CTX_R13);
|
||||
ulong r14 = ReadCtxU64(contextRecord, CTX_R14);
|
||||
ulong r15 = ReadCtxU64(contextRecord, CTX_R15);
|
||||
|
||||
Console.Error.WriteLine("[LOADER][INFO] =========================================");
|
||||
Console.Error.WriteLine("[LOADER][INFO] NATIVE EXCEPTION CAUGHT!");
|
||||
@@ -176,7 +187,7 @@ public sealed partial class DirectExecutionBackend
|
||||
|
||||
ulong accessType = 0;
|
||||
ulong target = 0;
|
||||
if (exceptionCode == 3221225477u && exceptionRecord->NumberParameters >= 2)
|
||||
if (exceptionCode == WindowsFaultCodes.AccessViolation && exceptionRecord->NumberParameters >= 2)
|
||||
{
|
||||
accessType = *exceptionRecord->ExceptionInformation;
|
||||
target = exceptionRecord->ExceptionInformation[1];
|
||||
@@ -189,9 +200,9 @@ public sealed partial class DirectExecutionBackend
|
||||
};
|
||||
Console.Error.WriteLine("[LOADER][INFO] AV access: " + accessText);
|
||||
Console.Error.WriteLine($"[LOADER][INFO] AV target: 0x{target:X16}");
|
||||
if (VirtualQuery((void*)target, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) != 0)
|
||||
if (_hostMemory.Query(target, out var mbi))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][INFO] AV target region: base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} state=0x{mbi.State:X08} protect=0x{mbi.Protect:X08}");
|
||||
Console.Error.WriteLine($"[LOADER][INFO] AV target region: base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} state=0x{mbi.RawState:X08} protect=0x{mbi.RawProtection:X08}");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -239,7 +250,7 @@ public sealed partial class DirectExecutionBackend
|
||||
|
||||
switch (exceptionCode)
|
||||
{
|
||||
case 3221225477u:
|
||||
case WindowsFaultCodes.AccessViolation:
|
||||
Console.Error.WriteLine("[LOADER][ERROR] Type: Access Violation");
|
||||
Console.Error.WriteLine("[LOADER][ERROR] This usually means:");
|
||||
Console.Error.WriteLine("[LOADER][ERROR] - Guest code called an unmapped import");
|
||||
@@ -286,11 +297,11 @@ public sealed partial class DirectExecutionBackend
|
||||
DumpGuestReferenceDiagnostics();
|
||||
DumpGuestPointerWindowDiagnostics();
|
||||
break;
|
||||
case 2147483651u:
|
||||
case WindowsFaultCodes.Breakpoint:
|
||||
Console.Error.WriteLine("[LOADER][WARNING] Type: Breakpoint (int3)");
|
||||
Console.Error.WriteLine("[LOADER][WARNING] Unexpected breakpoint in direct-bridge mode");
|
||||
break;
|
||||
case 3221225501u:
|
||||
case WindowsFaultCodes.IllegalInstruction:
|
||||
Console.Error.WriteLine("[LOADER][INFO] Type: Illegal Instruction");
|
||||
break;
|
||||
}
|
||||
@@ -323,8 +334,8 @@ public sealed partial class DirectExecutionBackend
|
||||
EXCEPTION_POINTERS* pointers = (EXCEPTION_POINTERS*)exceptionInfo;
|
||||
EXCEPTION_RECORD* record = pointers->ExceptionRecord;
|
||||
void* contextRecord = pointers->ContextRecord;
|
||||
ulong rip = contextRecord != null ? ReadCtxU64(contextRecord, 248) : 0;
|
||||
ulong rsp = contextRecord != null ? ReadCtxU64(contextRecord, 152) : 0;
|
||||
ulong rip = contextRecord != null ? ReadCtxU64(contextRecord, CTX_RIP) : 0;
|
||||
ulong rsp = contextRecord != null ? ReadCtxU64(contextRecord, CTX_RSP) : 0;
|
||||
ulong accessType = record->NumberParameters >= 1 ? *record->ExceptionInformation : 0;
|
||||
ulong target = record->NumberParameters >= 2 ? record->ExceptionInformation[1] : 0;
|
||||
Console.Error.WriteLine(
|
||||
@@ -470,7 +481,7 @@ public sealed partial class DirectExecutionBackend
|
||||
ulong address = scanBase;
|
||||
while (address < scanEnd)
|
||||
{
|
||||
if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
||||
if (!_hostMemory.Query(address, out var mbi))
|
||||
{
|
||||
break;
|
||||
}
|
||||
@@ -482,9 +493,9 @@ public sealed partial class DirectExecutionBackend
|
||||
break;
|
||||
}
|
||||
|
||||
if (mbi.State == MEM_COMMIT &&
|
||||
IsReadableProtection(mbi.Protect) &&
|
||||
IsExecutableProtection(mbi.Protect))
|
||||
if (mbi.State == HostRegionState.Committed &&
|
||||
IsReadableProtection(mbi.RawProtection) &&
|
||||
IsExecutableProtection(mbi.RawProtection))
|
||||
{
|
||||
ScanExecutableRegionForTargetReferences(regionBase, regionEnd, targetList, hitCounts, maxHitsPerTarget);
|
||||
}
|
||||
@@ -789,13 +800,13 @@ public sealed partial class DirectExecutionBackend
|
||||
return false;
|
||||
}
|
||||
|
||||
if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
||||
if (!_hostMemory.Query(address, out var mbi))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ulong regionEnd = mbi.BaseAddress + mbi.RegionSize;
|
||||
if (mbi.State != MEM_COMMIT || !IsReadableProtection(mbi.Protect) || regionEnd <= address || address > regionEnd - 8)
|
||||
if (mbi.State != HostRegionState.Committed || !IsReadableProtection(mbi.RawProtection) || regionEnd <= address || address > regionEnd - 8)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -907,25 +918,25 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (VirtualQuery((void*)faultAddress, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
||||
if (!_hostMemory.Query(faultAddress, out var mbi))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ulong pageBase = faultAddress & 0xFFFFFFFFFFFFF000uL;
|
||||
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.AllocationProtect);
|
||||
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.RawAllocationProtection);
|
||||
int traceIndex = Interlocked.Increment(ref _lazyCommitTraceCount);
|
||||
bool traceLazyCommit = ShouldTraceLazyCommit(traceIndex);
|
||||
if (traceLazyCommit)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-query#{traceIndex}: fault=0x{faultAddress:X16} owner={owner} rip=0x{rip:X16} rsp=0x{rsp:X16} state=0x{mbi.State:X08} base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} alloc=0x{mbi.AllocationProtect:X08} prot=0x{mbi.Protect:X08}");
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-query#{traceIndex}: fault=0x{faultAddress:X16} owner={owner} rip=0x{rip:X16} rsp=0x{rsp:X16} state=0x{mbi.RawState:X08} base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} alloc=0x{mbi.RawAllocationProtection:X08} prot=0x{mbi.RawProtection:X08}");
|
||||
}
|
||||
|
||||
if (mbi.State == 4096 && IsAccessCompatible(accessType, mbi.Protect))
|
||||
if (mbi.State == HostRegionState.Committed && IsAccessCompatible(accessType, mbi.RawProtection))
|
||||
{
|
||||
if (traceLazyCommit)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit-race#{traceIndex}: fault=0x{faultAddress:X16} protect=0x{mbi.Protect:X08}");
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit-race#{traceIndex}: fault=0x{faultAddress:X16} protect=0x{mbi.RawProtection:X08}");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -934,10 +945,10 @@ public sealed partial class DirectExecutionBackend
|
||||
ulong committedBase = 0;
|
||||
ulong committedSize = 0;
|
||||
|
||||
if (mbi.State == 65536)
|
||||
if (mbi.State == HostRegionState.Free)
|
||||
{
|
||||
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var windowBase, out var windowSize) &&
|
||||
TryReserveThenCommit(windowBase, windowSize, windowBase, windowSize, commitProtect))
|
||||
TryReserveThenCommit(_hostMemory, windowBase, windowSize, windowBase, windowSize, commitProtect))
|
||||
{
|
||||
committed = true;
|
||||
committedBase = windowBase;
|
||||
@@ -946,7 +957,7 @@ public sealed partial class DirectExecutionBackend
|
||||
else
|
||||
{
|
||||
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
||||
if (TryReserveThenCommit(largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
|
||||
if (TryReserveThenCommit(_hostMemory, largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
|
||||
{
|
||||
committed = true;
|
||||
committedBase = largeBase;
|
||||
@@ -957,13 +968,13 @@ public sealed partial class DirectExecutionBackend
|
||||
if (!committed)
|
||||
{
|
||||
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
||||
if (TryReserveThenCommit(region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
|
||||
if (TryReserveThenCommit(_hostMemory, region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
|
||||
{
|
||||
committed = true;
|
||||
committedBase = region64kBase;
|
||||
committedSize = 65536uL;
|
||||
}
|
||||
else if (TryReserveThenCommit(pageBase, 4096uL, pageBase, 4096uL, commitProtect))
|
||||
else if (TryReserveThenCommit(_hostMemory, pageBase, 4096uL, pageBase, 4096uL, commitProtect))
|
||||
{
|
||||
committed = true;
|
||||
committedBase = pageBase;
|
||||
@@ -976,7 +987,7 @@ public sealed partial class DirectExecutionBackend
|
||||
return false;
|
||||
}
|
||||
|
||||
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
|
||||
TryCommitRange(_hostMemory, pageBase + 4096, 4096uL, commitProtect);
|
||||
if (traceLazyCommit)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-reserve-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
||||
@@ -984,13 +995,13 @@ public sealed partial class DirectExecutionBackend
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mbi.State != 8192)
|
||||
if (mbi.State != HostRegionState.Reserved)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var commitWindowBase, out var commitWindowSize) &&
|
||||
TryCommitRange(commitWindowBase, commitWindowSize, commitProtect))
|
||||
TryCommitRange(_hostMemory, commitWindowBase, commitWindowSize, commitProtect))
|
||||
{
|
||||
committed = true;
|
||||
committedBase = commitWindowBase;
|
||||
@@ -999,7 +1010,7 @@ public sealed partial class DirectExecutionBackend
|
||||
else
|
||||
{
|
||||
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
||||
if (TryCommitRange(largeCommitBase, 2097152uL, commitProtect))
|
||||
if (TryCommitRange(_hostMemory, largeCommitBase, 2097152uL, commitProtect))
|
||||
{
|
||||
committed = true;
|
||||
committedBase = largeCommitBase;
|
||||
@@ -1010,19 +1021,19 @@ public sealed partial class DirectExecutionBackend
|
||||
if (!committed)
|
||||
{
|
||||
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
||||
if (TryCommitRange(region64kBase, 65536uL, commitProtect))
|
||||
if (TryCommitRange(_hostMemory, region64kBase, 65536uL, commitProtect))
|
||||
{
|
||||
committed = true;
|
||||
committedBase = region64kBase;
|
||||
committedSize = 65536uL;
|
||||
}
|
||||
else if (TryCommitRange(pageBase, 8192uL, commitProtect))
|
||||
else if (TryCommitRange(_hostMemory, pageBase, 8192uL, commitProtect))
|
||||
{
|
||||
committed = true;
|
||||
committedBase = pageBase;
|
||||
committedSize = 8192uL;
|
||||
}
|
||||
else if (TryCommitRange(pageBase, 4096uL, commitProtect))
|
||||
else if (TryCommitRange(_hostMemory, pageBase, 4096uL, commitProtect))
|
||||
{
|
||||
committed = true;
|
||||
committedBase = pageBase;
|
||||
@@ -1035,7 +1046,7 @@ public sealed partial class DirectExecutionBackend
|
||||
return false;
|
||||
}
|
||||
|
||||
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
|
||||
TryCommitRange(_hostMemory, pageBase + 4096, 4096uL, commitProtect);
|
||||
if (traceLazyCommit)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
||||
@@ -1076,31 +1087,33 @@ public sealed partial class DirectExecutionBackend
|
||||
return true;
|
||||
}
|
||||
|
||||
static unsafe bool TryCommitRange(ulong baseAddress, ulong length, uint protection)
|
||||
// The commit protection is one of the two raw values ResolveLazyCommitProtection
|
||||
// produces (0x40 RWX / 0x04 RW); the enum mapping reproduces those exactly.
|
||||
static bool TryCommitRange(IHostMemory hostMemory, ulong baseAddress, ulong length, uint protection)
|
||||
{
|
||||
if (length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return VirtualAlloc((void*)baseAddress, (nuint)length, 4096u, protection) != null;
|
||||
return hostMemory.Commit(baseAddress, length, protection == 64u ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite);
|
||||
}
|
||||
|
||||
static unsafe bool TryReserveRange(ulong baseAddress, ulong length)
|
||||
static bool TryReserveRange(IHostMemory hostMemory, ulong baseAddress, ulong length)
|
||||
{
|
||||
if (length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return VirtualAlloc((void*)baseAddress, (nuint)length, 8192u, 4u) != null;
|
||||
return hostMemory.Reserve(baseAddress, length, HostPageProtection.ReadWrite) != 0;
|
||||
}
|
||||
|
||||
static bool TryReserveThenCommit(ulong reserveAddress, ulong reserveSize, ulong commitAddress, ulong commitSize, uint protection)
|
||||
static bool TryReserveThenCommit(IHostMemory hostMemory, ulong reserveAddress, ulong reserveSize, ulong commitAddress, ulong commitSize, uint protection)
|
||||
{
|
||||
if (!TryReserveRange(reserveAddress, reserveSize))
|
||||
if (!TryReserveRange(hostMemory, reserveAddress, reserveSize))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return TryCommitRange(commitAddress, commitSize, protection);
|
||||
return TryCommitRange(hostMemory, commitAddress, commitSize, protection);
|
||||
}
|
||||
|
||||
static bool IsAccessCompatible(ulong accessType, uint protection)
|
||||
|
||||
@@ -8,7 +8,10 @@ using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using SharpEmu.Core.Cpu;
|
||||
using SharpEmu.Core.Cpu.Native.Windows;
|
||||
using SharpEmu.Core.Loader;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.HLE.Host;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
@@ -19,6 +22,16 @@ public sealed partial class DirectExecutionBackend
|
||||
|
||||
private static ulong ImportDispatchGatewayManaged(nint backendHandle, int importIndex, nint argPackPtr)
|
||||
{
|
||||
if (LogThreadMode)
|
||||
{
|
||||
_threadModeGatewayCalls++;
|
||||
_threadModeGatewayDepth++;
|
||||
if (!_threadModeGatewayFirstLogged)
|
||||
{
|
||||
_threadModeGatewayFirstLogged = true;
|
||||
TraceThreadMode($"gateway_first import={importIndex} total={_threadModeGatewayCalls}");
|
||||
}
|
||||
}
|
||||
try
|
||||
{
|
||||
if (!(GCHandle.FromIntPtr(backendHandle).Target is DirectExecutionBackend directExecutionBackend))
|
||||
@@ -36,6 +49,13 @@ public sealed partial class DirectExecutionBackend
|
||||
$"[LOADER][ERROR] ImportDispatchGatewayManaged exception: {ex.GetType().Name}: {ex.Message}");
|
||||
return 18446744071562199298uL;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (LogThreadMode)
|
||||
{
|
||||
_threadModeGatewayDepth--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe static int RawVectoredHandlerManaged(void* exceptionInfo)
|
||||
@@ -51,24 +71,30 @@ public sealed partial class DirectExecutionBackend
|
||||
private unsafe static int TryRecoverUnresolvedSentinel(void* exceptionInfo)
|
||||
{
|
||||
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
|
||||
if (exceptionRecord->ExceptionCode != 3221225477u)
|
||||
if (exceptionRecord->ExceptionCode != WindowsFaultCodes.AccessViolation)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
void* contextRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord;
|
||||
ulong value = ReadCtxU64(contextRecord, 248);
|
||||
ulong value = ReadCtxU64(contextRecord, CTX_RIP);
|
||||
ulong value2 = (ulong)exceptionRecord->ExceptionAddress;
|
||||
if (!IsUnresolvedSentinel(value) && !IsUnresolvedSentinel(value2))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
ulong rsp = ReadCtxU64(contextRecord, 152);
|
||||
WriteCtxU64(contextRecord, 120, 0uL);
|
||||
ulong rsp = ReadCtxU64(contextRecord, CTX_RSP);
|
||||
WriteCtxU64(contextRecord, CTX_RAX, 0uL);
|
||||
if (TryGetPlausibleReturnFromStack(rsp, out var returnRip, out var nextRsp))
|
||||
{
|
||||
WriteCtxU64(contextRecord, 152, nextRsp);
|
||||
WriteCtxU64(contextRecord, 248, returnRip);
|
||||
WriteCtxU64(contextRecord, CTX_RSP, nextRsp);
|
||||
WriteCtxU64(contextRecord, CTX_RIP, returnRip);
|
||||
Interlocked.Increment(ref _rawSentinelRecoveries);
|
||||
if (LogThreadMode)
|
||||
{
|
||||
TraceThreadMode(
|
||||
$"sentinel_recover rip=0x{value:X16} -> 0x{returnRip:X16} rsp=0x{rsp:X16} -> 0x{nextRsp:X16} " +
|
||||
$"gateway_depth={_threadModeGatewayDepth}");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
@@ -87,12 +113,13 @@ public sealed partial class DirectExecutionBackend
|
||||
LastError = "Import dispatch called without active CPU context";
|
||||
return 18446744071562199298uL;
|
||||
}
|
||||
if ((uint)importIndex >= (uint)_importEntries.Length)
|
||||
var importEntries = _importEntries;
|
||||
if ((uint)importIndex >= (uint)importEntries.Length)
|
||||
{
|
||||
LastError = $"Import dispatch index out of range: {importIndex}";
|
||||
return 18446744071562199042uL;
|
||||
}
|
||||
ImportStubEntry importStubEntry = _importEntries[importIndex];
|
||||
ImportStubEntry importStubEntry = importEntries[importIndex];
|
||||
int num2 = Volatile.Read(in _rawSentinelRecoveries);
|
||||
if (num2 != _lastReportedRawSentinelRecoveries)
|
||||
{
|
||||
@@ -136,6 +163,14 @@ public sealed partial class DirectExecutionBackend
|
||||
*(ulong*)(xmmSlot + 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];
|
||||
@@ -186,16 +221,6 @@ public sealed partial class DirectExecutionBackend
|
||||
TraceGuestContext(
|
||||
$"import dispatch={num} nid={importStubEntry.Nid} ret=0x{num7:X16} managed={Environment.CurrentManagedThreadId} guest=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} fiber=0x{GuestThreadExecution.CurrentFiberAddress:X16} active={HasActiveExecutionThread}");
|
||||
}
|
||||
if (_logBootstrap && string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
|
||||
{
|
||||
string symbolText = "<unreadable>";
|
||||
if (TryReadAsciiZ(value2, 256, out var sym))
|
||||
{
|
||||
symbolText = sym;
|
||||
}
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] bootstrap_call#{num}: op=0x{value:X16} sym_ptr=0x{value2:X16} sym='{symbolText}' out_ptr=0x{num3:X16} ret=0x{num7:X16}");
|
||||
}
|
||||
if (!isGuestWorker &&
|
||||
!ActiveForcedGuestExit &&
|
||||
ShouldForceGuestExitOnImportLoop(importStubEntry.Nid, num7, num, value, value2) &&
|
||||
@@ -204,6 +229,33 @@ 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;
|
||||
}
|
||||
if (_hostShutdownRequested)
|
||||
{
|
||||
if (isGuestWorker &&
|
||||
TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, "host shutdown"))
|
||||
{
|
||||
cpuContext[CpuRegister.Rax] = 0uL;
|
||||
return 0uL;
|
||||
}
|
||||
|
||||
if (!isGuestWorker &&
|
||||
TryAbortGuestForHostShutdown(argPackPtr, num, num7))
|
||||
{
|
||||
cpuContext[CpuRegister.Rax] = 1uL;
|
||||
return 1uL;
|
||||
}
|
||||
}
|
||||
bool flag0 = ShouldSuppressStrlenTrace(importStubEntry.Nid);
|
||||
bool flag = num7 >= 2156221920u && num7 <= 2156225024u;
|
||||
bool flag2 = num7 >= 2156351360u && num7 <= 2156352080u;
|
||||
@@ -211,12 +263,13 @@ 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 ||
|
||||
(importStubEntry.Nid == "tsvEmnenz48" && (num <= 256 || num % 1000 == 0L)) ||
|
||||
(importStubEntry.Nid == "rTXw65xmLIA" && (num <= 256 || num % 128 == 0)) ||
|
||||
((traceFlags & ImportStubTraceFlags.PeriodicEvery1000) != 0 && (num <= 256 || num % 1000 == 0L)) ||
|
||||
((traceFlags & ImportStubTraceFlags.PeriodicEvery128) != 0 && (num <= 256 || num % 128 == 0)) ||
|
||||
flag ||
|
||||
flag2 ||
|
||||
flag3;
|
||||
@@ -277,15 +330,15 @@ public sealed partial class DirectExecutionBackend
|
||||
cpuContext[CpuRegister.Rsi],
|
||||
cpuContext[CpuRegister.Rdx]);
|
||||
}
|
||||
if (importStubEntry.Nid == "8zTFvBIAIN8" && num <= 256)
|
||||
if ((traceFlags & ImportStubTraceFlags.Memset) != 0 && num <= 256)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] memset#{num}: dst=0x{cpuContext[CpuRegister.Rdi]:X16} val=0x{cpuContext[CpuRegister.Rsi] & 0xFF:X2} len=0x{cpuContext[CpuRegister.Rdx]:X16} ret=0x{num7:X16}");
|
||||
}
|
||||
if (importStubEntry.Nid == "tsvEmnenz48" && num <= 64)
|
||||
if ((traceFlags & ImportStubTraceFlags.CxaAtexit) != 0 && num <= 64)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] __cxa_atexit#{num}: func=0x{cpuContext[CpuRegister.Rdi]:X16} arg=0x{cpuContext[CpuRegister.Rsi]:X16} dso=0x{cpuContext[CpuRegister.Rdx]:X16} ret=0x{num7:X16}");
|
||||
}
|
||||
if (importStubEntry.Nid == "bzQExy189ZI" || importStubEntry.Nid == "8G2LB+A3rzg")
|
||||
if ((traceFlags & ImportStubTraceFlags.RawArgs) != 0)
|
||||
{
|
||||
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}");
|
||||
}
|
||||
@@ -315,7 +368,7 @@ public sealed partial class DirectExecutionBackend
|
||||
Console.Error.Flush();
|
||||
}
|
||||
}
|
||||
if (importStubEntry.Nid == "Ou3iL1abvng")
|
||||
if ((traceFlags & ImportStubTraceFlags.StackChkFail) != 0)
|
||||
{
|
||||
if (_logStackCheck)
|
||||
{
|
||||
@@ -347,16 +400,20 @@ public sealed partial class DirectExecutionBackend
|
||||
ActiveGuestReturnSlotAddress);
|
||||
try
|
||||
{
|
||||
if (string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
|
||||
if (importStubEntry.Kind == ImportStubKind.BootstrapBridge)
|
||||
{
|
||||
if (_logBootstrap)
|
||||
{
|
||||
RecordDeferredBootstrapTrace(num, value, value2, num3, num7);
|
||||
}
|
||||
|
||||
orbisGen2Result = DispatchBootstrapBridge();
|
||||
}
|
||||
else if (string.Equals(importStubEntry.Nid, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal) ||
|
||||
string.Equals(importStubEntry.Nid, "LwG8g3niqwA", StringComparison.Ordinal))
|
||||
else if (importStubEntry.Kind == ImportStubKind.KernelDynlibDlsym)
|
||||
{
|
||||
orbisGen2Result = DispatchKernelDynlibDlsym();
|
||||
}
|
||||
else if (string.Equals(importStubEntry.Nid, "r8mvOaWdi28", StringComparison.Ordinal))
|
||||
else if (importStubEntry.Kind == ImportStubKind.Il2CppApiLookupSymbol)
|
||||
{
|
||||
orbisGen2Result = DispatchIl2CppApiLookupSymbol();
|
||||
}
|
||||
@@ -511,6 +568,7 @@ public sealed partial class DirectExecutionBackend
|
||||
cpuContext.GetXmmRegister(0, out var returnXmm0Low, out var returnXmm0High);
|
||||
*(ulong*)(argPackPtr - 0x80) = returnXmm0Low;
|
||||
*(ulong*)(argPackPtr - 0x80 + 8) = returnXmm0High;
|
||||
DrainDeferredBootstrapTraces();
|
||||
return cpuContext[CpuRegister.Rax];
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -519,6 +577,7 @@ public sealed partial class DirectExecutionBackend
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] {LastError}");
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] {ex.StackTrace}");
|
||||
cpuContext[CpuRegister.Rax] = 18446744071562199298uL;
|
||||
DrainDeferredBootstrapTraces();
|
||||
return 18446744071562199298uL;
|
||||
}
|
||||
}
|
||||
@@ -671,6 +730,7 @@ public sealed partial class DirectExecutionBackend
|
||||
"eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
|
||||
"qvMUCyyaCSI" or // sceKernelAprSubmitCommandBufferAndGetId
|
||||
"Q2V+iqvjgC0" or // vsnprintf
|
||||
"AV6ipCNa4Rw" or // strcasecmp
|
||||
"q1cHNfGycLI" or // scePadRead
|
||||
"xk0AcarP3V4" or // scePadOpen
|
||||
"yH17Q6NWtVg" or // sceUserServiceGetEvent
|
||||
@@ -694,6 +754,9 @@ 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;
|
||||
@@ -706,6 +769,7 @@ public sealed partial class DirectExecutionBackend
|
||||
if (!expectedFileProbeMiss &&
|
||||
!expectedTimedWaitTimeout &&
|
||||
!expectedEqueueTimeout &&
|
||||
!expectedEventFlagTimeout &&
|
||||
!expectedMutexTrylockBusy &&
|
||||
!expectedUserServiceNoEvent &&
|
||||
!expectedPrivacyInvalidParameter)
|
||||
@@ -768,11 +832,15 @@ public sealed partial class DirectExecutionBackend
|
||||
return !_logUsleep;
|
||||
}
|
||||
|
||||
// Mutex lock uses this block-capable leaf path. Keep it out of the no-block subset.
|
||||
return nid is
|
||||
"9UK1vLZQft4" or // scePthreadMutexLock
|
||||
"tn3VlD0hG60" or // scePthreadMutexUnlock
|
||||
"7H0iTOciTLo" or // pthread_mutex_lock
|
||||
"tn3VlD0hG60" or // scePthreadMutexUnlock
|
||||
"2Z+PpY6CaJg" or // pthread_mutex_unlock
|
||||
"EgmLo6EWgso" or // pthread_rwlock_unlock
|
||||
"+L98PIbGttk" or // scePthreadRwlockUnlock
|
||||
"q1cHNfGycLI" or // scePadRead
|
||||
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
|
||||
"zgXifHT9ErY" or // sceVideoOutIsFlipPending
|
||||
"V++UgBtQhn0" or // sceAgcGetDataPacketPayloadAddress
|
||||
@@ -831,10 +899,11 @@ public sealed partial class DirectExecutionBackend
|
||||
"Q2V+iqvjgC0" or // vsnprintf
|
||||
"j4ViWNHEgww" or // strlen
|
||||
"5jNubw4vlAA" or // strnlen
|
||||
"LHMrG7e8G78" or // wcslen
|
||||
"LHMrG7e8G78" or // wcsmisc
|
||||
"WkkeywLJcgU" or // wcslen
|
||||
"Ovb2dSJOAuE" or // strcmp
|
||||
"aesyjrHVWy4" or // strncmp
|
||||
"AV6ipCNa4Rw" or // strcasecmp
|
||||
"pNtJdE3x49E" or // wcscmp
|
||||
"fV2xHER+bKE" or // wcscoll
|
||||
"E8wCoUEbfzk" or // wcsncmp
|
||||
@@ -845,12 +914,7 @@ public sealed partial class DirectExecutionBackend
|
||||
"6ULAa0fq4jA" or // scePthreadRwlockInit
|
||||
"1471ajPzxh0" or // pthread_rwlock_destroy
|
||||
"BB+kb08Tl9A" or // scePthreadRwlockDestroy
|
||||
"iGjsr1WAtI0" or // pthread_rwlock_rdlock
|
||||
"Ox9i0c7L5w0" or // scePthreadRwlockRdlock
|
||||
"sIlRvQqsN2Y" or // pthread_rwlock_wrlock
|
||||
"mqdNorrB+gI" or // scePthreadRwlockWrlock
|
||||
"EgmLo6EWgso" or // pthread_rwlock_unlock
|
||||
"+L98PIbGttk" or // scePthreadRwlockUnlock
|
||||
// rwlock rd/wr lock removed (can block); init/destroy/unlock stay.
|
||||
"aI+OeCz8xrQ" or // scePthreadSelf
|
||||
"EotR8a3ASf4" or // pthread_self
|
||||
"eoht7mQOCmo" or // scePthreadGetspecific
|
||||
@@ -929,6 +993,31 @@ public sealed partial class DirectExecutionBackend
|
||||
return true;
|
||||
}
|
||||
|
||||
private unsafe bool TryAbortGuestForHostShutdown(nint argPackPtr, long dispatchIndex, ulong returnRip)
|
||||
{
|
||||
ulong hostExit = ActiveEntryReturnSentinelRip;
|
||||
if (hostExit < 65536 || !TryPatchActiveGuestReturnSlot(hostExit))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
try
|
||||
{
|
||||
*(ulong*)(argPackPtr + 96) = hostExit;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
ActiveForcedGuestExit = true;
|
||||
if (string.IsNullOrWhiteSpace(LastError))
|
||||
{
|
||||
LastError = "Host shutdown requested.";
|
||||
}
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] Guest unwind for host shutdown at import#{dispatchIndex} ret=0x{returnRip:X16} -> host_exit=0x{hostExit:X16}");
|
||||
return true;
|
||||
}
|
||||
|
||||
private unsafe bool TryCompleteGuestEntryToHostStub(nint argPackPtr, long dispatchIndex, ulong returnRip, string nid, string reason, ulong value)
|
||||
{
|
||||
ulong hostExit = ActiveEntryReturnSentinelRip;
|
||||
@@ -1035,7 +1124,15 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
|
||||
private static bool IsImportLoopGuardBoundary(string nid) =>
|
||||
string.Equals(nid, "1jfXLRVzisc", StringComparison.Ordinal);
|
||||
nid switch
|
||||
{
|
||||
"1jfXLRVzisc" => true, // sceKernelUsleep
|
||||
"QcteRwbsnV0" => true, // usleep
|
||||
"n88vx3C5nW8" => true, // gettimeofday
|
||||
"Zxa0VhQVTsk" => true, // sceKernelWaitSema
|
||||
"T72hz6ffq08" => true, // scePthreadYield
|
||||
_ => false
|
||||
};
|
||||
|
||||
private void ResetImportLoopPattern()
|
||||
{
|
||||
@@ -1275,30 +1372,485 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
ulong symbolNameAddress = cpuContext[CpuRegister.Rsi];
|
||||
ulong outputAddress = cpuContext[CpuRegister.Rdx];
|
||||
if (!TryReadAsciiZ(symbolNameAddress, 512, out var symbolName))
|
||||
|
||||
NormalizeKernelDynlibDlsymArguments(cpuContext, out var symbolNameAddress, out var outputAddress);
|
||||
try
|
||||
{
|
||||
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
|
||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
if (!TryReadAsciiZ(symbolNameAddress, 512, out var symbolName) ||
|
||||
!TryResolveDlsymGuestAddress(symbolName, out var resolvedAddress) ||
|
||||
outputAddress == 0 ||
|
||||
!TryWriteUInt64Compat(outputAddress, resolvedAddress))
|
||||
{
|
||||
return CompleteKernelDynlibDlsymFailure(cpuContext, outputAddress);
|
||||
}
|
||||
}
|
||||
if (!TryResolveRuntimeSymbolAddress(symbolName, out var resolvedAddress) &&
|
||||
!TryResolveRuntimeSymbolAlias(symbolName, out resolvedAddress))
|
||||
catch
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] sceKernelDlsym failed: handle=0x{cpuContext[CpuRegister.Rdi]:X} symbol='{symbolName}'");
|
||||
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
|
||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
if (outputAddress == 0L || !TryWriteUInt64Compat(outputAddress, resolvedAddress))
|
||||
{
|
||||
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
|
||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
return CompleteKernelDynlibDlsymFailure(cpuContext, outputAddress);
|
||||
}
|
||||
|
||||
cpuContext[CpuRegister.Rax] = 0uL;
|
||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static void NormalizeKernelDynlibDlsymArguments(
|
||||
CpuContext cpuContext,
|
||||
out ulong symbolNameAddress,
|
||||
out ulong outputAddress)
|
||||
{
|
||||
var handle = cpuContext[CpuRegister.Rdi];
|
||||
symbolNameAddress = cpuContext[CpuRegister.Rsi];
|
||||
outputAddress = cpuContext[CpuRegister.Rdx];
|
||||
|
||||
// Standalone bootstrap loaders sometimes call through the bridge with
|
||||
// (symbol_ptr, handle, out) while sceKernelDlsym is (handle, symbol_ptr, out).
|
||||
// Heuristic only: valid when RSI looks like a small handle and RDI is a guest pointer.
|
||||
if (symbolNameAddress < 0x10000 &&
|
||||
IsPlausibleDynlibSymbolPointer(handle))
|
||||
{
|
||||
symbolNameAddress = handle;
|
||||
handle = cpuContext[CpuRegister.Rsi];
|
||||
cpuContext[CpuRegister.Rdi] = handle;
|
||||
cpuContext[CpuRegister.Rsi] = symbolNameAddress;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsPlausibleDynlibSymbolPointer(ulong address)
|
||||
{
|
||||
return address >= 0x10000 && address < 0x0000_8000_0000_0000UL;
|
||||
}
|
||||
|
||||
private OrbisGen2Result CompleteKernelDynlibDlsymFailure(CpuContext cpuContext, ulong outputAddress)
|
||||
{
|
||||
if (outputAddress != 0)
|
||||
{
|
||||
_ = TryWriteUInt64Compat(outputAddress, 0);
|
||||
}
|
||||
|
||||
cpuContext[CpuRegister.Rax] = ulong.MaxValue;
|
||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private void ResetLazyDlsymStubState()
|
||||
{
|
||||
lock (_lazyDlsymStubGate)
|
||||
{
|
||||
_lazyDlsymStubCache.Clear();
|
||||
_lazyImportStubPoolMapped = false;
|
||||
_lazyImportStubPoolBase = 0;
|
||||
_lazyImportStubNextSlot = 0;
|
||||
_lazyImportStubPoolLimit = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryResolveDlsymGuestAddress(string symbolName, out ulong guestAddress)
|
||||
{
|
||||
guestAddress = 0;
|
||||
if (string.IsNullOrWhiteSpace(symbolName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Tier 0: ELF runtime symbols and aliases.
|
||||
if (TryResolveRuntimeSymbolAddress(symbolName, out guestAddress) ||
|
||||
TryResolveRuntimeSymbolAlias(symbolName, out guestAddress))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Tier 1: existing import stub entries (duplicate prevention).
|
||||
if (TryFindImportStubGuestAddress(symbolName, out guestAddress))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var hasAerolibSymbol = Aerolib.Instance.TryGetByExportName(symbolName, out var hleSymbol);
|
||||
if (hasAerolibSymbol)
|
||||
{
|
||||
if (HleDataSymbols.TryGetAddress(hleSymbol.Nid, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (TryResolveRuntimeSymbolAddress(hleSymbol.Nid, out guestAddress) ||
|
||||
TryResolveRuntimeSymbolAddress(hleSymbol.ExportName, out guestAddress) ||
|
||||
TryFindImportStubGuestAddress(hleSymbol.Nid, out guestAddress) ||
|
||||
TryFindImportStubGuestAddress(hleSymbol.ExportName, out guestAddress))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (Aerolib.Instance.TryGetByNid(symbolName, out hleSymbol))
|
||||
{
|
||||
if (HleDataSymbols.TryGetAddress(hleSymbol.Nid, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (TryFindImportStubGuestAddress(hleSymbol.Nid, out guestAddress) ||
|
||||
TryFindImportStubGuestAddress(hleSymbol.ExportName, out guestAddress))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
hasAerolibSymbol = true;
|
||||
}
|
||||
|
||||
// Tier 3: lazy materialization for callable HLE symbols missing from ELF imports.
|
||||
if (!TryResolveLazyDispatchTarget(symbolName, hasAerolibSymbol, in hleSymbol, out var dispatchNid, out var export))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryGetOrCreateLazyImportStub(
|
||||
dispatchNid,
|
||||
symbolName,
|
||||
hasAerolibSymbol ? hleSymbol : null,
|
||||
export,
|
||||
out guestAddress);
|
||||
}
|
||||
|
||||
private bool TryFindImportStubGuestAddress(string identifier, out ulong guestAddress)
|
||||
{
|
||||
guestAddress = 0;
|
||||
if (string.IsNullOrWhiteSpace(identifier))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var importEntries = _importEntries;
|
||||
for (var i = 0; i < importEntries.Length; i++)
|
||||
{
|
||||
var entry = importEntries[i];
|
||||
if (!ImportStubEntryMatchesIdentifier(entry, identifier))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.Address >= 0x10000)
|
||||
{
|
||||
guestAddress = entry.Address;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool ImportStubEntryMatchesIdentifier(in ImportStubEntry entry, string identifier)
|
||||
{
|
||||
if (string.Equals(entry.Nid, identifier, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (entry.Export is not { } export)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return string.Equals(export.Name, identifier, StringComparison.Ordinal) ||
|
||||
string.Equals(export.Nid, identifier, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool IsKernelDynlibDlsymIdentifier(string identifier)
|
||||
{
|
||||
return string.Equals(identifier, "sceKernelDlsym", StringComparison.Ordinal) ||
|
||||
string.Equals(identifier, KernelDynlibDlsymAerolibNid, StringComparison.Ordinal) ||
|
||||
string.Equals(identifier, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private bool TryResolveLazyDispatchTarget(
|
||||
string symbolName,
|
||||
bool hasAerolibSymbol,
|
||||
in SysAbiSymbol hleSymbol,
|
||||
out string dispatchNid,
|
||||
out ExportedFunction? export)
|
||||
{
|
||||
export = null;
|
||||
dispatchNid = string.Empty;
|
||||
|
||||
if (IsKernelDynlibDlsymIdentifier(symbolName))
|
||||
{
|
||||
dispatchNid = KernelDynlibDlsymAerolibNid;
|
||||
_ = _moduleManager.TryGetExport(dispatchNid, out export);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!hasAerolibSymbol)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (HleDataSymbols.TryGetAddress(hleSymbol.Nid, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_moduleManager.TryGetExport(hleSymbol.Nid, out export))
|
||||
{
|
||||
dispatchNid = hleSymbol.Nid;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryGetOrCreateLazyImportStub(
|
||||
string dispatchNid,
|
||||
string symbolName,
|
||||
SysAbiSymbol? hleSymbol,
|
||||
ExportedFunction? export,
|
||||
out ulong guestAddress)
|
||||
{
|
||||
guestAddress = 0;
|
||||
if (string.IsNullOrWhiteSpace(dispatchNid))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (_lazyDlsymStubGate)
|
||||
{
|
||||
if (TryGetCachedLazyDlsymAddress(dispatchNid, symbolName, out guestAddress))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!EnsureLazyImportStubPoolMapped())
|
||||
{
|
||||
LogLazyImportStubMaterializationFailure(
|
||||
"import stub region unresolved",
|
||||
dispatchNid,
|
||||
symbolName);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryAllocateLazyImportStubSlot(out guestAddress))
|
||||
{
|
||||
LogLazyImportStubMaterializationFailure(
|
||||
"lazy import stub pool exhausted",
|
||||
dispatchNid,
|
||||
symbolName);
|
||||
return false;
|
||||
}
|
||||
|
||||
var previousEntries = _importEntries;
|
||||
var importIndex = previousEntries.Length;
|
||||
var newEntries = new ImportStubEntry[importIndex + 1];
|
||||
if (importIndex > 0)
|
||||
{
|
||||
Array.Copy(previousEntries, newEntries, importIndex);
|
||||
}
|
||||
|
||||
newEntries[importIndex] = new ImportStubEntry(guestAddress, dispatchNid, export);
|
||||
|
||||
var hostTrampoline = CreateImportHandlerTrampoline(importIndex);
|
||||
if (hostTrampoline == 0 ||
|
||||
!PatchImportStub((nint)(long)guestAddress, hostTrampoline))
|
||||
{
|
||||
guestAddress = 0;
|
||||
LogLazyImportStubMaterializationFailure(
|
||||
"failed to patch lazy import stub trampoline",
|
||||
dispatchNid,
|
||||
symbolName);
|
||||
return false;
|
||||
}
|
||||
|
||||
_importEntries = newEntries;
|
||||
|
||||
RegisterDlsymRuntimeSymbolKeys(symbolName, dispatchNid, hleSymbol, guestAddress);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGetCachedLazyDlsymAddress(string dispatchNid, string symbolName, out ulong guestAddress)
|
||||
{
|
||||
if (_lazyDlsymStubCache.TryGetValue(dispatchNid, out guestAddress) ||
|
||||
_lazyDlsymStubCache.TryGetValue(symbolName, out guestAddress))
|
||||
{
|
||||
return guestAddress >= 0x10000;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void RegisterDlsymRuntimeSymbolKeys(
|
||||
string symbolName,
|
||||
string dispatchNid,
|
||||
SysAbiSymbol? hleSymbol,
|
||||
ulong guestAddress)
|
||||
{
|
||||
RegisterDlsymRuntimeSymbolKey(symbolName, guestAddress);
|
||||
RegisterDlsymRuntimeSymbolKey(dispatchNid, guestAddress);
|
||||
|
||||
if (IsKernelDynlibDlsymIdentifier(symbolName) || IsKernelDynlibDlsymIdentifier(dispatchNid))
|
||||
{
|
||||
RegisterDlsymRuntimeSymbolKey(RuntimeStubNids.KernelDynlibDlsym, guestAddress);
|
||||
RegisterDlsymRuntimeSymbolKey(KernelDynlibDlsymAerolibNid, guestAddress);
|
||||
RegisterDlsymRuntimeSymbolKey("sceKernelDlsym", guestAddress);
|
||||
}
|
||||
|
||||
if (hleSymbol.HasValue)
|
||||
{
|
||||
RegisterDlsymRuntimeSymbolKey(hleSymbol.Value.Nid, guestAddress);
|
||||
RegisterDlsymRuntimeSymbolKey(hleSymbol.Value.ExportName, guestAddress);
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterDlsymRuntimeSymbolKey(string key, ulong guestAddress)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key) || !IsRuntimeSymbolAddressUsable(guestAddress))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_runtimeSymbolsByName[key] = guestAddress;
|
||||
_lazyDlsymStubCache[key] = guestAddress;
|
||||
}
|
||||
|
||||
private void LogLazyImportStubMaterializationFailure(string reason, string dispatchNid, string symbolName)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Lazy import stub materialization failed ({reason}): " +
|
||||
$"nid={dispatchNid} symbol='{symbolName}'");
|
||||
}
|
||||
|
||||
private unsafe bool TryResolveImportStubRegionBounds(
|
||||
ImportStubEntry[] importEntries,
|
||||
out ulong regionBase,
|
||||
out ulong regionLimit)
|
||||
{
|
||||
regionBase = 0;
|
||||
regionLimit = 0;
|
||||
|
||||
for (var candidateIndex = 0; candidateIndex < 64; candidateIndex++)
|
||||
{
|
||||
var candidateBase = ImportStubRegionCanonicalBase -
|
||||
(ulong)candidateIndex * ImportStubRegionAddressStride;
|
||||
if (!_hostMemory.Query(candidateBase, out var memoryInfo) ||
|
||||
memoryInfo.RegionSize == 0 ||
|
||||
memoryInfo.State != HostRegionState.Committed)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var candidateLimit = candidateBase + memoryInfo.RegionSize;
|
||||
var hasStub = false;
|
||||
for (var i = 0; i < importEntries.Length; i++)
|
||||
{
|
||||
var entryAddress = importEntries[i].Address;
|
||||
if (entryAddress < candidateBase || entryAddress >= candidateLimit)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((entryAddress - candidateBase) % LazyImportStubSlotSize != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
hasStub = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!hasStub)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
regionBase = candidateBase;
|
||||
regionLimit = candidateLimit;
|
||||
return true;
|
||||
}
|
||||
|
||||
ulong maxStubEnd = 0;
|
||||
for (var i = 0; i < importEntries.Length; i++)
|
||||
{
|
||||
var entryAddress = importEntries[i].Address;
|
||||
if (entryAddress < ImportStubRegionCanonicalBase)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((entryAddress - ImportStubRegionCanonicalBase) % LazyImportStubSlotSize != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var entryEnd = entryAddress + LazyImportStubSlotSize;
|
||||
if (entryEnd > maxStubEnd)
|
||||
{
|
||||
maxStubEnd = entryEnd;
|
||||
regionBase = ImportStubRegionCanonicalBase;
|
||||
}
|
||||
}
|
||||
|
||||
if (regionBase == 0 || maxStubEnd <= regionBase)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var spanBytes = maxStubEnd - regionBase;
|
||||
regionLimit = regionBase + AlignUp(spanBytes, ImportStubRegionPageSize);
|
||||
return regionLimit > regionBase;
|
||||
}
|
||||
|
||||
private bool EnsureLazyImportStubPoolMapped()
|
||||
{
|
||||
if (_lazyImportStubPoolMapped && _lazyImportStubPoolBase != 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var importEntries = _importEntries;
|
||||
if (!TryResolveImportStubRegionBounds(importEntries, out var importStubRegionBase, out var importStubRegionLimit))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ulong nextSlot = importStubRegionBase;
|
||||
for (var i = 0; i < importEntries.Length; i++)
|
||||
{
|
||||
var entryAddress = importEntries[i].Address;
|
||||
if (entryAddress < importStubRegionBase || entryAddress >= importStubRegionLimit)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var entryEnd = entryAddress + LazyImportStubSlotSize;
|
||||
if (entryEnd > nextSlot)
|
||||
{
|
||||
nextSlot = entryEnd;
|
||||
}
|
||||
}
|
||||
|
||||
if (nextSlot >= importStubRegionLimit)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_lazyImportStubPoolBase = importStubRegionBase;
|
||||
_lazyImportStubNextSlot = nextSlot;
|
||||
_lazyImportStubPoolLimit = importStubRegionLimit;
|
||||
_lazyImportStubPoolMapped = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryAllocateLazyImportStubSlot(out ulong guestAddress)
|
||||
{
|
||||
guestAddress = 0;
|
||||
if (!_lazyImportStubPoolMapped ||
|
||||
_lazyImportStubNextSlot < _lazyImportStubPoolBase ||
|
||||
_lazyImportStubNextSlot + LazyImportStubSlotSize > _lazyImportStubPoolLimit)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
guestAddress = _lazyImportStubNextSlot;
|
||||
_lazyImportStubNextSlot += LazyImportStubSlotSize;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryResolveRuntimeSymbolAlias(string symbolName, out ulong address)
|
||||
{
|
||||
address = 0;
|
||||
@@ -1363,29 +1915,20 @@ public sealed partial class DirectExecutionBackend
|
||||
return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
ulong bridgeHandle = cpuContext[CpuRegister.Rdi];
|
||||
ulong symbolNameAddress = cpuContext[CpuRegister.Rsi];
|
||||
ulong outputAddress = cpuContext[CpuRegister.Rdx];
|
||||
_ = TryReadAsciiZ(symbolNameAddress, 512, out var symbolName);
|
||||
|
||||
OrbisGen2Result result = DispatchKernelDynlibDlsym();
|
||||
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
try
|
||||
{
|
||||
return result;
|
||||
OrbisGen2Result result = DispatchKernelDynlibDlsym();
|
||||
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
if (_logBootstrap)
|
||||
catch
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] bootstrap_dispatch: handle=0x{bridgeHandle:X16} symbol='{symbolName}' out=0x{outputAddress:X16} rax=0x{cpuContext[CpuRegister.Rax]:X16}");
|
||||
return CompleteKernelDynlibDlsymFailure(cpuContext, outputAddress);
|
||||
}
|
||||
|
||||
if (cpuContext[CpuRegister.Rax] == 0uL)
|
||||
{
|
||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] bootstrap_bridge unresolved: handle=0x{bridgeHandle:X} symbol='{symbolName}' out=0x{outputAddress:X16}");
|
||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -1426,6 +1969,7 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
List<byte> list = new List<byte>(Math.Min(maxLength, 256));
|
||||
Span<byte> destination = stackalloc byte[1];
|
||||
for (int i = 0; i < maxLength; i++)
|
||||
@@ -1524,7 +2068,7 @@ public sealed partial class DirectExecutionBackend
|
||||
uint flNewProtect = default(uint);
|
||||
try
|
||||
{
|
||||
if (Marshal.ReadByte(num2) != 232 || !VirtualProtect((void*)num, 5u, 64u, &flNewProtect))
|
||||
if (Marshal.ReadByte(num2) != 232 || !_hostMemory.Protect((ulong)(void*)num, 5u, HostPageProtection.ReadWriteExecute, out flNewProtect))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -1532,7 +2076,7 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
Marshal.WriteByte(num2 + i, 144);
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), (void*)num, 5u);
|
||||
_hostMemory.FlushInstructionCache((ulong)(void*)num, 5u);
|
||||
_patchedEa020eLookupCall = true;
|
||||
Console.Error.WriteLine($"[LOADER][WARNING] Import#{dispatchIndex}: patched hash-lookup call at 0x{num:X16} -> NOP*5");
|
||||
}
|
||||
@@ -1543,7 +2087,7 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
if (flNewProtect != 0)
|
||||
{
|
||||
VirtualProtect((void*)num, 5u, flNewProtect, &flNewProtect);
|
||||
_hostMemory.ProtectRaw((ulong)(void*)num, 5u, flNewProtect, out flNewProtect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,562 @@
|
||||
// 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;
|
||||
using SharpEmu.HLE.Host;
|
||||
|
||||
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;
|
||||
|
||||
// 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)
|
||||
{
|
||||
_hostThreading.SetTlsValue(_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 (!EnsureHostRuntimeExports(backend._hostSymbols))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var executor = new NativeGuestExecutor(backend);
|
||||
if (!executor.Initialize())
|
||||
{
|
||||
executor.Dispose();
|
||||
return null;
|
||||
}
|
||||
return executor;
|
||||
}
|
||||
|
||||
private static bool EnsureHostRuntimeExports(IHostSymbolResolver symbols)
|
||||
{
|
||||
if (_exitThreadAddress != 0)
|
||||
{
|
||||
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0;
|
||||
}
|
||||
_waitForSingleObjectAddress = symbols.GetAddress(HostRuntimeFunction.WaitForSingleObject);
|
||||
_setEventAddress = symbols.GetAddress(HostRuntimeFunction.SetEvent);
|
||||
_exitThreadAddress = symbols.GetAddress(HostRuntimeFunction.ExitThread);
|
||||
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0 && _exitThreadAddress != 0;
|
||||
}
|
||||
|
||||
private bool Initialize()
|
||||
{
|
||||
_selfHandle = GCHandle.Alloc(this);
|
||||
_controlBlock = (void*)_backend._hostMemory.Allocate(0, 4096u, HostPageProtection.ReadWrite);
|
||||
if (_controlBlock == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
_loopStub = (void*)_backend._hostMemory.Allocate(0, LoopStubSize, HostPageProtection.ReadWriteExecute);
|
||||
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 (!_backend._hostMemory.Protect((ulong)_loopStub, LoopStubSize, HostPageProtection.ReadExecute, out oldProtect))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
_backend._hostMemory.FlushInstructionCache((ulong)_loopStub, LoopStubSize);
|
||||
_threadHandle = _backend._hostThreading.CreateNativeThread(
|
||||
(nint)_loopStub,
|
||||
0,
|
||||
WorkerStackReservation,
|
||||
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 = backend._hostThreading.GetTlsValue(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!);
|
||||
backend._hostThreading.SetTlsValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
|
||||
if (_runState is { } state)
|
||||
{
|
||||
_prevHostThreadId = Volatile.Read(ref state.HostThreadId);
|
||||
Volatile.Write(ref state.HostThreadId, unchecked((int)backend._hostThreading.CurrentThreadId));
|
||||
}
|
||||
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);
|
||||
}
|
||||
_backend._hostThreading.SetTlsValue(_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 = _backend._hostThreading.WaitForThreadExit(_threadHandle, 1000u);
|
||||
_backend._hostThreading.CloseThreadHandle(_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)
|
||||
{
|
||||
_backend._hostMemory.Free((ulong)_loopStub);
|
||||
_loopStub = null;
|
||||
}
|
||||
if (_controlBlock != null)
|
||||
{
|
||||
_backend._hostMemory.Free((ulong)_controlBlock);
|
||||
_controlBlock = null;
|
||||
}
|
||||
if (_selfHandle.IsAllocated)
|
||||
{
|
||||
_selfHandle.Free();
|
||||
}
|
||||
_workAvailable.Dispose();
|
||||
_workCompleted.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.HLE.Host;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
@@ -11,17 +12,15 @@ public sealed unsafe class StubManager : IDisposable
|
||||
private readonly List<nint> _allocatedStubs = new();
|
||||
private readonly Dictionary<string, nint> _importHandlers = new();
|
||||
private readonly Dictionary<ulong, nint> _stubAddresses = new();
|
||||
private readonly IHostMemory _hostMemory;
|
||||
private byte* _pltMemory;
|
||||
private int _pltOffset;
|
||||
private const int PltMemorySize = 1024 * 1024; // 1MB for stubs
|
||||
|
||||
public StubManager()
|
||||
public StubManager(IHostMemory? hostMemory = null)
|
||||
{
|
||||
_pltMemory = (byte*)VirtualAlloc(
|
||||
null,
|
||||
(nuint)PltMemorySize,
|
||||
AllocationType.Reserve | AllocationType.Commit,
|
||||
MemoryProtection.ExecuteReadWrite);
|
||||
_hostMemory = hostMemory ?? HostPlatform.Current.Memory;
|
||||
_pltMemory = (byte*)_hostMemory.Allocate(0, PltMemorySize, HostPageProtection.ReadWriteExecute);
|
||||
|
||||
if (_pltMemory == null)
|
||||
{
|
||||
@@ -185,7 +184,7 @@ public sealed unsafe class StubManager : IDisposable
|
||||
{
|
||||
if (_pltMemory != null)
|
||||
{
|
||||
VirtualFree(_pltMemory, 0, FreeType.Release);
|
||||
_hostMemory.Free((ulong)_pltMemory);
|
||||
_pltMemory = null;
|
||||
}
|
||||
|
||||
@@ -194,29 +193,5 @@ public sealed unsafe class StubManager : IDisposable
|
||||
_stubAddresses.Clear();
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern void* VirtualAlloc(void* lpAddress, nuint dwSize, AllocationType flAllocationType, MemoryProtection flProtect);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool VirtualFree(void* lpAddress, nuint dwSize, FreeType dwFreeType);
|
||||
|
||||
[Flags]
|
||||
private enum AllocationType : uint
|
||||
{
|
||||
Commit = 0x1000,
|
||||
Reserve = 0x2000,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
private enum MemoryProtection : uint
|
||||
{
|
||||
ExecuteReadWrite = 0x40,
|
||||
}
|
||||
|
||||
private enum FreeType : uint
|
||||
{
|
||||
Release = 0x8000,
|
||||
}
|
||||
|
||||
public delegate void ImportHandler(CpuContext context);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Byte offsets into the Win64 CONTEXT record delivered to vectored exception
|
||||
/// handlers. The handlers read/write guest registers directly at these offsets
|
||||
/// (no managed CONTEXT struct exists); a future POSIX backend gets a sibling
|
||||
/// class for its mcontext layout.
|
||||
/// </summary>
|
||||
internal static class Win64ContextOffsets
|
||||
{
|
||||
public const int Mxcsr = 52;
|
||||
public const int Rax = 120;
|
||||
public const int Rcx = 128;
|
||||
public const int Rdx = 136;
|
||||
public const int Rbx = 144;
|
||||
public const int Rsp = 152;
|
||||
public const int Rbp = 160;
|
||||
public const int Rsi = 168;
|
||||
public const int Rdi = 176;
|
||||
public const int R8 = 184;
|
||||
public const int R9 = 192;
|
||||
public const int R10 = 200;
|
||||
public const int R11 = 208;
|
||||
public const int R12 = 216;
|
||||
public const int R13 = 224;
|
||||
public const int R14 = 232;
|
||||
public const int R15 = 240;
|
||||
public const int Rip = 248;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Windows NTSTATUS exception codes and EXCEPTION_RECORD access-type values the
|
||||
/// fault handlers filter on. Values are the same numbers the handlers previously
|
||||
/// compared as bare literals; only the spelling changed.
|
||||
/// </summary>
|
||||
internal static class WindowsFaultCodes
|
||||
{
|
||||
public const uint AccessViolation = 0xC0000005u; // 3221225477
|
||||
public const uint Breakpoint = 0x80000003u; // 2147483651
|
||||
public const uint IllegalInstruction = 0xC000001Du; // 3221225501
|
||||
public const uint FastFail = 0xC0000409u; // 3221226505
|
||||
public const uint StackOverflow = 0xC00000FDu;
|
||||
public const uint ClrManagedException = 0xE0434352u;
|
||||
|
||||
// EXCEPTION_RECORD.ExceptionInformation[0] for access violations.
|
||||
public const ulong AccessRead = 0;
|
||||
public const ulong AccessWrite = 1;
|
||||
public const ulong AccessExecute = 8;
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.HLE.Host;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Vectored-exception-handler installation and the handler pre-filter thunk.
|
||||
/// The thunk is inherently Windows-shaped (TEB stack-limit reads via gs:,
|
||||
/// NTSTATUS pre-filtering, Win64 calling convention) and moved here whole from
|
||||
/// DirectExecutionBackend; a POSIX backend supplies a sibling built around
|
||||
/// sigaction/sigaltstack instead.
|
||||
/// </summary>
|
||||
internal sealed unsafe partial class WindowsFaultHandling : IHostFaultHandling
|
||||
{
|
||||
private readonly IHostMemory _memory;
|
||||
|
||||
public WindowsFaultHandling(IHostMemory memory)
|
||||
{
|
||||
_memory = memory;
|
||||
}
|
||||
|
||||
public nint CreateHandlerThunk(nint managedCallback, uint hostRspSwitchTlsSlot, nint tlsGetValueAddress)
|
||||
{
|
||||
const uint stubSize = 256u;
|
||||
void* ptr = (void*)_memory.Allocate(0, stubSize, HostPageProtection.ReadWriteExecute);
|
||||
if (ptr == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
byte* code = (byte*)ptr;
|
||||
int offset = 0;
|
||||
// Native pre-filter: these exception codes are raised while the thread can be in
|
||||
// cooperative GC mode (a C# throw is RaiseException(0xE0434352) on the throwing
|
||||
// thread; FailFast/stack-overflow arrive mid-runtime-failure). Entering the managed
|
||||
// handler then trips the CLR's reverse-P/Invoke check and kills the process with
|
||||
// "Invalid Program: attempted to call a UnmanagedCallersOnly method from managed
|
||||
// code" — this is why no managed throw (even one with a catch handler) ever
|
||||
// survived inside the emulator. Continue the handler search without touching
|
||||
// managed code; the CLR's own VEH handles its exceptions. MSVC C++ exceptions
|
||||
// (Vulkan drivers, host CRT) are excluded too: the managed handler only ever
|
||||
// returned CONTINUE_SEARCH for them.
|
||||
ReadOnlySpan<uint> nonManagedExceptionCodes =
|
||||
[WindowsFaultCodes.ClrManagedException, 0xE06D7363u, WindowsFaultCodes.FastFail, WindowsFaultCodes.StackOverflow];
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x01); // mov rax, [rcx] (ExceptionRecord*)
|
||||
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x00); // mov eax, [rax] (ExceptionCode)
|
||||
var passJumpOffsets = stackalloc int[nonManagedExceptionCodes.Length];
|
||||
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
|
||||
{
|
||||
EmitByte(code, ref offset, 0x3D); // cmp eax, imm32
|
||||
EmitUInt32(code, ref offset, nonManagedExceptionCodes[i]);
|
||||
EmitByte(code, ref offset, 0x74); // je pass
|
||||
passJumpOffsets[i] = offset;
|
||||
EmitByte(code, ref offset, 0x00);
|
||||
}
|
||||
EmitByte(code, ref offset, 0xEB); EmitByte(code, ref offset, 0x03); // jmp over pass block
|
||||
int passOffset = offset;
|
||||
EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0); // pass: xor eax, eax (EXCEPTION_CONTINUE_SEARCH)
|
||||
EmitByte(code, ref offset, 0xC3); // ret
|
||||
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
|
||||
{
|
||||
code[passJumpOffsets[i]] = checked((byte)(passOffset - (passJumpOffsets[i] + 1)));
|
||||
}
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x54); // push r12
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x55); // push r13
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE4); // mov r12, rsp
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xCD); // mov r13, rcx
|
||||
EmitByte(code, ref offset, 0x65); EmitByte(code, ref offset, 0x48); // mov rax, gs:[8]
|
||||
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x04); EmitByte(code, ref offset, 0x25);
|
||||
EmitUInt32(code, ref offset, 8u);
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x39); EmitByte(code, ref offset, 0xC4); // cmp r12, rax
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x83); // jae guestStack
|
||||
int aboveStackJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
EmitByte(code, ref offset, 0x65); EmitByte(code, ref offset, 0x48); // mov rax, gs:[0x10]
|
||||
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x04); EmitByte(code, ref offset, 0x25);
|
||||
EmitUInt32(code, ref offset, 0x10u);
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x39); EmitByte(code, ref offset, 0xC4); // cmp r12, rax
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x82); // jb guestStack
|
||||
int belowStackJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE9); // mov rcx, r13
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = managedCallback;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28);
|
||||
EmitByte(code, ref offset, 0xE9);
|
||||
int hostRestoreJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
|
||||
int guestStackOffset = offset;
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28);
|
||||
EmitByte(code, ref offset, 0xB9);
|
||||
EmitUInt32(code, ref offset, hostRspSwitchTlsSlot);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = tlsGetValueAddress;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x85); EmitByte(code, ref offset, 0xC0); // test rax, rax
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x84);
|
||||
int missingTlsJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x18); // mov r11, [rax]
|
||||
EmitByte(code, ref offset, 0x4D); EmitByte(code, ref offset, 0x85); EmitByte(code, ref offset, 0xDB); // test r11, r11
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x84);
|
||||
int missingHostStackJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xDC); // mov rsp, r11
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE9); // mov rcx, r13
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = managedCallback;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28);
|
||||
EmitByte(code, ref offset, 0xE9);
|
||||
int guestRestoreJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
|
||||
int passThroughOffset = offset;
|
||||
EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0); // xor eax, eax
|
||||
int restoreOffset = offset;
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE4); // mov rsp, r12
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5D);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5C);
|
||||
EmitByte(code, ref offset, 0xC3);
|
||||
|
||||
*(int*)(code + aboveStackJump) = guestStackOffset - (aboveStackJump + sizeof(int));
|
||||
*(int*)(code + belowStackJump) = guestStackOffset - (belowStackJump + sizeof(int));
|
||||
*(int*)(code + hostRestoreJump) = restoreOffset - (hostRestoreJump + sizeof(int));
|
||||
*(int*)(code + missingTlsJump) = passThroughOffset - (missingTlsJump + sizeof(int));
|
||||
*(int*)(code + missingHostStackJump) = passThroughOffset - (missingHostStackJump + sizeof(int));
|
||||
*(int*)(code + guestRestoreJump) = restoreOffset - (guestRestoreJump + sizeof(int));
|
||||
|
||||
if (!_memory.Protect((ulong)ptr, stubSize, HostPageProtection.ReadExecute, out _))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for exception handler trampoline at 0x{(nint)ptr:X16}");
|
||||
_ = _memory.Free((ulong)ptr);
|
||||
return 0;
|
||||
}
|
||||
_memory.FlushInstructionCache((ulong)ptr, (ulong)offset);
|
||||
return (nint)ptr;
|
||||
}
|
||||
|
||||
public void FreeThunk(nint thunk)
|
||||
{
|
||||
_ = _memory.Free((ulong)thunk);
|
||||
}
|
||||
|
||||
public nint AddFirstChanceHandler(nint thunk)
|
||||
{
|
||||
return (nint)AddVectoredExceptionHandler(1u, thunk);
|
||||
}
|
||||
|
||||
public void RemoveHandler(nint handle)
|
||||
{
|
||||
_ = RemoveVectoredExceptionHandler((void*)handle);
|
||||
}
|
||||
|
||||
public void SetUnhandledFilter(nint thunk)
|
||||
{
|
||||
_ = SetUnhandledExceptionFilter(thunk);
|
||||
}
|
||||
|
||||
private static void EmitByte(byte* code, ref int offset, byte value)
|
||||
{
|
||||
code[offset++] = value;
|
||||
}
|
||||
|
||||
private static void EmitUInt32(byte* code, ref int offset, uint value)
|
||||
{
|
||||
*(uint*)(code + offset) = value;
|
||||
offset += sizeof(uint);
|
||||
}
|
||||
|
||||
[LibraryImport("kernel32.dll")]
|
||||
private static partial void* AddVectoredExceptionHandler(uint first, IntPtr handler);
|
||||
|
||||
[LibraryImport("kernel32.dll")]
|
||||
private static partial uint RemoveVectoredExceptionHandler(void* handle);
|
||||
|
||||
[LibraryImport("kernel32.dll")]
|
||||
private static partial IntPtr SetUnhandledExceptionFilter(IntPtr lpTopLevelExceptionFilter);
|
||||
}
|
||||
@@ -5,7 +5,7 @@ using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Core.Cpu;
|
||||
|
||||
public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemoryAllocator
|
||||
public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemoryAllocator, ICpuMemoryWrapper
|
||||
{
|
||||
private readonly ICpuMemory _inner;
|
||||
|
||||
|
||||
@@ -45,7 +45,13 @@ public static class Ps5ParamJsonReader
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(data);
|
||||
ReadOnlyMemory<byte> json = data;
|
||||
if (json.Span.StartsWith("\uFEFF"u8))
|
||||
{
|
||||
json = json[3..];
|
||||
}
|
||||
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
return TryReadPs5Param(doc.RootElement);
|
||||
}
|
||||
catch (JsonException)
|
||||
@@ -56,12 +62,15 @@ public static class Ps5ParamJsonReader
|
||||
|
||||
private static (string? Title, string? TitleId, string? Version) TryReadPs5Param(JsonElement root)
|
||||
{
|
||||
string? titleId = root.TryGetProperty("titleId", out var eTid) ? eTid.GetString() : null;
|
||||
if (root.ValueKind != JsonValueKind.Object)
|
||||
return (null, null, null);
|
||||
|
||||
var titleId = GetString(root, "titleId");
|
||||
|
||||
string? ver =
|
||||
(root.TryGetProperty("contentVersion", out var cv) ? cv.GetString() : null)
|
||||
?? (root.TryGetProperty("masterVersion", out var mv) ? mv.GetString() : null)
|
||||
?? (root.TryGetProperty("targetContentVersion", out var tv) ? tv.GetString() : null);
|
||||
GetString(root, "contentVersion")
|
||||
?? GetString(root, "masterVersion")
|
||||
?? GetString(root, "targetContentVersion");
|
||||
|
||||
string? title = ExtractTitleName(root);
|
||||
|
||||
@@ -70,34 +79,49 @@ public static class Ps5ParamJsonReader
|
||||
|
||||
private static string? ExtractTitleName(JsonElement root)
|
||||
{
|
||||
if (!root.TryGetProperty("localizedParameters", out var lp))
|
||||
if ((!root.TryGetProperty("localizedParameters", out var lp) || lp.ValueKind != JsonValueKind.Object) &&
|
||||
root.TryGetProperty("disc", out var disc) && disc.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
if (root.TryGetProperty("disc", out var disc) && disc.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
disc.TryGetProperty("localizedParameters", out lp);
|
||||
}
|
||||
disc.TryGetProperty("localizedParameters", out lp);
|
||||
}
|
||||
|
||||
if (lp.ValueKind != JsonValueKind.Object)
|
||||
return null;
|
||||
|
||||
string? defLang = lp.TryGetProperty("defaultLanguage", out var dl) ? dl.GetString() : null;
|
||||
var defLang = GetString(lp, "defaultLanguage");
|
||||
|
||||
if (!string.IsNullOrEmpty(defLang))
|
||||
{
|
||||
if (lp.TryGetProperty(defLang, out var langObj) && langObj.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
if (langObj.TryGetProperty("titleName", out var tn))
|
||||
return tn.GetString();
|
||||
var title = GetString(langObj, "titleName");
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
return title;
|
||||
}
|
||||
}
|
||||
|
||||
if (lp.TryGetProperty("en-US", out var en) && en.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
if (en.TryGetProperty("titleName", out var tn2))
|
||||
return tn2.GetString();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string? GetString(JsonElement parent, string propertyName) =>
|
||||
parent.TryGetProperty(propertyName, out var value) && value.ValueKind == JsonValueKind.String
|
||||
? value.GetString()
|
||||
: null;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ namespace SharpEmu.Core.Loader;
|
||||
public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Loader");
|
||||
private const uint SelfMagic = 0x4F153D1D;
|
||||
private const uint ElfMagic = 0x7F454C46;
|
||||
private const uint Ps4SelfMagic = 0x4F153D1D;
|
||||
private const uint Ps5SelfMagic = 0x5414F5EE;
|
||||
private const ulong SelfSegmentFlag = 0x800;
|
||||
private const int PageSize = 0x1000;
|
||||
private const ulong ImportStubBaseAddress = 0x0000_7000_0000_0000UL;
|
||||
@@ -323,7 +325,8 @@ public sealed class SelfLoader : ISelfLoader
|
||||
throw new InvalidDataException("Input image is too small to contain an ELF header.");
|
||||
}
|
||||
|
||||
if (imageData.Length >= sizeof(uint) && BinaryPrimitives.ReadUInt32BigEndian(imageData[..sizeof(uint)]) == SelfMagic)
|
||||
var magic = BinaryPrimitives.ReadUInt32BigEndian(imageData[..sizeof(uint)]);
|
||||
if (magic is Ps4SelfMagic or Ps5SelfMagic)
|
||||
{
|
||||
var selfHeader = ReadUnmanaged<SelfHeader>(imageData, 0);
|
||||
if (!selfHeader.HasKnownLayout || selfHeader.Unknown != 0x22)
|
||||
@@ -345,6 +348,12 @@ public sealed class SelfLoader : ISelfLoader
|
||||
return new LoadContext(IsSelf: true, elfOffset, selfHeader.FileSize, segments);
|
||||
}
|
||||
|
||||
if (magic != ElfMagic)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Unsupported executable signature 0x{magic:X8}");
|
||||
}
|
||||
|
||||
return new LoadContext(IsSelf: false, ElfOffset: 0, SelfFileSize: 0, Array.Empty<SelfSegment>());
|
||||
}
|
||||
|
||||
@@ -2380,10 +2389,14 @@ public sealed class SelfLoader : ISelfLoader
|
||||
public ulong FileSize => _fileSize;
|
||||
|
||||
public bool HasKnownLayout =>
|
||||
_ident0 == 0x4F &&
|
||||
_ident1 == 0x15 &&
|
||||
_ident2 == 0x3D &&
|
||||
_ident3 == 0x1D &&
|
||||
((_ident0 == 0x4F &&
|
||||
_ident1 == 0x15 &&
|
||||
_ident2 == 0x3D &&
|
||||
_ident3 == 0x1D) ||
|
||||
(_ident0 == 0x54 &&
|
||||
_ident1 == 0x14 &&
|
||||
_ident2 == 0xF5 &&
|
||||
_ident3 == 0xEE)) &&
|
||||
_ident4 == 0x00 &&
|
||||
_ident5 == 0x01 &&
|
||||
_ident6 == 0x01 &&
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.Core.Loader;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.HLE.Host;
|
||||
using SharpEmu.Logging;
|
||||
|
||||
namespace SharpEmu.Core.Memory;
|
||||
|
||||
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryAllocator, IDisposable
|
||||
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryAllocator, IGuestAddressSpace, IDisposable
|
||||
{
|
||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("VMEM");
|
||||
|
||||
@@ -28,41 +29,26 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
private const ulong DefaultLazyReservePrimeBytes = 0x0400_0000UL; // 64 MiB
|
||||
private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB
|
||||
|
||||
private const uint MEM_COMMIT = 0x1000;
|
||||
private const uint MEM_RESERVE = 0x2000;
|
||||
private const uint MEM_RELEASE = 0x8000;
|
||||
// Raw Windows PAGE_* values retained for the internal region/protection
|
||||
// bookkeeping: regions and saved old-protection values always carry the raw
|
||||
// value of the host platform in use, and these classification helpers only
|
||||
// ever see values this class itself assigned (see IHostMemory.ProtectRaw).
|
||||
private const uint PAGE_EXECUTE_READ = 0x20;
|
||||
private const uint PAGE_EXECUTE_READWRITE = 0x40;
|
||||
private const uint PAGE_EXECUTE = 0x10;
|
||||
private const uint PAGE_EXECUTE_WRITECOPY = 0x80;
|
||||
private const uint PAGE_NOACCESS = 0x01;
|
||||
private const uint PAGE_READWRITE = 0x04;
|
||||
private const uint PAGE_READONLY = 0x02;
|
||||
|
||||
private readonly IHostMemory _hostMemory;
|
||||
private ulong _guestAllocationArenaBase;
|
||||
private ulong _guestAllocationOffset;
|
||||
private static readonly ulong LazyReservePrimeBytes = ResolveLazyReservePrimeBytes();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern void* VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool VirtualFree(void* lpAddress, nuint dwSize, uint dwFreeType);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool VirtualProtect(void* lpAddress, nuint dwSize, uint flNewProtect, out uint lpflOldProtect);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern nuint VirtualQuery(void* lpAddress, out MemoryBasicInformation64 lpBuffer, nuint dwLength);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern void* GetCurrentProcess();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
|
||||
public PhysicalVirtualMemory(IHostMemory? hostMemory = null)
|
||||
{
|
||||
_hostMemory = hostMemory ?? HostPlatform.Current.Memory;
|
||||
}
|
||||
|
||||
public bool TryAllocateAtExact(ulong desiredAddress, ulong size, bool executable, out ulong actualAddress)
|
||||
{
|
||||
@@ -74,17 +60,17 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
var alignedSize = (size + 0xFFF) & ~0xFFFUL;
|
||||
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
||||
var allocationType = MEM_COMMIT | MEM_RESERVE;
|
||||
var result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, allocationType, protection);
|
||||
if (result == null)
|
||||
var hostProtection = executable ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite;
|
||||
var result = _hostMemory.Allocate(desiredAddress, alignedSize, hostProtection);
|
||||
if (result == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
actualAddress = (ulong)result;
|
||||
actualAddress = result;
|
||||
if (actualAddress != desiredAddress)
|
||||
{
|
||||
VirtualFree(result, 0, MEM_RELEASE);
|
||||
_hostMemory.Free(result);
|
||||
actualAddress = 0;
|
||||
return false;
|
||||
}
|
||||
@@ -119,33 +105,33 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
var alignedSize = (size + 0xFFF) & ~0xFFFUL;
|
||||
|
||||
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
||||
var allocationType = MEM_COMMIT | MEM_RESERVE;
|
||||
var hostProtection = executable ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite;
|
||||
var reservedOnly = false;
|
||||
var preferReserveOnly = !executable &&
|
||||
alignedSize >= LargeDataReserveThreshold &&
|
||||
alignedSize > FullCommitRegionLimit;
|
||||
|
||||
void* result = null;
|
||||
ulong result = 0;
|
||||
if (preferReserveOnly)
|
||||
{
|
||||
result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, MEM_RESERVE, PAGE_READWRITE);
|
||||
if (result == null && allowAlternative)
|
||||
result = _hostMemory.Reserve(desiredAddress, alignedSize, HostPageProtection.ReadWrite);
|
||||
if (result == 0 && allowAlternative)
|
||||
{
|
||||
result = VirtualAlloc(null, (nuint)alignedSize, MEM_RESERVE, PAGE_READWRITE);
|
||||
result = _hostMemory.Reserve(0, alignedSize, HostPageProtection.ReadWrite);
|
||||
}
|
||||
|
||||
if (result != null)
|
||||
if (result != 0)
|
||||
{
|
||||
reservedOnly = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (result == null)
|
||||
if (result == 0)
|
||||
{
|
||||
result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, allocationType, protection);
|
||||
result = _hostMemory.Allocate(desiredAddress, alignedSize, hostProtection);
|
||||
}
|
||||
|
||||
if (result == null)
|
||||
if (result == 0)
|
||||
{
|
||||
if (!allowAlternative)
|
||||
{
|
||||
@@ -153,32 +139,32 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
}
|
||||
|
||||
TraceVmem($"Could not allocate at 0x{desiredAddress:X16}, trying any address...");
|
||||
result = VirtualAlloc(null, (nuint)alignedSize, allocationType, protection);
|
||||
result = _hostMemory.Allocate(0, alignedSize, hostProtection);
|
||||
|
||||
if (result == null)
|
||||
if (result == 0)
|
||||
{
|
||||
if (!executable)
|
||||
{
|
||||
result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, MEM_RESERVE, PAGE_READWRITE);
|
||||
if (result == null && allowAlternative)
|
||||
result = _hostMemory.Reserve(desiredAddress, alignedSize, HostPageProtection.ReadWrite);
|
||||
if (result == 0 && allowAlternative)
|
||||
{
|
||||
result = VirtualAlloc(null, (nuint)alignedSize, MEM_RESERVE, PAGE_READWRITE);
|
||||
result = _hostMemory.Reserve(0, alignedSize, HostPageProtection.ReadWrite);
|
||||
}
|
||||
|
||||
if (result != null)
|
||||
if (result != 0)
|
||||
{
|
||||
reservedOnly = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (result == null)
|
||||
if (result == 0)
|
||||
{
|
||||
throw new OutOfMemoryException($"Failed to allocate {alignedSize} bytes of virtual memory");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var actualAddress = (ulong)result;
|
||||
var actualAddress = result;
|
||||
|
||||
var lazyPrimeState = "n/a";
|
||||
if (reservedOnly)
|
||||
@@ -191,9 +177,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
{
|
||||
var remaining = primeBytes - committedBytes;
|
||||
var chunkBytes = Math.Min(remaining, LazyReservePrimeChunkBytes);
|
||||
var commitAddress = (void*)(actualAddress + committedBytes);
|
||||
var committed = VirtualAlloc(commitAddress, (nuint)chunkBytes, MEM_COMMIT, PAGE_READWRITE);
|
||||
if (committed == null)
|
||||
var commitAddress = actualAddress + committedBytes;
|
||||
if (!_hostMemory.Commit(commitAddress, chunkBytes, HostPageProtection.ReadWrite))
|
||||
{
|
||||
break;
|
||||
}
|
||||
@@ -336,6 +321,41 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryProtect(ulong address, ulong size, GuestPageProtection protection)
|
||||
{
|
||||
if (size == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return _hostMemory.Protect(address, size, ResolveProtection(protection), out _);
|
||||
}
|
||||
|
||||
// Reproduces the decomposition KernelMemoryCompatExports.ResolveHostProtection
|
||||
// performed before this seam existed; the Windows backend maps each case back
|
||||
// to the identical PAGE_* value.
|
||||
private static HostPageProtection ResolveProtection(GuestPageProtection protection)
|
||||
{
|
||||
var read = (protection & GuestPageProtection.Read) != 0;
|
||||
var write = (protection & GuestPageProtection.Write) != 0;
|
||||
var execute = (protection & GuestPageProtection.Execute) != 0;
|
||||
|
||||
if (execute)
|
||||
{
|
||||
return write
|
||||
? HostPageProtection.ReadWriteExecute
|
||||
: read
|
||||
? HostPageProtection.ReadExecute
|
||||
: HostPageProtection.Execute;
|
||||
}
|
||||
|
||||
return write
|
||||
? HostPageProtection.ReadWrite
|
||||
: read
|
||||
? HostPageProtection.ReadOnly
|
||||
: HostPageProtection.NoAccess;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
lock (_guestAllocationGate)
|
||||
@@ -345,7 +365,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
{
|
||||
VirtualFree((void*)region.VirtualAddress, 0, MEM_RELEASE);
|
||||
_hostMemory.Free(region.VirtualAddress);
|
||||
}
|
||||
_regions.Clear();
|
||||
_pageProtections.Clear();
|
||||
@@ -430,35 +450,35 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
private void SetProtection(ulong address, ulong size, ProgramHeaderFlags flags)
|
||||
{
|
||||
uint protection;
|
||||
HostPageProtection protection;
|
||||
|
||||
if (flags == ProgramHeaderFlags.None)
|
||||
{
|
||||
protection = PAGE_NOACCESS;
|
||||
protection = HostPageProtection.NoAccess;
|
||||
}
|
||||
else if ((flags & ProgramHeaderFlags.Execute) != 0)
|
||||
{
|
||||
protection = (flags & ProgramHeaderFlags.Write) != 0
|
||||
? PAGE_EXECUTE_READWRITE
|
||||
: PAGE_EXECUTE_READ;
|
||||
? HostPageProtection.ReadWriteExecute
|
||||
: HostPageProtection.ReadExecute;
|
||||
}
|
||||
else if ((flags & ProgramHeaderFlags.Write) != 0)
|
||||
{
|
||||
protection = PAGE_READWRITE;
|
||||
protection = HostPageProtection.ReadWrite;
|
||||
}
|
||||
else
|
||||
{
|
||||
protection = PAGE_READONLY;
|
||||
protection = HostPageProtection.ReadOnly;
|
||||
}
|
||||
|
||||
if (!VirtualProtect((void*)address, (nuint)size, protection, out _))
|
||||
if (!_hostMemory.Protect(address, size, protection, out _))
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to set memory protection at 0x{address:X16}");
|
||||
}
|
||||
|
||||
if ((flags & ProgramHeaderFlags.Execute) != 0)
|
||||
{
|
||||
FlushInstructionCache(GetCurrentProcess(), (void*)address, (nuint)size);
|
||||
_hostMemory.FlushInstructionCache(address, size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -550,6 +570,47 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryCompare(ulong virtualAddress, ReadOnlySpan<byte> expected)
|
||||
{
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
var region = FindRegion(virtualAddress, (ulong)expected.Length);
|
||||
if (region is null ||
|
||||
!TryResolveRegionOffset(
|
||||
virtualAddress,
|
||||
(ulong)expected.Length,
|
||||
region,
|
||||
out var offset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (expected.IsEmpty)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var srcPtr = (void*)(region.VirtualAddress + offset);
|
||||
if (region.IsReservedOnly &&
|
||||
!EnsureRangeCommitted((ulong)srcPtr, (ulong)expected.Length, region))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)expected.Length, region))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return new ReadOnlySpan<byte>(srcPtr, expected.Length).SequenceEqual(expected);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
|
||||
{
|
||||
var requiresExclusiveAccess = false;
|
||||
@@ -689,7 +750,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!VirtualProtect(destPtr, (nuint)source.Length, PAGE_EXECUTE_READWRITE, out var oldProtect))
|
||||
if (!_hostMemory.Protect((ulong)destPtr, (ulong)source.Length, HostPageProtection.ReadWriteExecute, out var oldProtect))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -703,10 +764,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
}
|
||||
finally
|
||||
{
|
||||
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _);
|
||||
_hostMemory.ProtectRaw((ulong)destPtr, (ulong)source.Length, oldProtect, out _);
|
||||
if (IsExecutableProtection(oldProtect))
|
||||
{
|
||||
FlushInstructionCache(GetCurrentProcess(), destPtr, (nuint)source.Length);
|
||||
_hostMemory.FlushInstructionCache((ulong)destPtr, (ulong)source.Length);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -932,12 +993,12 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
return protection is PAGE_READWRITE or PAGE_EXECUTE_READWRITE;
|
||||
}
|
||||
|
||||
private static uint GetCommitProtection(MemoryRegion region)
|
||||
private static HostPageProtection GetCommitProtection(MemoryRegion region)
|
||||
{
|
||||
return region.IsExecutable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
||||
return region.IsExecutable ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite;
|
||||
}
|
||||
|
||||
private static unsafe bool EnsureRangeCommitted(ulong address, ulong size, MemoryRegion region)
|
||||
private bool EnsureRangeCommitted(ulong address, ulong size, MemoryRegion region)
|
||||
{
|
||||
if (size == 0 || !region.IsReservedOnly)
|
||||
{
|
||||
@@ -951,7 +1012,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
var pageAddress = startPage;
|
||||
while (pageAddress < endPage)
|
||||
{
|
||||
if (VirtualQuery((void*)pageAddress, out var info, (nuint)sizeof(MemoryBasicInformation64)) == 0)
|
||||
if (!_hostMemory.Query(pageAddress, out var info))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -965,19 +1026,19 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
return false;
|
||||
}
|
||||
|
||||
if (info.State == MEM_COMMIT)
|
||||
if (info.State == HostRegionState.Committed)
|
||||
{
|
||||
pageAddress = rangeEnd;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (info.State != MEM_RESERVE)
|
||||
if (info.State != HostRegionState.Reserved)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var commitSize = rangeEnd - pageAddress;
|
||||
if (VirtualAlloc((void*)pageAddress, (nuint)commitSize, MEM_COMMIT, commitProtection) == null)
|
||||
if (!_hostMemory.Commit(pageAddress, commitSize, commitProtection))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -998,11 +1059,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
var startPage = AlignDown(address, PageSize);
|
||||
var endPage = AlignUp(address + size, PageSize);
|
||||
var temporaryProtection = region.IsExecutable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
||||
var temporaryProtection = region.IsExecutable ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite;
|
||||
|
||||
for (var pageAddress = startPage; pageAddress < endPage; pageAddress += PageSize)
|
||||
{
|
||||
if (!VirtualProtect((void*)pageAddress, (nuint)PageSize, temporaryProtection, out var oldProtection))
|
||||
if (!_hostMemory.Protect(pageAddress, PageSize, temporaryProtection, out var oldProtection))
|
||||
{
|
||||
RestorePageProtections(touchedPages);
|
||||
touchedPages.Clear();
|
||||
@@ -1015,11 +1076,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void RestorePageProtections(List<(ulong Address, uint Protection)> touchedPages)
|
||||
private void RestorePageProtections(List<(ulong Address, uint Protection)> touchedPages)
|
||||
{
|
||||
foreach (var (pageAddress, protection) in touchedPages)
|
||||
{
|
||||
VirtualProtect((void*)pageAddress, (nuint)PageSize, protection, out _);
|
||||
_hostMemory.ProtectRaw(pageAddress, PageSize, protection, out _);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1076,16 +1137,4 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
public uint Protection { get; set; }
|
||||
}
|
||||
|
||||
private struct MemoryBasicInformation64
|
||||
{
|
||||
public ulong BaseAddress;
|
||||
public ulong AllocationBase;
|
||||
public uint AllocationProtect;
|
||||
public uint Alignment1;
|
||||
public ulong RegionSize;
|
||||
public uint State;
|
||||
public uint Protect;
|
||||
public uint Type;
|
||||
public uint Alignment2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using SharpEmu.Core.Cpu.Disasm;
|
||||
using SharpEmu.Core.Loader;
|
||||
using SharpEmu.Core.Memory;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.HLE.Host;
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using SharpEmu.Libs.AppContent;
|
||||
@@ -83,18 +84,22 @@ 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();
|
||||
|
||||
var virtualMemory = new PhysicalVirtualMemory();
|
||||
// Resolve the host platform once at the composition root; on unsupported
|
||||
// OSes this throws PlatformNotSupportedException with a clear message
|
||||
// instead of failing on the first native call.
|
||||
var hostPlatform = HostPlatform.Current;
|
||||
|
||||
var virtualMemory = new PhysicalVirtualMemory(hostPlatform.Memory);
|
||||
|
||||
var fileSystem = new PhysicalFileSystem();
|
||||
|
||||
return new SharpEmuRuntime(
|
||||
new SelfLoader(),
|
||||
virtualMemory,
|
||||
new CpuDispatcher(virtualMemory, moduleManager),
|
||||
new CpuDispatcher(virtualMemory, moduleManager, hostPlatform: hostPlatform),
|
||||
moduleManager,
|
||||
Aerolib.Instance,
|
||||
cpuExecutionOptions,
|
||||
@@ -140,7 +145,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
LastMilestoneLog = null;
|
||||
KernelModuleRegistry.Reset();
|
||||
var image = LoadImage(normalizedEbootPath);
|
||||
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
|
||||
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version, BuildInfo.CommitSha);
|
||||
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
|
||||
LogAppBundleInfo(normalizedEbootPath, image);
|
||||
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
|
||||
|
||||
@@ -65,7 +65,10 @@
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
},
|
||||
"sharpemu.hle": {
|
||||
"type": "Project"
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"SharpEmu.Logging": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.libs": {
|
||||
"type": "Project",
|
||||
|
||||
@@ -127,6 +127,34 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
</Style>
|
||||
|
||||
<!-- Top-level page switcher (Library / Options): plain transparent
|
||||
buttons, not TabItem, so there is no Fluent selected-tab underline.
|
||||
The active page is conveyed by brightness alone; LB/RB gamepad
|
||||
hints flank the pair. -->
|
||||
<Style Selector="Button.segment">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Foreground" Value="{StaticResource MutedBrush}" />
|
||||
<Setter Property="FontSize" Value="22" />
|
||||
<Setter Property="FontWeight" Value="Bold" />
|
||||
<Setter Property="Padding" Value="6,4" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
</Style>
|
||||
<Style Selector="Button.segment:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
</Style>
|
||||
<Style Selector="Button.segment.active">
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
</Style>
|
||||
|
||||
<!-- Gamepad shoulder-button hint chip (LB/RB, L1/R1). -->
|
||||
<Style Selector="Border.padHint">
|
||||
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
|
||||
<Setter Property="CornerRadius" Value="6" />
|
||||
<Setter Property="Padding" Value="8,3" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ContextMenu">
|
||||
<Setter Property="Background" Value="{StaticResource CardBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using Avalonia;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Templates;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Platform;
|
||||
using Avalonia.Threading;
|
||||
using System.Collections.Specialized;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
public sealed class ConsoleWindow : Window
|
||||
{
|
||||
private readonly AvaloniaList<LogLine> _sourceLines;
|
||||
private readonly AvaloniaList<LogLine> _visibleLines = new();
|
||||
private readonly ListBox _list;
|
||||
private readonly TextBox _searchBox;
|
||||
private readonly CheckBox _autoScrollCheck;
|
||||
|
||||
public ConsoleWindow(
|
||||
AvaloniaList<LogLine> lines,
|
||||
Action clear,
|
||||
bool autoScroll)
|
||||
{
|
||||
var loc = Localization.Instance;
|
||||
|
||||
_sourceLines = lines;
|
||||
Title = loc.Get("Console.WindowTitle");
|
||||
Width = 980;
|
||||
Height = 620;
|
||||
MinWidth = 520;
|
||||
MinHeight = 320;
|
||||
Background = new SolidColorBrush(Color.Parse("#0D1017"));
|
||||
Icon = new WindowIcon(AssetLoader.Open(new Uri("avares://SharpEmu.GUI/Assets/SharpEmu.ico")));
|
||||
|
||||
_searchBox = new TextBox
|
||||
{
|
||||
Watermark = loc.Get("Console.SearchWatermark"),
|
||||
Width = 320,
|
||||
Margin = new Thickness(0, 0, 12, 0),
|
||||
};
|
||||
_autoScrollCheck = new CheckBox
|
||||
{
|
||||
Content = loc.Get("Console.AutoScroll"),
|
||||
IsChecked = autoScroll,
|
||||
FontSize = 12,
|
||||
Margin = new Thickness(0, 0, 12, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
var copyButton = new Button
|
||||
{
|
||||
Classes = { "ghost" },
|
||||
Content = loc.Get("Console.Copy"),
|
||||
Padding = new Thickness(10, 4),
|
||||
Margin = new Thickness(0, 0, 8, 0),
|
||||
};
|
||||
var clearButton = new Button
|
||||
{
|
||||
Classes = { "ghost" },
|
||||
Content = loc.Get("Console.Clear"),
|
||||
Padding = new Thickness(10, 4),
|
||||
};
|
||||
copyButton.Click += async (_, _) => await CopyAsync();
|
||||
clearButton.Click += (_, _) => clear();
|
||||
_searchBox.TextChanged += (_, _) => RefreshVisibleLines();
|
||||
|
||||
_list = new ListBox
|
||||
{
|
||||
Classes = { "console" },
|
||||
ItemsSource = _visibleLines,
|
||||
BorderThickness = new Thickness(1),
|
||||
BorderBrush = new SolidColorBrush(Color.Parse("#232B3A")),
|
||||
ItemTemplate = new FuncDataTemplate<LogLine>((_, _) =>
|
||||
{
|
||||
var text = new TextBlock { TextWrapping = TextWrapping.NoWrap };
|
||||
text.Bind(TextBlock.TextProperty, new Binding(nameof(LogLine.Text)));
|
||||
text.Bind(TextBlock.ForegroundProperty, new Binding(nameof(LogLine.Brush)));
|
||||
return text;
|
||||
}),
|
||||
};
|
||||
|
||||
Content = new Grid
|
||||
{
|
||||
Margin = new Thickness(12),
|
||||
RowDefinitions = new RowDefinitions("Auto,*"),
|
||||
Children =
|
||||
{
|
||||
new Grid
|
||||
{
|
||||
Margin = new Thickness(0, 0, 0, 8),
|
||||
ColumnDefinitions = new ColumnDefinitions("*,Auto,Auto,Auto,Auto"),
|
||||
Children =
|
||||
{
|
||||
new TextBlock
|
||||
{
|
||||
Classes = { "sectionTitle" },
|
||||
Text = loc.Get("Console.Title"),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
},
|
||||
_searchBox.WithGridColumn(1),
|
||||
_autoScrollCheck.WithGridColumn(2),
|
||||
copyButton.WithGridColumn(3),
|
||||
clearButton.WithGridColumn(4),
|
||||
},
|
||||
},
|
||||
_list.WithGridRow(1),
|
||||
},
|
||||
};
|
||||
|
||||
lines.CollectionChanged += OnLinesChanged;
|
||||
Closed += (_, _) => lines.CollectionChanged -= OnLinesChanged;
|
||||
RefreshVisibleLines();
|
||||
}
|
||||
|
||||
private void OnLinesChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
RefreshVisibleLines();
|
||||
if (_autoScrollCheck.IsChecked == true)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() => (_list.Scroll as ScrollViewer)?.ScrollToEnd());
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshVisibleLines()
|
||||
{
|
||||
var query = _searchBox.Text ?? string.Empty;
|
||||
_visibleLines.Clear();
|
||||
_visibleLines.AddRange(string.IsNullOrWhiteSpace(query)
|
||||
? _sourceLines
|
||||
: _sourceLines.Where(line => line.Text.Contains(query, StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
|
||||
private async Task CopyAsync()
|
||||
{
|
||||
if (_visibleLines.Count == 0 || Clipboard is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await Clipboard.SetTextAsync(string.Join(Environment.NewLine, _visibleLines.Select(line => line.Text)));
|
||||
}
|
||||
}
|
||||
|
||||
file static class GridExtensions
|
||||
{
|
||||
public static T WithGridColumn<T>(this T control, int column) where T : Control
|
||||
{
|
||||
Grid.SetColumn(control, column);
|
||||
return control;
|
||||
}
|
||||
|
||||
public static T WithGridRow<T>(this T control, int row) where T : Control
|
||||
{
|
||||
Grid.SetRow(control, row);
|
||||
return control;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.IO.Pipes;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal Discord Rich Presence client. Speaks Discord's local IPC
|
||||
/// protocol (framed JSON over the discord-ipc-N named pipe) directly, so no
|
||||
/// external dependency is needed. All failures are silent: no Discord, no
|
||||
/// presence, no impact on the launcher.
|
||||
/// </summary>
|
||||
public sealed class DiscordRichPresence : IDisposable
|
||||
{
|
||||
private const int OpHandshake = 0;
|
||||
private const int OpFrame = 1;
|
||||
private const int MaxFrameBytes = 64 * 1024;
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
|
||||
};
|
||||
|
||||
private readonly object _gate = new();
|
||||
private readonly string _clientId;
|
||||
private readonly AutoResetEvent _signal = new(false);
|
||||
private readonly Thread _worker;
|
||||
|
||||
private NamedPipeClientStream? _pipe;
|
||||
private PresenceState? _desired;
|
||||
private PresenceState? _sent;
|
||||
private volatile bool _disposed;
|
||||
|
||||
private sealed record PresenceState(string Details, string? State, long? StartUnixSeconds);
|
||||
|
||||
public DiscordRichPresence(string clientId)
|
||||
{
|
||||
_clientId = clientId;
|
||||
_worker = new Thread(WorkerLoop)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "DiscordRichPresence",
|
||||
};
|
||||
_worker.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests the given presence; the worker applies it asynchronously and
|
||||
/// retries (with reconnection) until it is delivered or replaced.
|
||||
/// </summary>
|
||||
public void SetPresence(string details, string? state = null, long? startUnixSeconds = null)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_desired = new PresenceState(details, state, startUnixSeconds);
|
||||
}
|
||||
|
||||
Trace($"queued details=\"{details}\" state=\"{state}\"");
|
||||
_signal.Set();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_disposed = true;
|
||||
_signal.Set();
|
||||
ClosePipe();
|
||||
}
|
||||
|
||||
private void WorkerLoop()
|
||||
{
|
||||
while (!_disposed)
|
||||
{
|
||||
// Woken immediately on presence changes; the periodic tick
|
||||
// retries after connection failures (e.g. Discord starts later).
|
||||
_signal.WaitOne(TimeSpan.FromSeconds(15));
|
||||
if (_disposed || _clientId.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
PresenceState? desired;
|
||||
lock (_gate)
|
||||
{
|
||||
desired = _desired;
|
||||
}
|
||||
|
||||
if (desired is null || desired == _sent)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
EnsureConnected();
|
||||
SendActivity(desired);
|
||||
_sent = desired;
|
||||
Trace($"sent details=\"{desired.Details}\" state=\"{desired.State}\"");
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// Connection died (or never existed); reconnect on a later tick.
|
||||
Trace($"send failed: {exception.Message}");
|
||||
ClosePipe();
|
||||
_sent = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureConnected()
|
||||
{
|
||||
if (_pipe is { IsConnected: true })
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ClosePipe();
|
||||
Exception? lastError = null;
|
||||
for (var index = 0; index < 10; index++)
|
||||
{
|
||||
var pipe = new NamedPipeClientStream(
|
||||
".",
|
||||
$"discord-ipc-{index}",
|
||||
PipeDirection.InOut,
|
||||
PipeOptions.Asynchronous);
|
||||
try
|
||||
{
|
||||
pipe.Connect(500);
|
||||
_pipe = pipe;
|
||||
WriteFrame(OpHandshake, JsonSerializer.Serialize(new
|
||||
{
|
||||
v = 1,
|
||||
client_id = _clientId,
|
||||
}));
|
||||
|
||||
// Discord replies with a READY (or error) frame; consume it
|
||||
// so the pipe buffer stays drained.
|
||||
_ = ReadFrame();
|
||||
return;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
lastError = exception;
|
||||
pipe.Dispose();
|
||||
_pipe = null;
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError ?? new IOException("Discord IPC pipe not found.");
|
||||
}
|
||||
|
||||
private void SendActivity(PresenceState presence)
|
||||
{
|
||||
var payload = new Dictionary<string, object?>
|
||||
{
|
||||
["cmd"] = "SET_ACTIVITY",
|
||||
["nonce"] = Guid.NewGuid().ToString(),
|
||||
["args"] = new Dictionary<string, object?>
|
||||
{
|
||||
["pid"] = Environment.ProcessId,
|
||||
["activity"] = new Dictionary<string, object?>
|
||||
{
|
||||
["details"] = presence.Details,
|
||||
["state"] = presence.State,
|
||||
["timestamps"] = presence.StartUnixSeconds is { } start
|
||||
? new Dictionary<string, object?> { ["start"] = start }
|
||||
: null,
|
||||
["assets"] = new Dictionary<string, object?>
|
||||
{
|
||||
["large_image"] = "logo",
|
||||
["large_text"] = "SharpEmu",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
WriteFrame(OpFrame, JsonSerializer.Serialize(payload, JsonOptions));
|
||||
|
||||
// Consume Discord's acknowledgement to keep the pipe drained.
|
||||
_ = ReadFrame();
|
||||
}
|
||||
|
||||
private void WriteFrame(int opcode, string json)
|
||||
{
|
||||
var pipe = _pipe ?? throw new IOException("Discord IPC pipe is not connected.");
|
||||
var payload = Encoding.UTF8.GetBytes(json);
|
||||
var frame = new byte[8 + payload.Length];
|
||||
BitConverter.TryWriteBytes(frame.AsSpan(0, 4), opcode);
|
||||
BitConverter.TryWriteBytes(frame.AsSpan(4, 4), payload.Length);
|
||||
payload.CopyTo(frame, 8);
|
||||
pipe.Write(frame, 0, frame.Length);
|
||||
pipe.Flush();
|
||||
}
|
||||
|
||||
private string ReadFrame()
|
||||
{
|
||||
var pipe = _pipe ?? throw new IOException("Discord IPC pipe is not connected.");
|
||||
var header = ReadExactly(pipe, 8);
|
||||
var length = BitConverter.ToInt32(header, 4);
|
||||
if (length is < 0 or > MaxFrameBytes)
|
||||
{
|
||||
throw new IOException($"Discord IPC frame length {length} is out of range.");
|
||||
}
|
||||
|
||||
return Encoding.UTF8.GetString(ReadExactly(pipe, length));
|
||||
}
|
||||
|
||||
private static byte[] ReadExactly(NamedPipeClientStream pipe, int count)
|
||||
{
|
||||
var buffer = new byte[count];
|
||||
var read = 0;
|
||||
while (read < count)
|
||||
{
|
||||
var chunk = pipe.Read(buffer, read, count - read);
|
||||
if (chunk <= 0)
|
||||
{
|
||||
throw new IOException("Discord IPC pipe closed.");
|
||||
}
|
||||
|
||||
read += chunk;
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private static void Trace(string message)
|
||||
{
|
||||
if (string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_DISCORD"),
|
||||
"1",
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine($"[DISCORD] {message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void ClosePipe()
|
||||
{
|
||||
var pipe = _pipe;
|
||||
_pipe = null;
|
||||
try
|
||||
{
|
||||
pipe?.Dispose();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Best effort; the pipe may already be broken.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,14 @@ internal sealed class EmulatorProcess : IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefer terminating the job: it kills the whole tree, including
|
||||
// any children the emulator spawned, even when the main process
|
||||
// is wedged in a GPU driver call.
|
||||
if (_jobHandle != 0)
|
||||
{
|
||||
_ = TerminateJobObject(_jobHandle, 1);
|
||||
}
|
||||
|
||||
if (_processHandle != 0)
|
||||
{
|
||||
_ = TerminateProcess(_processHandle, 1);
|
||||
@@ -160,8 +168,7 @@ internal sealed class EmulatorProcess : IDisposable
|
||||
var policy1 = PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF;
|
||||
var policy2 =
|
||||
PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF |
|
||||
PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF |
|
||||
PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF;
|
||||
PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF;
|
||||
|
||||
mitigationPolicies = Marshal.AllocHGlobal(sizeof(ulong) * 2);
|
||||
Marshal.WriteInt64(mitigationPolicies, unchecked((long)policy1));
|
||||
@@ -192,29 +199,10 @@ internal sealed class EmulatorProcess : IDisposable
|
||||
ref startupInfoEx,
|
||||
out var processInfo);
|
||||
|
||||
if (!created)
|
||||
{
|
||||
// Some mitigation policy bits (e.g. XFG) are not supported on
|
||||
// older Windows builds. Mirror the CLI's behavior and fall back
|
||||
// to launching without the mitigation attribute list.
|
||||
startupInfoEx.lpAttributeList = 0;
|
||||
created = CreateProcessW(
|
||||
exePath,
|
||||
new StringBuilder(BuildCommandLine(exePath, arguments)),
|
||||
0,
|
||||
0,
|
||||
true,
|
||||
CREATE_NO_WINDOW,
|
||||
0,
|
||||
currentDirectory,
|
||||
ref startupInfoEx,
|
||||
out processInfo);
|
||||
}
|
||||
|
||||
if (!created)
|
||||
{
|
||||
var error = Marshal.GetLastWin32Error();
|
||||
throw new Win32Exception(error, $"Failed to start '{exePath}' (Win32 error {error}: {new Win32Exception(error).Message}).");
|
||||
throw new Win32Exception(error, $"Failed to start '{exePath}' with CET/CFG mitigation disabled (Win32 error {error}: {new Win32Exception(error).Message}).");
|
||||
}
|
||||
|
||||
CloseHandle(processInfo.hThread);
|
||||
@@ -639,6 +627,10 @@ internal sealed class EmulatorProcess : IDisposable
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool TerminateProcess(nint process, uint exitCode);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool TerminateJobObject(nint job, uint exitCode);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool CloseHandle(nint handle);
|
||||
|
||||
@@ -29,10 +29,12 @@ public sealed class GameEntry : INotifyPropertyChanged
|
||||
private long _sizeBytes;
|
||||
|
||||
public GameEntry(
|
||||
string name, string? titleId, string path, long sizeBytes, string? coverPath, string? backgroundPath)
|
||||
string name, string? titleId, string? version, string path, long sizeBytes,
|
||||
string? coverPath, string? backgroundPath)
|
||||
{
|
||||
Name = name;
|
||||
TitleId = titleId;
|
||||
Version = version;
|
||||
Path = path;
|
||||
_sizeBytes = sizeBytes;
|
||||
CoverPath = coverPath;
|
||||
@@ -46,6 +48,9 @@ public sealed class GameEntry : INotifyPropertyChanged
|
||||
|
||||
public string? TitleId { get; }
|
||||
|
||||
/// <summary>Content version from sce_sys/param.json, e.g. "01.000.000".</summary>
|
||||
public string? Version { get; }
|
||||
|
||||
public string Path { get; }
|
||||
|
||||
/// <summary>
|
||||
@@ -65,7 +70,7 @@ public sealed class GameEntry : INotifyPropertyChanged
|
||||
|
||||
_sizeBytes = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SizeBytes)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Detail)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SizeText)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,9 +111,15 @@ public sealed class GameEntry : INotifyPropertyChanged
|
||||
|
||||
public bool HasCover => _cover is not null;
|
||||
|
||||
public string Detail => TitleId is not null
|
||||
? $"{TitleId} • {FormatSize(SizeBytes)}"
|
||||
: FormatSize(SizeBytes);
|
||||
public bool HasTitleId => TitleId is not null;
|
||||
|
||||
/// <summary>Badge text shown in the launch bar, e.g. "v01.000.000".</summary>
|
||||
public string? VersionText => Version is null ? null : $"v{Version}";
|
||||
|
||||
public bool HasVersion => Version is not null;
|
||||
|
||||
/// <summary>Formatted install size badge shown in the launch bar.</summary>
|
||||
public string SizeText => FormatSize(SizeBytes);
|
||||
|
||||
private static string ComputeInitials(string name)
|
||||
{
|
||||
|
||||
@@ -23,14 +23,41 @@ public sealed class GuiSettings
|
||||
|
||||
public bool StrictDynlibResolution { get; set; }
|
||||
|
||||
/// <summary>Mirror emulator output to user/logs/<titleId>-<timestamp>.log.</summary>
|
||||
/// <summary>
|
||||
/// Mirror emulator output to user/logs/<titleId>-<timestamp>.log, if <see cref="LogFilePath"/> is null.
|
||||
/// </summary>
|
||||
public bool LogToFile { get; set; }
|
||||
|
||||
/// <summary>If <see cref="LogToFile"/> is true it logs to this file path.</summary>
|
||||
public string? LogFilePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If <see cref="OverrideLogFile"/> is false it appends <titleId>-<timestamp> to the filename specified by
|
||||
/// <see cref="LogFilePath"/>. Otherwise it uses the exact filename from <see cref="LogFilePath"/>
|
||||
/// </summary>
|
||||
public bool OverrideLogFile { get; set; }
|
||||
|
||||
/// <summary>Loop the selected game's sce_sys/snd0.at9 preview music.</summary>
|
||||
public bool PlayTitleMusic { get; set; } = true;
|
||||
|
||||
public string? EmulatorPath { get; set; }
|
||||
|
||||
/// <summary>UI language, matching a file code under Languages/ (e.g. "en", "tr").</summary>
|
||||
public string Language { get; set; } = "en";
|
||||
|
||||
/// <summary>Publish launcher/game status to Discord Rich Presence.</summary>
|
||||
public bool DiscordRichPresence { get; set; } = true;
|
||||
|
||||
/// <summary>Names of SHARPEMU_* switches set to "1" in the emulator's environment at launch.</summary>
|
||||
public List<string> EnvironmentToggles { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Discord application ID used for Rich Presence; the default is the
|
||||
/// SharpEmu application. Override to rebrand what Discord shows as
|
||||
/// "Playing …" (register at discord.com/developers/applications).
|
||||
/// </summary>
|
||||
public string DiscordClientId { get; set; } = "1525606762248540221";
|
||||
|
||||
// The emulator is portable and keeps its data next to the executable;
|
||||
// the GUI follows the same convention.
|
||||
public static string SettingsPath => Path.Combine(AppContext.BaseDirectory, "gui-settings.json");
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"_languageName": "العربية",
|
||||
|
||||
"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": "ملفات السجل"
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"_languageName": "Português (Brasil)",
|
||||
|
||||
"Page.Library": "Biblioteca",
|
||||
"Page.Options": "Opções",
|
||||
"Page.GameCount.One": "1 Jogo",
|
||||
"Page.GameCount.Other": "{0} jogos",
|
||||
|
||||
"Library.SearchWatermark": "Pesquisar na biblioteca…",
|
||||
"Library.AddFolder": "+ Adicionar pasta",
|
||||
"Library.Rescan": "⟳ Atualizar biblioteca",
|
||||
"Library.OpenFile": "Abrir arquivo…",
|
||||
|
||||
"Library.Context.Launch": "Jogar",
|
||||
"Library.Context.OpenFolder": "Abrir pasta do jogo",
|
||||
"Library.Context.CopyPath": "Copiar o caminho",
|
||||
"Library.Context.CopyTitleId": "Copiar ID do título",
|
||||
"Library.Context.Remove": "Remover da biblioteca",
|
||||
|
||||
"Library.Empty.Title": "Sua biblioteca está vazia",
|
||||
"Library.Empty.Hint": "Adicione uma pasta contendo seus jogos para começar.",
|
||||
"Library.Empty.SearchTitle": "Nenhum jogo corresponde à sua busca",
|
||||
"Library.Empty.SearchHint": "Nada na biblioteca corresponde a “{0}”.",
|
||||
"Library.Empty.AddFolder": "+ Adicionar pasta do jogo",
|
||||
|
||||
"Library.Loading": "Carregando biblioteca…",
|
||||
|
||||
"Options.General": "Opções Gerais",
|
||||
"Options.Section.Emulation": "EMULAÇÃO",
|
||||
"Options.Section.Logging": "LOGS",
|
||||
"Options.Section.Launcher": "INICIALIZADOR",
|
||||
|
||||
"Options.CpuEngine.Label": "Motor da CPU",
|
||||
"Options.CpuEngine.Desc": "Motor de execução usado para executar o código do jogo.",
|
||||
"Options.CpuEngine.Native": "Nativo",
|
||||
|
||||
"Options.Strict.Label": "Resolução estrita de bibliotecas dinâmicas",
|
||||
"Options.Strict.Desc": "Interrompe a inicialização caso um símbolo importado não possa ser vinculado.",
|
||||
|
||||
"Options.LogLevel.Label": "Nível de log",
|
||||
"Options.LogLevel.Desc": "Nível de detalhamento das mensagens exibidas no console do emulador.",
|
||||
"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 de rastreamento de importações",
|
||||
"Options.TraceImports.Desc": "Rastreia as primeiras N importações de cada módulo (0 = desativado).",
|
||||
|
||||
"Options.LogToFile.Label": "Salvar log em arquivo",
|
||||
"Options.LogToFile.Desc": "Copia a saída do emulador para um arquivo de log.",
|
||||
|
||||
"Options.LogFilePath.Label": "Caminho do arquivo de log",
|
||||
"Options.LogFilePath.Default": "Nenhum caminho definido — logs vão para user/logs na pasta do emulador.",
|
||||
"Options.LogFilePath.Select": "Selecionar…",
|
||||
|
||||
"Options.OverrideLogFile.Label": "Sobrescrever arquivo de log",
|
||||
"Options.OverrideLogFile.Desc": "Use o caminho exato do arquivo em vez de adicionar o ID do título e o log de data e hora.",
|
||||
|
||||
"Options.TitleMusic.Label": "Música de prévia",
|
||||
"Options.TitleMusic.Desc": "Reproduz em loop a música de prévia do jogo selecionado na biblioteca.",
|
||||
|
||||
"Options.Discord.Label": "Status do Discord",
|
||||
"Options.Discord.Desc": "Exibir o jogo em execução no seu perfil do Discord.",
|
||||
|
||||
"Options.Language.Label": "Idioma do emulador",
|
||||
"Options.Language.Desc": "Idioma usado em toda a interface do emulador. A alteração é aplicada imediatamente.",
|
||||
|
||||
"Common.On": "Ativado",
|
||||
"Common.Off": "Desativado",
|
||||
|
||||
"Console.Title": "CONSOLE",
|
||||
"Console.SearchWatermark": "Pesquisar...",
|
||||
"Console.AutoScroll": "Rolagem automática",
|
||||
"Console.Split": "Recortar",
|
||||
"Console.Copy": "Copiar",
|
||||
"Console.Clear": "Limpar",
|
||||
"Console.WindowTitle": "Console do SharpEmu",
|
||||
|
||||
"Launch.NoGameSelected": "Nenhum jogo selecionado",
|
||||
"Launch.NoGameHint": "Selecione um jogo na biblioteca ou abra um arquivo eboot.bin diretamente.",
|
||||
"Launch.Idle": "Ocioso",
|
||||
"Launch.Console": "≡ Console",
|
||||
"Launch.Launch": "▶ Iniciar",
|
||||
"Launch.Stop": "■ Parar",
|
||||
"Launch.Running": "Em execução — {0}",
|
||||
"Launch.Stopping": "Encerrando…",
|
||||
"Launch.Exited": "Encerrado com código {0} ({1})",
|
||||
"Launch.ExeNotFound": "Executável do SharpEmu não encontrado. Compile primeiro o projeto SharpEmu.CLI (dotnet build).",
|
||||
"Launch.LogFile": "Arquivo de log: {0}",
|
||||
"Launch.Command": "$ SharpEmu {0}",
|
||||
"Launch.StartFailed": "Falha ao iniciar o emulador: {0}",
|
||||
"Launch.ProcessExited": "O processo foi encerrado com código {0} ({1}).",
|
||||
|
||||
"Exit.Ok": "OK",
|
||||
"Exit.InvalidArguments": "argumentos inválidos",
|
||||
"Exit.EbootNotFound": "eboot.bin não encontrado",
|
||||
"Exit.RuntimeException": "exceção em tempo de execução",
|
||||
"Exit.EmulationError": "erro de emulação",
|
||||
"Exit.Unknown": "desconhecido",
|
||||
|
||||
"Status.EmulatorLocating": "Emulador: localizando…",
|
||||
"Status.EmulatorPath": "Emulador: {0}",
|
||||
"Status.EmulatorNotFound": "Emulador: executável do SharpEmu não encontrado — compile o SharpEmu.CLI primeiro.",
|
||||
"Status.ScanningLibrary": "Verificando biblioteca…",
|
||||
"Status.AddFolderPrompt": "Adicione uma pasta de jogos para preencher a biblioteca.",
|
||||
"Status.LibraryScanned": "Biblioteca verificada: {0} jogo(s) em {1} pasta(s).",
|
||||
"Status.CouldNotOpenFolder": "Não foi possível abrir a pasta: {0}",
|
||||
"Status.CopiedToClipboard": "{0} copiado para a área de transferência.",
|
||||
"Status.RemovedFromLibrary": "“{0}” removido da biblioteca. Adicione novamente sua pasta para restaurá-lo.",
|
||||
"Status.Running": "Executando {0}",
|
||||
"Status.Stopping": "Encerrando…",
|
||||
"Status.Idle": "Ocioso",
|
||||
|
||||
"Clipboard.Path": "Caminho",
|
||||
"Clipboard.TitleId": "ID do título",
|
||||
|
||||
"Discord.Playing": "Jogando {0}",
|
||||
"Discord.Browsing": "Navegando pela biblioteca",
|
||||
|
||||
"Dialog.ChooseGameFolder": "Escolha uma pasta contendo jogos",
|
||||
"Dialog.OpenExecutable": "Abrir um executável para iniciar",
|
||||
"Dialog.PsExecutables": "Executáveis de PS",
|
||||
"Dialog.SaveLogFile": "Selecione onde salvar o arquivo de log",
|
||||
"Dialog.PlainTextFiles": "Arquivos de texto simples",
|
||||
"Dialog.LogFiles": "Arquivos de log"
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"_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"
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"_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"
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"_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.Env.Tab": "Environment",
|
||||
"Options.Section.Environment": "ENVIRONMENT VARIABLES",
|
||||
"Options.Env.Desc": "Switches passed to the emulator as environment variables at launch.",
|
||||
"Options.Env.Bthid.Desc": "Report Bluetooth HID as unavailable for titles whose wheel/FFB middleware polls forever.\nLeave off normally. Some titles freeze when init fails.",
|
||||
"Options.Env.LoopGuard.Desc": "Do not force quit titles that repeat the same call for too long.\nTry this when a game exits on its own while loading.",
|
||||
"Options.Env.VkValidation.Desc": "Enable Vulkan validation layers for GPU debugging.\nSlow. Requires the Vulkan SDK to be installed.",
|
||||
"Options.Env.DumpSpirv.Desc": "Dump AGC shaders and their SPIR-V translations to the shader-dumps folder.\nUse when reporting shader or rendering bugs.",
|
||||
"Options.Env.LogDirectMemory.Desc": "Log direct memory allocations and failures to the console.\nUse when a game aborts or exits during boot.",
|
||||
"Options.Env.LogNp.Desc": "Log NP (PlayStation Network) library calls to the console.",
|
||||
"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",
|
||||
|
||||
"Options.About" : "About",
|
||||
"About.Github.Label": "GitHub",
|
||||
"About.Github.Desc": "Source code, issues and project development.",
|
||||
"About.Discord.Label": "Discord",
|
||||
"About.Discord.Desc": "Join the community, get support and follow development.",
|
||||
"About.GithubButton": "Contribute in GitHub!",
|
||||
"About.DiscordButton": "Join our Discord!"
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
{
|
||||
"_languageName": "Español",
|
||||
|
||||
"Page.Library": "Biblioteca",
|
||||
"Page.Options": "Opciones",
|
||||
"Page.GameCount.One": "1 juego",
|
||||
"Page.GameCount.Other": "{0} juegos",
|
||||
|
||||
"Library.SearchWatermark": "Buscar en la biblioteca…",
|
||||
"Library.AddFolder": "+ Añadir carpeta",
|
||||
"Library.Rescan": "⟳ Volver a escanear",
|
||||
"Library.OpenFile": "Abrir archivo…",
|
||||
|
||||
"Library.Context.Launch": "Iniciar",
|
||||
"Library.Context.OpenFolder": "Abrir carpeta de juegos",
|
||||
"Library.Context.CopyPath": "Copiar ruta",
|
||||
"Library.Context.CopyTitleId": "Copiar ID del título",
|
||||
"Library.Context.Remove": "Eliminar de la biblioteca",
|
||||
|
||||
"Library.Empty.Title": "Tu biblioteca está vacía",
|
||||
"Library.Empty.Hint": "Añade una carpeta que contenga tus juegos para empezar.",
|
||||
"Library.Empty.SearchTitle": "Ningún juego coincide con la búsqueda",
|
||||
"Library.Empty.SearchHint": "No se ha encontrado nada en la biblioteca que coincida con “{0}”.",
|
||||
"Library.Empty.AddFolder": "+ Añadir carpeta de juegos",
|
||||
|
||||
"Library.Loading": "Cargando biblioteca…",
|
||||
|
||||
"Options.General": "General",
|
||||
"Options.Section.Emulation": "EMULACIÓN",
|
||||
"Options.Section.Logging": "LOGS",
|
||||
"Options.Section.Launcher": "LAUNCHER",
|
||||
|
||||
"Options.CpuEngine.Label": "Motor de CPU",
|
||||
"Options.CpuEngine.Desc": "Motor utilizado para ejecutar el código del juego.",
|
||||
"Options.CpuEngine.Native": "Nativo",
|
||||
|
||||
"Options.Strict.Label": "Resolución estricta de dynlib (Bibliotecas dinámicas)",
|
||||
"Options.Strict.Desc": "Detener la ejecución cuando un símbolo importado no se pueda resolver.",
|
||||
|
||||
"Options.LogLevel.Label": "Nivel de Log",
|
||||
"Options.LogLevel.Desc": "Verbosidad de la salida en consola del emulador.",
|
||||
"Options.LogLevel.Trace": "Trazas",
|
||||
"Options.LogLevel.Debug": "Depuración",
|
||||
"Options.LogLevel.Info": "Información",
|
||||
"Options.LogLevel.Warning": "Advertencia",
|
||||
"Options.LogLevel.Error": "Error",
|
||||
"Options.LogLevel.Critical": "Crítico",
|
||||
|
||||
"Options.TraceImports.Label": "Límite de trazado de importaciones",
|
||||
"Options.TraceImports.Desc": "Trazar las primeras N importaciones por módulo (0 = off).",
|
||||
|
||||
"Options.LogToFile.Label": "Registrar log en archivo",
|
||||
"Options.LogToFile.Desc": "Duplicar la salida del emulador en un archivo de logs.",
|
||||
|
||||
"Options.LogFilePath.Label": "Ruta del archivo de Log",
|
||||
"Options.LogFilePath.Default": "Sin ruta personalizada — los logs van a user/logs al lado del emulador.",
|
||||
"Options.LogFilePath.Select": "Seleccionar…",
|
||||
|
||||
"Options.OverrideLogFile.Label": "Sobreescribir archivo de logs",
|
||||
"Options.OverrideLogFile.Desc": "Utilizar la misma ruta para el archivo de logs en vez de añadir la ID del título y marca de tiempo.",
|
||||
|
||||
"Options.TitleMusic.Label": "Música del título",
|
||||
"Options.TitleMusic.Desc": "Repetir en bucle la preview de la música del juego seleccionado en la biblioteca.",
|
||||
|
||||
"Options.Discord.Label": "Actividad de Discord",
|
||||
"Options.Discord.Desc": "Mostrar juego en ejecución en tu perfil de Discord.",
|
||||
|
||||
"Options.Language.Label": "Idioma del emulador",
|
||||
"Options.Language.Desc": "Idioma utilizado en todo el launcher. Se aplica inmediatamente.",
|
||||
|
||||
"Common.On": "Encendido",
|
||||
"Common.Off": "Apagado",
|
||||
|
||||
"Console.Title": "CONSOLA",
|
||||
"Console.SearchWatermark": "Buscar...",
|
||||
"Console.AutoScroll": "Desplazamiento automático",
|
||||
"Console.Split": "Desacoplar",
|
||||
"Console.Copy": "Copiar",
|
||||
"Console.Clear": "Limpiar",
|
||||
"Console.WindowTitle": "Consola SharpEmu",
|
||||
|
||||
"Launch.NoGameSelected": "No hay ningún juego seleccionado",
|
||||
"Launch.NoGameHint": "Selecciona un juego de la biblioteca o abre un eboot.bin directamente.",
|
||||
"Launch.Idle": "Inactivo",
|
||||
"Launch.Console": "≡ Consola",
|
||||
"Launch.Launch": "▶ Iniciar",
|
||||
"Launch.Stop": "■ Detener",
|
||||
"Launch.Running": "En ejecución — {0}",
|
||||
"Launch.Stopping": "Deteniendo…",
|
||||
"Launch.Exited": "Finalizó con el código {0} ({1})",
|
||||
"Launch.ExeNotFound": "No se ha encontrado el ejecutable de SharpEmu. Compila previamente el proyecto SharpEmu.CLI (dotnet build).",
|
||||
"Launch.LogFile": "Archivo de Log: {0}",
|
||||
"Launch.Command": "$ SharpEmu {0}",
|
||||
"Launch.StartFailed": "Error al iniciar el emulador: {0}",
|
||||
"Launch.ProcessExited": "El proceso finalizó con el código {0} ({1}).",
|
||||
|
||||
"Exit.Ok": "OK",
|
||||
"Exit.InvalidArguments": "argumentos no válidos",
|
||||
"Exit.EbootNotFound": "no se encontró eboot",
|
||||
"Exit.RuntimeException": "excepción en tiempo de ejecución",
|
||||
"Exit.EmulationError": "error de emulación",
|
||||
"Exit.Unknown": "desconocido",
|
||||
|
||||
"Status.EmulatorLocating": "Emulador: localizando…",
|
||||
"Status.EmulatorPath": "Emulador: {0}",
|
||||
"Status.EmulatorNotFound": "Emulador: No se encontró el ejecutable de SharpEmu — compila previamente SharpEmu.CLI.",
|
||||
"Status.ScanningLibrary": "Escaneando biblioteca…",
|
||||
"Status.AddFolderPrompt": "Añade una carpeta de juegos para poblar la biblioteca.",
|
||||
"Status.LibraryScanned": "Biblioteca escaneada: Se encontraron {0} juego(s) en {1} carpeta(s).",
|
||||
"Status.CouldNotOpenFolder": "No se ha podido abrir la carpeta: {0}",
|
||||
"Status.CopiedToClipboard": "{0} copiado al portapapeles.",
|
||||
"Status.RemovedFromLibrary": "Se eliminó “{0}” de la biblioteca. Vuelve a añadir su carpeta para restaurarlo.",
|
||||
"Status.Running": "Ejecutando {0}",
|
||||
"Status.Stopping": "Deteniendo…",
|
||||
"Status.Idle": "Inactivo",
|
||||
|
||||
"Clipboard.Path": "Ruta",
|
||||
"Clipboard.TitleId": "ID del título",
|
||||
|
||||
"Discord.Playing": "Jugando a {0}",
|
||||
"Discord.Browsing": "Navegando en la biblioteca, buscando un juego para divertirse.",
|
||||
|
||||
"Dialog.ChooseGameFolder": "Selecciona una carpeta que contenga juegos",
|
||||
"Dialog.OpenExecutable": "Abrir un ejecutable para iniciar",
|
||||
"Dialog.PsExecutables": "Ejecutables de PS",
|
||||
"Dialog.SaveLogFile": "Selecciona dónde guardar el archivo de Logs",
|
||||
"Dialog.PlainTextFiles": "Archivos en texto plano",
|
||||
"Dialog.LogFiles": "Archivos de Log",
|
||||
|
||||
"Options.About" : "Informacion",
|
||||
"About.Github.Label": "GitHub",
|
||||
"About.Github.Desc": "Código fuente, issues y desarrollo del proyecto.",
|
||||
"About.Discord.Label": "Discord",
|
||||
"About.Discord.Desc": "Únete a la comunidad, recibe soporte y sigue el desarrollo.",
|
||||
"About.GithubButton": "Contribuye en GitHub!",
|
||||
"About.DiscordButton": "Únete a nuestro Discord!"
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"_languageName": "Français",
|
||||
|
||||
"Page.Library": "Bibliothèque",
|
||||
"Page.Options": "Options",
|
||||
"Page.GameCount.One": "1 jeu",
|
||||
"Page.GameCount.Other": "{0} jeux",
|
||||
|
||||
"Library.SearchWatermark": "Rechercher dans la bibliothèque…",
|
||||
"Library.AddFolder": "+ Ajouter un dossier",
|
||||
"Library.Rescan": "⟳ Analyser à nouveau",
|
||||
"Library.OpenFile": "Ouvrir un fichier…",
|
||||
|
||||
"Library.Context.Launch": "Lancer",
|
||||
"Library.Context.OpenFolder": "Ouvrir le dossier du jeu",
|
||||
"Library.Context.CopyPath": "Copier le chemin",
|
||||
"Library.Context.CopyTitleId": "Copier l’identifiant du jeu",
|
||||
"Library.Context.Remove": "Retirer de la bibliothèque",
|
||||
|
||||
"Library.Empty.Title": "Votre bibliothèque est vide",
|
||||
"Library.Empty.Hint": "Ajoutez un dossier contenant vos jeux pour commencer.",
|
||||
"Library.Empty.SearchTitle": "Aucun jeu ne correspond à votre recherche",
|
||||
"Library.Empty.SearchHint": "Aucun élément de la bibliothèque ne correspond à « {0} ».",
|
||||
"Library.Empty.AddFolder": "+ Ajouter un dossier de jeux",
|
||||
|
||||
"Library.Loading": "Chargement de la bibliothèque…",
|
||||
|
||||
"Options.General": "Général",
|
||||
"Options.Section.Emulation": "ÉMULATION",
|
||||
"Options.Section.Logging": "JOURNALISATION",
|
||||
"Options.Section.Launcher": "LANCEUR",
|
||||
|
||||
"Options.CpuEngine.Label": "Moteur CPU",
|
||||
"Options.CpuEngine.Desc": "Moteur d’exécution utilisé pour exécuter le code du jeu.",
|
||||
"Options.CpuEngine.Native": "Natif",
|
||||
|
||||
"Options.Strict.Label": "Résolution stricte des bibliothèques dynamiques",
|
||||
"Options.Strict.Desc": "Interrompre le lancement lorsqu’un symbole importé ne peut pas être résolu.",
|
||||
|
||||
"Options.LogLevel.Label": "Niveau de journalisation",
|
||||
"Options.LogLevel.Desc": "Niveau de détail des messages affichés dans la console de l’émulateur.",
|
||||
"Options.LogLevel.Trace": "Traçage",
|
||||
"Options.LogLevel.Debug": "Débogage",
|
||||
"Options.LogLevel.Info": "Informations",
|
||||
"Options.LogLevel.Warning": "Avertissements",
|
||||
"Options.LogLevel.Error": "Erreurs",
|
||||
"Options.LogLevel.Critical": "Erreurs critiques",
|
||||
|
||||
"Options.TraceImports.Label": "Limite de traçage des imports",
|
||||
"Options.TraceImports.Desc": "Tracer les N premiers imports de chaque module (0 = désactivé).",
|
||||
|
||||
"Options.LogToFile.Label": "Enregistrer dans un fichier",
|
||||
"Options.LogToFile.Desc": "Copier la sortie de l’émulateur dans un fichier journal.",
|
||||
|
||||
"Options.LogFilePath.Label": "Chemin du fichier journal",
|
||||
"Options.LogFilePath.Default": "Aucun chemin personnalisé — les journaux sont enregistrés dans user/logs à côté de l’émulateur.",
|
||||
"Options.LogFilePath.Select": "Sélectionner…",
|
||||
|
||||
"Options.OverrideLogFile.Label": "Remplacer le fichier journal",
|
||||
"Options.OverrideLogFile.Desc": "Utiliser exactement ce chemin au lieu d’ajouter l’identifiant du jeu et l’horodatage.",
|
||||
|
||||
"Options.TitleMusic.Label": "Musique du jeu",
|
||||
"Options.TitleMusic.Desc": "Lire en boucle la musique d’aperçu du jeu sélectionné dans la bibliothèque.",
|
||||
|
||||
"Options.Discord.Label": "Présence Discord",
|
||||
"Options.Discord.Desc": "Afficher le jeu en cours d’exécution sur votre profil Discord.",
|
||||
|
||||
"Options.Language.Label": "Langue de l’émulateur",
|
||||
"Options.Language.Desc": "Langue utilisée dans l’ensemble du lanceur. Le changement est immédiat.",
|
||||
|
||||
"Common.On": "Activé",
|
||||
"Common.Off": "Désactivé",
|
||||
|
||||
"Console.Title": "CONSOLE",
|
||||
"Console.SearchWatermark": "Rechercher…",
|
||||
"Console.AutoScroll": "Défilement automatique",
|
||||
"Console.Split": "Détacher",
|
||||
"Console.Copy": "Copier",
|
||||
"Console.Clear": "Effacer",
|
||||
"Console.WindowTitle": "Console SharpEmu",
|
||||
|
||||
"Launch.NoGameSelected": "Aucun jeu sélectionné",
|
||||
"Launch.NoGameHint": "Choisissez un jeu dans la bibliothèque ou ouvrez directement un fichier eboot.bin.",
|
||||
"Launch.Idle": "Inactif",
|
||||
"Launch.Console": "≡ Console",
|
||||
"Launch.Launch": "▶ Lancer",
|
||||
"Launch.Stop": "■ Arrêter",
|
||||
"Launch.Running": "En cours d’exécution — {0}",
|
||||
"Launch.Stopping": "Arrêt en cours…",
|
||||
"Launch.Exited": "Processus terminé avec le code {0} ({1})",
|
||||
"Launch.ExeNotFound": "L’exécutable SharpEmu est introuvable. Compilez d’abord le projet SharpEmu.CLI (dotnet build).",
|
||||
"Launch.LogFile": "Fichier journal : {0}",
|
||||
"Launch.Command": "$ SharpEmu {0}",
|
||||
"Launch.StartFailed": "Impossible de démarrer l’émulateur : {0}",
|
||||
"Launch.ProcessExited": "Le processus s’est terminé avec le code {0} ({1}).",
|
||||
|
||||
"Exit.Ok": "OK",
|
||||
"Exit.InvalidArguments": "arguments non valides",
|
||||
"Exit.EbootNotFound": "eboot introuvable",
|
||||
"Exit.RuntimeException": "exception d’exécution",
|
||||
"Exit.EmulationError": "erreur d’émulation",
|
||||
"Exit.Unknown": "inconnu",
|
||||
|
||||
"Status.EmulatorLocating": "Émulateur : recherche en cours…",
|
||||
"Status.EmulatorPath": "Émulateur : {0}",
|
||||
"Status.EmulatorNotFound": "Émulateur : l’exécutable SharpEmu est introuvable — compilez d’abord SharpEmu.CLI.",
|
||||
"Status.ScanningLibrary": "Analyse de la bibliothèque…",
|
||||
"Status.AddFolderPrompt": "Ajoutez un dossier de jeux pour remplir la bibliothèque.",
|
||||
"Status.LibraryScanned": "Bibliothèque analysée : {0} jeu(x) dans {1} dossier(s).",
|
||||
"Status.CouldNotOpenFolder": "Impossible d’ouvrir le dossier : {0}",
|
||||
"Status.CopiedToClipboard": "{0} copié dans le presse-papiers.",
|
||||
"Status.RemovedFromLibrary": "« {0} » a été retiré de la bibliothèque. Ajoutez à nouveau son dossier pour le restaurer.",
|
||||
"Status.Running": "Exécution de {0}",
|
||||
"Status.Stopping": "Arrêt en cours…",
|
||||
"Status.Idle": "Inactif",
|
||||
|
||||
"Clipboard.Path": "Chemin",
|
||||
"Clipboard.TitleId": "Identifiant du jeu",
|
||||
|
||||
"Discord.Playing": "Joue à {0}",
|
||||
"Discord.Browsing": "Parcourt la bibliothèque",
|
||||
|
||||
"Dialog.ChooseGameFolder": "Choisir un dossier contenant des jeux",
|
||||
"Dialog.OpenExecutable": "Ouvrir un exécutable à lancer",
|
||||
"Dialog.PsExecutables": "Exécutables PlayStation",
|
||||
"Dialog.SaveLogFile": "Choisir l’emplacement du fichier journal",
|
||||
"Dialog.PlainTextFiles": "Fichiers texte brut",
|
||||
"Dialog.LogFiles": "Fichiers journaux"
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
{
|
||||
"_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"
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"_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": "CPUエンジン",
|
||||
"Options.CpuEngine.Desc": "ゲームコードを実行するために使用される実行エンジン。",
|
||||
"Options.CpuEngine.Native": "ネイティブ",
|
||||
|
||||
"Options.Strict.Label": "厳格な動的ライブラリ解決",
|
||||
"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": "ゲーム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": "ライブラリのスキャン完了: {1} 個のフォルダーから {0} 本のゲームを検出。",
|
||||
"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": "PlayStation 実行ファイル",
|
||||
"Dialog.SaveLogFile": "ログファイルの保存先を選択",
|
||||
"Dialog.PlainTextFiles": "プレーンテキストファイル",
|
||||
"Dialog.LogFiles": "ログファイル"
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"_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": "CPU 엔진",
|
||||
"Options.CpuEngine.Desc": "게임 코드를 실행하는 데 사용되는 실행 엔진입니다.",
|
||||
"Options.CpuEngine.Native": "네이티브",
|
||||
|
||||
"Options.Strict.Label": "엄격한 동적 라이브러리 해석",
|
||||
"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": "게임 ID와 타임스탬프를 추가하는 대신 정확히 이 경로를 사용합니다.",
|
||||
|
||||
"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": "라이브러리 스캔 완료: {1}개 폴더에서 {0}개 게임 발견.",
|
||||
"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": "PlayStation 실행 파일",
|
||||
"Dialog.SaveLogFile": "로그 파일 저장 위치 선택",
|
||||
"Dialog.PlainTextFiles": "일반 텍스트 파일",
|
||||
"Dialog.LogFiles": "로그 파일"
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"_languageName": "Nederlands",
|
||||
|
||||
"Page.Library": "Bibliotheek",
|
||||
"Page.Options": "Opties",
|
||||
"Page.GameCount.One": "1 game",
|
||||
"Page.GameCount.Other": "{0} games",
|
||||
|
||||
"Library.SearchWatermark": "Zoeken in bibliotheek…",
|
||||
"Library.AddFolder": "+ Map toevoegen",
|
||||
"Library.Rescan": "⟳ Opnieuw scannen",
|
||||
"Library.OpenFile": "Bestand openen…",
|
||||
|
||||
"Library.Context.Launch": "Starten",
|
||||
"Library.Context.OpenFolder": "Gamemap openen",
|
||||
"Library.Context.CopyPath": "Pad kopiëren",
|
||||
"Library.Context.CopyTitleId": "Titel-ID kopiëren",
|
||||
"Library.Context.Remove": "Verwijderen uit bibliotheek",
|
||||
|
||||
"Library.Empty.Title": "Je bibliotheek is leeg",
|
||||
"Library.Empty.Hint": "Voeg een map met je games toe om te beginnen.",
|
||||
"Library.Empty.SearchTitle": "Geen games komen overeen met je zoekopdracht",
|
||||
"Library.Empty.SearchHint": "Niets in de bibliotheek komt overeen met “{0}”.",
|
||||
"Library.Empty.AddFolder": "+ Gamemap toevoegen",
|
||||
|
||||
"Library.Loading": "Bibliotheek laden…",
|
||||
|
||||
"Options.General": "Algemeen",
|
||||
"Options.Section.Emulation": "EMULATIE",
|
||||
"Options.Section.Logging": "LOGGING",
|
||||
"Options.Section.Launcher": "LAUNCHER",
|
||||
|
||||
"Options.CpuEngine.Label": "CPU-engine",
|
||||
"Options.CpuEngine.Desc": "Engine die wordt gebruikt om gamecode uit te voeren.",
|
||||
"Options.CpuEngine.Native": "Native",
|
||||
|
||||
"Options.Strict.Label": "Strikte dynlib-resolutie",
|
||||
"Options.Strict.Desc": "Laat het opstarten mislukken wanneer een geïmporteerd symbool niet kan worden opgelost.",
|
||||
|
||||
"Options.LogLevel.Label": "Logniveau",
|
||||
"Options.LogLevel.Desc": "Uitgebreidheid van de console-uitvoer van de emulator.",
|
||||
"Options.LogLevel.Trace": "Trace",
|
||||
"Options.LogLevel.Debug": "Debug",
|
||||
"Options.LogLevel.Info": "Info",
|
||||
"Options.LogLevel.Warning": "Waarschuwing",
|
||||
"Options.LogLevel.Error": "Fout",
|
||||
"Options.LogLevel.Critical": "Kritiek",
|
||||
|
||||
"Options.TraceImports.Label": "Tracelimiet voor imports",
|
||||
"Options.TraceImports.Desc": "Traceer de eerste N imports per module (0 = uit).",
|
||||
|
||||
"Options.LogToFile.Label": "Loggen naar bestand",
|
||||
"Options.LogToFile.Desc": "Stuur de uitvoer van de emulator ook naar een logbestand.",
|
||||
|
||||
"Options.LogFilePath.Label": "Pad naar logbestand",
|
||||
"Options.LogFilePath.Default": "Geen aangepast pad — logs komen terecht in user/logs naast de emulator.",
|
||||
"Options.LogFilePath.Select": "Selecteren…",
|
||||
|
||||
"Options.OverrideLogFile.Label": "Logbestand overschrijven",
|
||||
"Options.OverrideLogFile.Desc": "Gebruik het exacte bestandspad in plaats van de titel-ID en tijdstempel toe te voegen.",
|
||||
|
||||
"Options.TitleMusic.Label": "Titelmuziek",
|
||||
"Options.TitleMusic.Desc": "Herhaal de voorbeeldmuziek van de geselecteerde game in de bibliotheek.",
|
||||
|
||||
"Options.Discord.Label": "Discord-aanwezigheid",
|
||||
"Options.Discord.Desc": "Toon de actieve game op je Discord-profiel.",
|
||||
|
||||
"Options.Language.Label": "Taal van de emulator",
|
||||
"Options.Language.Desc": "Taal die in de hele launcher wordt gebruikt. Wordt direct toegepast.",
|
||||
|
||||
"Common.On": "Aan",
|
||||
"Common.Off": "Uit",
|
||||
|
||||
"Console.Title": "CONSOLE",
|
||||
"Console.SearchWatermark": "Zoeken...",
|
||||
"Console.AutoScroll": "Automatisch scrollen",
|
||||
"Console.Split": "Splitsen",
|
||||
"Console.Copy": "Kopiëren",
|
||||
"Console.Clear": "Wissen",
|
||||
"Console.WindowTitle": "SharpEmu-console",
|
||||
|
||||
"Launch.NoGameSelected": "Geen game geselecteerd",
|
||||
"Launch.NoGameHint": "Kies een game uit de bibliotheek, of open direct een eboot.bin-bestand.",
|
||||
"Launch.Idle": "Inactief",
|
||||
"Launch.Console": "≡ Console",
|
||||
"Launch.Launch": "▶ Starten",
|
||||
"Launch.Stop": "■ Stoppen",
|
||||
"Launch.Running": "Actief — {0}",
|
||||
"Launch.Stopping": "Stoppen…",
|
||||
"Launch.Exited": "Afgesloten met code {0} ({1})",
|
||||
"Launch.ExeNotFound": "SharpEmu-uitvoerbaar bestand niet gevonden. Bouw eerst het SharpEmu.CLI-project (dotnet build).",
|
||||
"Launch.LogFile": "Logbestand: {0}",
|
||||
"Launch.Command": "$ SharpEmu {0}",
|
||||
"Launch.StartFailed": "Starten van de emulator mislukt: {0}",
|
||||
"Launch.ProcessExited": "Proces afgesloten met code {0} ({1}).",
|
||||
|
||||
"Exit.Ok": "OK",
|
||||
"Exit.InvalidArguments": "ongeldige argumenten",
|
||||
"Exit.EbootNotFound": "eboot niet gevonden",
|
||||
"Exit.RuntimeException": "runtime-uitzondering",
|
||||
"Exit.EmulationError": "emulatiefout",
|
||||
"Exit.Unknown": "onbekend",
|
||||
|
||||
"Status.EmulatorLocating": "Emulator: zoeken…",
|
||||
"Status.EmulatorPath": "Emulator: {0}",
|
||||
"Status.EmulatorNotFound": "Emulator: SharpEmu-uitvoerbaar bestand niet gevonden — bouw eerst SharpEmu.CLI.",
|
||||
"Status.ScanningLibrary": "Bibliotheek scannen…",
|
||||
"Status.AddFolderPrompt": "Voeg een gamemap toe om de bibliotheek te vullen.",
|
||||
"Status.LibraryScanned": "Bibliotheek gescand: {0} game(s) in {1} map(pen).",
|
||||
"Status.CouldNotOpenFolder": "Kan map niet openen: {0}",
|
||||
"Status.CopiedToClipboard": "{0} gekopieerd naar klembord.",
|
||||
"Status.RemovedFromLibrary": "“{0}” verwijderd uit de bibliotheek. Voeg de map opnieuw toe om dit te herstellen.",
|
||||
"Status.Running": "Actief {0}",
|
||||
"Status.Stopping": "Stoppen…",
|
||||
"Status.Idle": "Inactief",
|
||||
|
||||
"Clipboard.Path": "Pad",
|
||||
"Clipboard.TitleId": "Titel-ID",
|
||||
|
||||
"Discord.Playing": "Speelt {0}",
|
||||
"Discord.Browsing": "Bladert door de bibliotheek",
|
||||
|
||||
"Dialog.ChooseGameFolder": "Kies een map met games",
|
||||
"Dialog.OpenExecutable": "Open een uitvoerbaar bestand om te starten",
|
||||
"Dialog.PsExecutables": "PS-uitvoerbare bestanden",
|
||||
"Dialog.SaveLogFile": "Selecteer waar het logbestand moet worden opgeslagen",
|
||||
"Dialog.PlainTextFiles": "Platte tekstbestanden",
|
||||
"Dialog.LogFiles": "Logbestanden"
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"_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": "Логи"
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"_languageName": "Türkçe",
|
||||
|
||||
"Page.Library": "Kütüphane",
|
||||
"Page.Options": "Ayarlar",
|
||||
"Page.GameCount.One": "1 oyun",
|
||||
"Page.GameCount.Other": "{0} oyun",
|
||||
|
||||
"Library.SearchWatermark": "Kütüphanede ara…",
|
||||
"Library.AddFolder": "+ Klasör ekle",
|
||||
"Library.Rescan": "⟳ Yeniden tara",
|
||||
"Library.OpenFile": "Dosya aç…",
|
||||
|
||||
"Library.Context.Launch": "Başlat",
|
||||
"Library.Context.OpenFolder": "Oyun klasörünü aç",
|
||||
"Library.Context.CopyPath": "Yolu kopyala",
|
||||
"Library.Context.CopyTitleId": "Title ID'yi kopyala",
|
||||
"Library.Context.Remove": "Kütüphaneden kaldır",
|
||||
|
||||
"Library.Empty.Title": "Kütüphaneniz boş",
|
||||
"Library.Empty.Hint": "Başlamak için oyunlarınızı içeren bir klasör ekleyin.",
|
||||
"Library.Empty.SearchTitle": "Aramanızla eşleşen oyun yok",
|
||||
"Library.Empty.SearchHint": "Kütüphanede “{0}” ile eşleşen bir şey yok.",
|
||||
"Library.Empty.AddFolder": "+ Oyun klasörü ekle",
|
||||
|
||||
"Library.Loading": "Kütüphane yükleniyor…",
|
||||
|
||||
"Options.General": "Genel",
|
||||
"Options.Section.Emulation": "EMÜLASYON",
|
||||
"Options.Section.Logging": "GÜNLÜKLEME",
|
||||
"Options.Section.Launcher": "BAŞLATICI",
|
||||
|
||||
"Options.CpuEngine.Label": "CPU motoru",
|
||||
"Options.CpuEngine.Desc": "Oyun kodunu çalıştırmak için kullanılan yürütme motoru.",
|
||||
"Options.CpuEngine.Native": "Native",
|
||||
|
||||
"Options.Strict.Label": "Katı dynlib çözümü",
|
||||
"Options.Strict.Desc": "İçe aktarılan bir sembol çözümlenemediğinde başlatmayı başarısız kılar.",
|
||||
|
||||
"Options.LogLevel.Label": "Günlük seviyesi",
|
||||
"Options.LogLevel.Desc": "Emülatör konsol çıktısının ayrıntı düzeyi.",
|
||||
"Options.LogLevel.Trace": "Trace",
|
||||
"Options.LogLevel.Debug": "Debug",
|
||||
"Options.LogLevel.Info": "Info",
|
||||
"Options.LogLevel.Warning": "Warning",
|
||||
"Options.LogLevel.Error": "Error",
|
||||
"Options.LogLevel.Critical": "Critical",
|
||||
|
||||
"Options.TraceImports.Label": "Import izleme sınırı",
|
||||
"Options.TraceImports.Desc": "Modül başına ilk N import'u izle (0 = kapalı).",
|
||||
|
||||
"Options.LogToFile.Label": "Dosyaya günlükle",
|
||||
"Options.LogToFile.Desc": "Emülatör çıktısını bir günlük dosyasına yansıt.",
|
||||
|
||||
"Options.LogFilePath.Label": "Günlük dosyası yolu",
|
||||
"Options.LogFilePath.Default": "Özel yol yok — günlükler emülatörün yanındaki user/logs klasörüne yazılır.",
|
||||
"Options.LogFilePath.Select": "Seç…",
|
||||
|
||||
"Options.OverrideLogFile.Label": "Günlük dosyasının üzerine yaz",
|
||||
"Options.OverrideLogFile.Desc": "Title ID ve zaman damgası eklemek yerine tam dosya yolunu kullan.",
|
||||
|
||||
"Options.TitleMusic.Label": "Oyun müziği",
|
||||
"Options.TitleMusic.Desc": "Kütüphanede seçili oyunun önizleme müziğini döngüye al.",
|
||||
|
||||
"Options.Discord.Label": "Discord durumu",
|
||||
"Options.Discord.Desc": "Discord profilinde çalışan oyunu göster.",
|
||||
|
||||
"Options.Language.Label": "Emülatör dili",
|
||||
"Options.Language.Desc": "Başlatıcı genelinde kullanılan dil. Hemen uygulanır.",
|
||||
|
||||
"Common.On": "Açık",
|
||||
"Common.Off": "Kapalı",
|
||||
|
||||
"Console.Title": "KONSOL",
|
||||
"Console.SearchWatermark": "Ara...",
|
||||
"Console.AutoScroll": "Otomatik kaydır",
|
||||
"Console.Split": "Ayır",
|
||||
"Console.Copy": "Kopyala",
|
||||
"Console.Clear": "Temizle",
|
||||
"Console.WindowTitle": "SharpEmu Konsol",
|
||||
|
||||
"Launch.NoGameSelected": "Oyun seçilmedi",
|
||||
"Launch.NoGameHint": "Kütüphaneden bir oyun seçin veya doğrudan bir eboot.bin açın.",
|
||||
"Launch.Idle": "Boşta",
|
||||
"Launch.Console": "≡ Konsol",
|
||||
"Launch.Launch": "▶ Başlat",
|
||||
"Launch.Stop": "■ Durdur",
|
||||
"Launch.Running": "Çalışıyor — {0}",
|
||||
"Launch.Stopping": "Durduruluyor…",
|
||||
"Launch.Exited": "Çıkış kodu {0} ({1})",
|
||||
"Launch.ExeNotFound": "SharpEmu çalıştırılabilir dosyası bulunamadı. Önce SharpEmu.CLI projesini derleyin (dotnet build).",
|
||||
"Launch.LogFile": "Günlük dosyası: {0}",
|
||||
"Launch.Command": "$ SharpEmu {0}",
|
||||
"Launch.StartFailed": "Emülatör başlatılamadı: {0}",
|
||||
"Launch.ProcessExited": "İşlem {0} kodla sonlandı ({1}).",
|
||||
|
||||
"Exit.Ok": "Tamam",
|
||||
"Exit.InvalidArguments": "geçersiz argümanlar",
|
||||
"Exit.EbootNotFound": "eboot bulunamadı",
|
||||
"Exit.RuntimeException": "çalışma zamanı hatası",
|
||||
"Exit.EmulationError": "emülasyon hatası",
|
||||
"Exit.Unknown": "bilinmiyor",
|
||||
|
||||
"Status.EmulatorLocating": "Emülatör: bulunuyor…",
|
||||
"Status.EmulatorPath": "Emülatör: {0}",
|
||||
"Status.EmulatorNotFound": "Emülatör: SharpEmu çalıştırılabilir dosyası bulunamadı — önce SharpEmu.CLI'yi derleyin.",
|
||||
"Status.ScanningLibrary": "Kütüphane taranıyor…",
|
||||
"Status.AddFolderPrompt": "Kütüphaneyi doldurmak için bir oyun klasörü ekleyin.",
|
||||
"Status.LibraryScanned": "Kütüphane tarandı: {1} klasörde {0} oyun.",
|
||||
"Status.CouldNotOpenFolder": "Klasör açılamadı: {0}",
|
||||
"Status.CopiedToClipboard": "{0} panoya kopyalandı.",
|
||||
"Status.RemovedFromLibrary": "“{0}” kütüphaneden kaldırıldı. Geri getirmek için klasörünü yeniden ekleyin.",
|
||||
"Status.Running": "{0} çalışıyor",
|
||||
"Status.Stopping": "Durduruluyor…",
|
||||
"Status.Idle": "Boşta",
|
||||
|
||||
"Clipboard.Path": "Yol",
|
||||
"Clipboard.TitleId": "Title ID",
|
||||
|
||||
"Discord.Playing": "{0} oynuyor",
|
||||
"Discord.Browsing": "Kütüphaneye göz atıyor",
|
||||
|
||||
"Dialog.ChooseGameFolder": "Oyunlarınızı içeren bir klasör seçin",
|
||||
"Dialog.OpenExecutable": "Başlatılacak bir çalıştırılabilir dosya aç",
|
||||
"Dialog.PsExecutables": "PS çalıştırılabilirleri",
|
||||
"Dialog.SaveLogFile": "Günlük dosyasının kaydedileceği yeri seçin",
|
||||
"Dialog.PlainTextFiles": "Düz Metin Dosyaları",
|
||||
"Dialog.LogFiles": "Günlük Dosyaları"
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
/// <summary>
|
||||
/// Loads UI strings for the launcher. Every language ships embedded in the
|
||||
/// assembly (see SharpEmu.GUI.csproj) so a release build is fully
|
||||
/// self-contained; an optional Languages/<code>.json file next to the
|
||||
/// executable overrides the embedded copy for that code, so a translation
|
||||
/// fix or a brand-new language never needs a rebuild.
|
||||
/// </summary>
|
||||
public sealed class Localization
|
||||
{
|
||||
public static Localization Instance { get; } = new();
|
||||
|
||||
public sealed record LanguageInfo(string Code, string NativeName);
|
||||
|
||||
private const string EmbeddedResourcePrefix = "Languages.";
|
||||
private const string EmbeddedResourceSuffix = ".json";
|
||||
|
||||
private Dictionary<string, string> _strings = new();
|
||||
private Dictionary<string, string> _fallbackStrings = 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)
|
||||
{
|
||||
if (_strings.TryGetValue(key, out var value))
|
||||
return value;
|
||||
|
||||
if (_fallbackStrings.TryGetValue(key, out var fallbackValue))
|
||||
return fallbackValue;
|
||||
|
||||
return 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>
|
||||
/// english is the fallback language
|
||||
public void Load(string code)
|
||||
{
|
||||
if (_fallbackStrings.Count == 0 && !string.Equals(code, "en", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!TryLoadLooseFile("en", out var fallback) && !TryLoadEmbedded("en", out fallback))
|
||||
{
|
||||
fallback = new Dictionary<string, string>();
|
||||
}
|
||||
_fallbackStrings = fallback;
|
||||
}
|
||||
else if (string.Equals(code, "en", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (TryLoadLooseFile("en", out var enDict) || TryLoadEmbedded("en", out enDict))
|
||||
{
|
||||
_strings = enDict;
|
||||
_fallbackStrings = enDict;
|
||||
}
|
||||
else
|
||||
{
|
||||
_strings = new Dictionary<string, string>();
|
||||
_fallbackStrings = new Dictionary<string, string>();
|
||||
}
|
||||
CurrentCode = "en";
|
||||
return;
|
||||
}
|
||||
|
||||
// Load the requested language
|
||||
if (TryLoadLooseFile(code, out var loaded) || TryLoadEmbedded(code, out loaded))
|
||||
{
|
||||
_strings = loaded;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_fallbackStrings.Count > 0)
|
||||
_strings = new Dictionary<string, string>(_fallbackStrings);
|
||||
else
|
||||
_strings = new Dictionary<string, string>();
|
||||
}
|
||||
CurrentCode = code;
|
||||
}
|
||||
|
||||
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 TryLoadLooseFile(string code, out Dictionary<string, string> result)
|
||||
{
|
||||
result = new Dictionary<string, string>();
|
||||
try
|
||||
{
|
||||
var path = Path.Combine(LanguagesDirectory, $"{code}.json");
|
||||
if (!File.Exists(path))
|
||||
return false;
|
||||
|
||||
var json = File.ReadAllText(path);
|
||||
return TryLoad(json, out result);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryLoadEmbedded(string code, out Dictionary<string, string> result)
|
||||
{
|
||||
result = new Dictionary<string, string>();
|
||||
try
|
||||
{
|
||||
using var stream = OpenEmbeddedLanguageStream(code);
|
||||
if (stream is null)
|
||||
return false;
|
||||
|
||||
using var reader = new StreamReader(stream);
|
||||
var json = reader.ReadToEnd();
|
||||
return TryLoad(json, out result);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryLoad(string json, out Dictionary<string, string> result)
|
||||
{
|
||||
var loaded = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
|
||||
if (loaded is null)
|
||||
{
|
||||
result = new Dictionary<string, string>();
|
||||
return false;
|
||||
}
|
||||
|
||||
result = loaded;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryLoad(string code, string json)
|
||||
{
|
||||
if (TryLoad(json, out var dict))
|
||||
{
|
||||
_strings = dict;
|
||||
CurrentCode = code;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+457
-150
@@ -9,14 +9,16 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
Title="SharpEmu"
|
||||
Width="1280" Height="820"
|
||||
MinWidth="980" MinHeight="640"
|
||||
WindowState="Maximized"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
Background="{StaticResource BgBrush}"
|
||||
ExtendClientAreaToDecorationsHint="True"
|
||||
ExtendClientAreaChromeHints="PreferSystemChrome"
|
||||
ExtendClientAreaTitleBarHeightHint="44"
|
||||
Icon="avares://SharpEmu.GUI/Assets/SharpEmu.ico">
|
||||
Icon="avares://SharpEmu.GUI/Assets/SharpEmu.ico"
|
||||
KeyDown="OnKeyDown">
|
||||
|
||||
<Grid RowDefinitions="44,*,32">
|
||||
<Grid RowDefinitions="Auto,*,Auto">
|
||||
|
||||
<!-- Selected-game backdrop: key art behind the main content, dimmed by
|
||||
a scrim so tiles and text stay readable. Fades on selection. -->
|
||||
@@ -40,8 +42,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
</Border>
|
||||
</Panel>
|
||||
|
||||
<!-- Title bar -->
|
||||
<Grid x:Name="TitleBar" Grid.Row="0" Background="{StaticResource ChromeBrush}">
|
||||
<!-- Title bar; hidden in fullscreen (F11) along with the status bar, so
|
||||
the game gets the whole screen. -->
|
||||
<Grid x:Name="TitleBar" Grid.Row="0" Height="44" Background="{StaticResource ChromeBrush}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10" Margin="16,0" VerticalAlignment="Center">
|
||||
<Image Source="avares://SharpEmu.GUI/Assets/SharpEmu.ico" Width="20" Height="20"
|
||||
RenderOptions.BitmapInterpolationMode="HighQuality" />
|
||||
@@ -53,119 +56,438 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
</Grid>
|
||||
|
||||
<!-- Main content -->
|
||||
<Grid Grid.Row="1" Margin="18,14,18,14" RowDefinitions="Auto,*,Auto,Auto">
|
||||
<Grid Grid.Row="1" Margin="32,24,32,20" RowDefinitions="Auto,*,Auto,Auto">
|
||||
|
||||
<!-- Library toolbar -->
|
||||
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,*,Auto,Auto,Auto,Auto" Margin="6,0,6,12">
|
||||
<TextBlock Grid.Column="0" Text="Library" FontSize="22" FontWeight="Bold" VerticalAlignment="Center" />
|
||||
<Border Grid.Column="1" Classes="pill" Margin="12,2,0,0" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="GameCountText" Text="0 games" FontSize="11" Foreground="{StaticResource MutedBrush}" />
|
||||
</Border>
|
||||
<TextBox Grid.Column="3" x:Name="SearchBox" Watermark="Search library…" Width="280"
|
||||
VerticalAlignment="Center" Margin="0,0,12,0" />
|
||||
<Button Grid.Column="4" x:Name="AddFolderButton" Classes="ghost" Content="+ Add folder"
|
||||
VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<Button Grid.Column="5" x:Name="RescanButton" Classes="ghost" Content="⟳ Rescan"
|
||||
VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<Button Grid.Column="6" x:Name="OpenFileButton" Classes="ghost" Content="Open file…"
|
||||
VerticalAlignment="Center" />
|
||||
<!-- Library / Options page switcher, with the library toolbar sharing
|
||||
the same row on the right. Plain buttons (not TabItem) so there is
|
||||
no underline; LB/RB hint chips flank the pair and the gamepad's
|
||||
shoulder buttons actually switch pages from anywhere. -->
|
||||
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto" Margin="0,0,0,20">
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="14" VerticalAlignment="Center">
|
||||
<Border Classes="padHint" VerticalAlignment="Center">
|
||||
<TextBlock Text="LB" FontSize="11" FontWeight="Bold" Foreground="{StaticResource MutedBrush}" />
|
||||
</Border>
|
||||
<Button x:Name="LibraryTabButton" Classes="segment active" Content="Library" />
|
||||
<Button x:Name="OptionsTabButton" Classes="segment" Content="Options" />
|
||||
<Border Classes="padHint" VerticalAlignment="Center">
|
||||
<TextBlock Text="RB" FontSize="11" FontWeight="Bold" Foreground="{StaticResource MutedBrush}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="2" x:Name="LibraryToolbar" Orientation="Horizontal" Spacing="8"
|
||||
VerticalAlignment="Center">
|
||||
<TextBox x:Name="SearchBox" Watermark="Search library…" Width="240" VerticalAlignment="Center" />
|
||||
<Button x:Name="AddFolderButton" Classes="ghost" Content="+ Add folder" VerticalAlignment="Center" />
|
||||
<Button x:Name="RescanButton" Classes="ghost" Content="⟳ Rescan" VerticalAlignment="Center" />
|
||||
<Button x:Name="OpenFileButton" Classes="ghost" Content="Open file…" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Cover-art grid -->
|
||||
<Panel Grid.Row="1">
|
||||
<ListBox x:Name="GameList" Classes="tileGrid" Background="Transparent"
|
||||
SelectionMode="Single" Padding="0">
|
||||
<ListBox.ContextMenu>
|
||||
<ContextMenu x:Name="GameContextMenu" Placement="Pointer">
|
||||
<MenuItem x:Name="CtxLaunch" Header="Launch" FontWeight="SemiBold">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="▶" FontSize="11" Foreground="{StaticResource AccentHoverBrush}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem x:Name="CtxOpenFolder" Header="Open game folder">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="📂" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem x:Name="CtxCopyPath" Header="Copy path">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem x:Name="CtxCopyTitleId" Header="Copy title ID">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem x:Name="CtxRemove" Header="Remove from library"
|
||||
Foreground="{StaticResource DangerHoverBrush}">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="✕" FontSize="12" Foreground="{StaticResource DangerHoverBrush}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
</ContextMenu>
|
||||
</ListBox.ContextMenu>
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel />
|
||||
</ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Width="128" Height="186" Spacing="7">
|
||||
<Border Classes="coverShadow" Width="128" Height="128">
|
||||
<Border Classes="coverClip">
|
||||
<Panel>
|
||||
<Border Background="{Binding PlaceholderBrush}" IsVisible="{Binding !HasCover}">
|
||||
<TextBlock Text="{Binding Initials}" FontSize="36" FontWeight="Bold"
|
||||
Foreground="#E8ECF4" Opacity="0.85"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<Image Source="{Binding Cover}" Stretch="UniformToFill" IsVisible="{Binding HasCover}" />
|
||||
</Panel>
|
||||
</Border>
|
||||
</Border>
|
||||
<StackPanel Spacing="2">
|
||||
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="SemiBold"
|
||||
TextWrapping="Wrap" MaxLines="2" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Text="{Binding Detail}" FontSize="11" Foreground="{StaticResource MutedBrush}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<!-- Empty state -->
|
||||
<StackPanel x:Name="EmptyState" Spacing="10" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
IsVisible="False">
|
||||
<TextBlock Text="🎮" FontSize="44" HorizontalAlignment="Center" Opacity="0.7" />
|
||||
<TextBlock x:Name="EmptyStateTitle" Text="Your library is empty" FontSize="18" FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center" />
|
||||
<TextBlock x:Name="EmptyStateHint" Text="Add a folder containing your games to get started."
|
||||
FontSize="13" Foreground="{StaticResource MutedBrush}" HorizontalAlignment="Center" />
|
||||
<Button x:Name="EmptyAddFolderButton" Classes="accent" Content="+ Add game folder"
|
||||
HorizontalAlignment="Center" Margin="0,8,0,0" />
|
||||
</StackPanel>
|
||||
<!-- Library page. The tile row gets extra top margin so it sits
|
||||
closer to eye level (PS5 home-screen style) instead of hugging
|
||||
the Library/Options switcher above it. -->
|
||||
<Panel x:Name="LibraryPage" Margin="0,64,0,0">
|
||||
<ListBox x:Name="GameList" Classes="tileGrid" Background="Transparent"
|
||||
SelectionMode="Single" Padding="0">
|
||||
<ListBox.ContextMenu>
|
||||
<ContextMenu x:Name="GameContextMenu" Placement="Pointer">
|
||||
<MenuItem x:Name="CtxLaunch" Header="Launch" FontWeight="SemiBold">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="▶" FontSize="11" Foreground="{StaticResource AccentHoverBrush}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem x:Name="CtxOpenFolder" Header="Open game folder">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="📂" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem x:Name="CtxCopyPath" Header="Copy path">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem x:Name="CtxCopyTitleId" Header="Copy title ID">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem x:Name="CtxRemove" Header="Remove from library"
|
||||
Foreground="{StaticResource DangerHoverBrush}">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="✕" FontSize="12" Foreground="{StaticResource DangerHoverBrush}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
</ContextMenu>
|
||||
</ListBox.ContextMenu>
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel />
|
||||
</ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Width="128" Height="172" Spacing="7">
|
||||
<Border Classes="coverShadow" Width="128" Height="128">
|
||||
<Border Classes="coverClip">
|
||||
<Panel>
|
||||
<Border Background="{Binding PlaceholderBrush}" IsVisible="{Binding !HasCover}">
|
||||
<TextBlock Text="{Binding Initials}" FontSize="36" FontWeight="Bold"
|
||||
Foreground="#E8ECF4" Opacity="0.85"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<Image Source="{Binding Cover}" Stretch="UniformToFill" IsVisible="{Binding HasCover}" />
|
||||
</Panel>
|
||||
</Border>
|
||||
</Border>
|
||||
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="SemiBold"
|
||||
TextWrapping="Wrap" MaxLines="2" TextTrimming="CharacterEllipsis"
|
||||
TextAlignment="Center" HorizontalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<!-- Empty state -->
|
||||
<StackPanel x:Name="EmptyState" Spacing="10" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
IsVisible="False">
|
||||
<TextBlock Text="🎮" FontSize="44" HorizontalAlignment="Center" Opacity="0.7" />
|
||||
<TextBlock x:Name="EmptyStateTitle" Text="Your library is empty" FontSize="18" FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center" />
|
||||
<TextBlock x:Name="EmptyStateHint" Text="Add a folder containing your games to get started."
|
||||
FontSize="13" Foreground="{StaticResource MutedBrush}" HorizontalAlignment="Center" />
|
||||
<Button x:Name="EmptyAddFolderButton" Classes="accent" Content="+ Add game folder"
|
||||
HorizontalAlignment="Center" Margin="0,8,0,0" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Loading state: covers the grid while a scan (initial load,
|
||||
rescan, or a just-added folder) is in flight, so the screen
|
||||
never looks blank mid-scan. -->
|
||||
<StackPanel x:Name="LoadingState" Spacing="14" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
IsVisible="False">
|
||||
<ProgressBar IsIndeterminate="True" Width="180" Height="3" />
|
||||
<TextBlock x:Name="LoadingStateText" Text="Loading library…" FontSize="13"
|
||||
Foreground="{StaticResource MutedBrush}" HorizontalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
|
||||
<!-- Options page: sub-tabs so future categories (Graphics, …) slot
|
||||
in next to General. Card-grouped sections match the visual
|
||||
language used by the console panel and launch bar below. -->
|
||||
<Grid x:Name="OptionsPage" IsVisible="False">
|
||||
<TabControl>
|
||||
<TabItem x:Name="GeneralTabItem" Header="General" FontSize="15">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
|
||||
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="14">
|
||||
<TextBlock x:Name="EmulationSectionTitle" Classes="sectionTitle" Text="EMULATION" />
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="CpuEngineLabel" Text="CPU engine" FontSize="13" />
|
||||
<TextBlock x:Name="CpuEngineDesc" Text="Execution engine used to run game code."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ComboBox Grid.Column="1" x:Name="CpuEngineBox" Width="160" SelectedIndex="0"
|
||||
VerticalAlignment="Center" CornerRadius="8">
|
||||
<ComboBoxItem x:Name="CpuEngineNativeItem" Content="Native" />
|
||||
</ComboBox>
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="StrictLabel" Text="Strict dynlib resolution" FontSize="13" />
|
||||
<TextBlock x:Name="StrictDesc" Text="Fail the launch when an imported symbol cannot be resolved."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="StrictToggle" OnContent="On" OffContent="Off"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="14">
|
||||
<TextBlock x:Name="LoggingSectionTitle" Classes="sectionTitle" Text="LOGGING" />
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="LogLevelLabel" Text="Log level" FontSize="13" />
|
||||
<TextBlock x:Name="LogLevelDesc" Text="Verbosity of the emulator console output."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ComboBox Grid.Column="1" x:Name="LogLevelBox" Width="160" SelectedIndex="2"
|
||||
VerticalAlignment="Center" CornerRadius="8">
|
||||
<ComboBoxItem x:Name="LogLevelTraceItem" Content="Trace" />
|
||||
<ComboBoxItem x:Name="LogLevelDebugItem" Content="Debug" />
|
||||
<ComboBoxItem x:Name="LogLevelInfoItem" Content="Info" />
|
||||
<ComboBoxItem x:Name="LogLevelWarningItem" Content="Warning" />
|
||||
<ComboBoxItem x:Name="LogLevelErrorItem" Content="Error" />
|
||||
<ComboBoxItem x:Name="LogLevelCriticalItem" Content="Critical" />
|
||||
</ComboBox>
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="TraceImportsLabel" Text="Import trace limit" FontSize="13" />
|
||||
<TextBlock x:Name="TraceImportsDesc" Text="Trace the first N imports per module (0 = off)."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<NumericUpDown Grid.Column="1" x:Name="TraceImportsBox" Width="160" Minimum="0"
|
||||
Maximum="4096" Increment="16" Value="0" FormatString="0"
|
||||
VerticalAlignment="Center" CornerRadius="8" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="LogToFileLabel" Text="Log to file" FontSize="13" />
|
||||
<TextBlock x:Name="LogToFileDesc" Text="Mirror emulator output to a log file."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="LogToFileToggle" OnContent="On" OffContent="Off"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="LogFilePathLabel" Text="Log file path" FontSize="13" />
|
||||
<TextBlock x:Name="LogFilePathText" Text="No custom path"
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" x:Name="SelectLogFilePathButton" Classes="ghost" Content="Select…"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="OverrideLogFileLabel" Text="Override log file" FontSize="13" />
|
||||
<TextBlock x:Name="OverrideLogFileDesc"
|
||||
Text="Use the exact file path instead of appending title ID and timestamp."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="OverrideLogFileToggle" OnContent="On" OffContent="Off"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="14">
|
||||
<TextBlock x:Name="LauncherSectionTitle" Classes="sectionTitle" Text="LAUNCHER" />
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="LanguageLabel" Text="Emulator language" FontSize="13" />
|
||||
<TextBlock x:Name="LanguageDesc"
|
||||
Text="Language used throughout the launcher. Applies immediately."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ComboBox Grid.Column="1" x:Name="LanguageBox" Width="160"
|
||||
VerticalAlignment="Center" CornerRadius="8"
|
||||
DisplayMemberBinding="{Binding NativeName}" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="TitleMusicLabel" Text="Title music" FontSize="13" />
|
||||
<TextBlock x:Name="TitleMusicDesc" Text="Loop the selected game's preview music in the library."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="TitleMusicToggle" OnContent="On" OffContent="Off"
|
||||
IsChecked="True" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="DiscordLabel" Text="Discord presence" FontSize="13" />
|
||||
<TextBlock x:Name="DiscordDesc" Text="Show the running game on your Discord profile."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="DiscordToggle" OnContent="On" OffContent="Off"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="14">
|
||||
<TextBlock x:Name="AboutSectionTitle"
|
||||
Classes="sectionTitle"
|
||||
Text="ABOUT" />
|
||||
|
||||
<!--Github-->
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8"
|
||||
VerticalAlignment="Center">
|
||||
<Image Source="avares://SharpEmu.GUI/Assets/github.png"
|
||||
Width="20"
|
||||
Height="20"
|
||||
VerticalAlignment="Center" />
|
||||
<StackPanel VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock x:Name="GithubLabel"
|
||||
Text="GitHub"
|
||||
FontSize="13" />
|
||||
<TextBlock x:Name="GithubDesc"
|
||||
Text="Source code, issues and project development."
|
||||
FontSize="11"
|
||||
Foreground="{StaticResource MutedBrush}"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<Button Grid.Column="1"
|
||||
x:Name="GithubButton"
|
||||
Classes="ghost"
|
||||
Content="Open"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<!--Discord-->
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8"
|
||||
VerticalAlignment="Center">
|
||||
<Image Source="avares://SharpEmu.GUI/Assets/discord.png"
|
||||
Width="20"
|
||||
Height="20"
|
||||
VerticalAlignment="Center" />
|
||||
<StackPanel VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock x:Name="DiscordServerLabel"
|
||||
Text="Discord"
|
||||
FontSize="13" />
|
||||
<TextBlock x:Name="DiscordServerDesc"
|
||||
Text="Join the community, get support and follow development."
|
||||
FontSize="11"
|
||||
Foreground="{StaticResource MutedBrush}"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<Button Grid.Column="1"
|
||||
x:Name="DiscordButton"
|
||||
Classes="ghost"
|
||||
Content="Join"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
<TabItem x:Name="EnvTabItem" Header="Environment" FontSize="15">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
|
||||
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="14">
|
||||
<TextBlock x:Name="EnvSectionTitle" Classes="sectionTitle" Text="ENVIRONMENT VARIABLES" />
|
||||
<TextBlock x:Name="EnvDesc"
|
||||
Text="Switches passed to the emulator as environment variables at launch."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock Text="SHARPEMU_BTHID_UNAVAILABLE" FontSize="13" FontFamily="Consolas,monospace" />
|
||||
<TextBlock x:Name="EnvBthidDesc"
|
||||
Text="Report Bluetooth HID as unavailable for titles whose wheel/FFB middleware polls forever. Leave off normally. Some titles freeze when init fails."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="EnvBthidToggle" OnContent="On" OffContent="Off"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock Text="SHARPEMU_DISABLE_IMPORT_LOOP_GUARD" FontSize="13" FontFamily="Consolas,monospace" />
|
||||
<TextBlock x:Name="EnvLoopGuardDesc"
|
||||
Text="Do not force quit titles that repeat the same call for too long. Try this when a game exits on its own while loading."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="EnvLoopGuardToggle" OnContent="On" OffContent="Off"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock Text="SHARPEMU_VK_VALIDATION" FontSize="13" FontFamily="Consolas,monospace" />
|
||||
<TextBlock x:Name="EnvVkValidationDesc"
|
||||
Text="Enable Vulkan validation layers for GPU debugging. Slow. Requires the Vulkan SDK to be installed."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="EnvVkValidationToggle" OnContent="On" OffContent="Off"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock Text="SHARPEMU_DUMP_SPIRV" FontSize="13" FontFamily="Consolas,monospace" />
|
||||
<TextBlock x:Name="EnvDumpSpirvDesc"
|
||||
Text="Dump AGC shaders and their SPIR-V translations to the shader-dumps folder. Use when reporting shader or rendering bugs."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="EnvDumpSpirvToggle" OnContent="On" OffContent="Off"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock Text="SHARPEMU_LOG_DIRECT_MEMORY" FontSize="13" FontFamily="Consolas,monospace" />
|
||||
<TextBlock x:Name="EnvLogDirectMemoryDesc"
|
||||
Text="Log direct memory allocations and failures to the console. Use when a game aborts or exits during boot."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="EnvLogDirectMemoryToggle" OnContent="On" OffContent="Off"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock Text="SHARPEMU_LOG_NP" FontSize="13" FontFamily="Consolas,monospace" />
|
||||
<TextBlock x:Name="EnvLogNpDesc"
|
||||
Text="Log NP (PlayStation Network) library calls to the console."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="EnvLogNpToggle" OnContent="On" OffContent="Off"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</Grid>
|
||||
</Panel>
|
||||
|
||||
<!-- Console (collapsible) -->
|
||||
<Border Grid.Row="2" x:Name="ConsolePanel" Classes="card" Padding="0" Height="240"
|
||||
Margin="0,12,0,0" IsVisible="False">
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto" Margin="16,12,16,8">
|
||||
<TextBlock Classes="sectionTitle" Text="CONSOLE" VerticalAlignment="Center" />
|
||||
<CheckBox Grid.Column="1" x:Name="AutoScrollCheck" Content="Auto-scroll" IsChecked="True"
|
||||
FontSize="12" Margin="0,0,12,0" />
|
||||
<Button Grid.Column="2" x:Name="CopyLogButton" Classes="ghost" Content="Copy" FontSize="12"
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto,Auto,Auto" Margin="16,12,16,8">
|
||||
<TextBlock x:Name="ConsoleSectionTitle" Classes="sectionTitle" Text="CONSOLE" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Column="1" FontSize="12" Margin="0,0,12,0" x:Name="ConsoleSearchBox"
|
||||
Watermark="Search..." Width="320" />
|
||||
<CheckBox Grid.Column="2" x:Name="AutoScrollCheck" Content="Auto-scroll" IsChecked="True"
|
||||
FontSize="12" Margin="0,0,12,0" />
|
||||
<Button Grid.Column="3" x:Name="DetachConsoleButton" Classes="ghost" Content="Split" FontSize="12"
|
||||
Padding="10,4" Margin="0,0,8,0" />
|
||||
<Button Grid.Column="3" x:Name="ClearLogButton" Classes="ghost" Content="Clear" FontSize="12"
|
||||
<Button Grid.Column="4" x:Name="CopyLogButton" Classes="ghost" Content="Copy" FontSize="12"
|
||||
Padding="10,4" Margin="0,0,8,0" />
|
||||
<Button Grid.Column="5" x:Name="ClearLogButton" Classes="ghost" Content="Clear" FontSize="12"
|
||||
Padding="10,4" />
|
||||
</Grid>
|
||||
<ListBox Grid.Row="1" x:Name="ConsoleList" Classes="console" BorderThickness="0,1,0,0"
|
||||
@@ -179,8 +501,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Launch bar -->
|
||||
<Border Grid.Row="3" Classes="card" Margin="0,12,0,0" Padding="14">
|
||||
<!-- Launch bar; capped so it does not sprawl on a maximized window. -->
|
||||
<Border Grid.Row="3" Classes="card" Margin="0,12,0,0" Padding="14" MaxWidth="1280">
|
||||
<StackPanel Spacing="14">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
|
||||
@@ -199,9 +521,32 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
</Panel>
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Column="1" Spacing="3" VerticalAlignment="Center" Margin="14,0,14,0">
|
||||
<TextBlock x:Name="SelectedGameTitle" Text="No game selected" FontSize="16" FontWeight="Bold"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<StackPanel Grid.Column="1" Spacing="4" VerticalAlignment="Center" Margin="14,0,14,0">
|
||||
<Grid ColumnDefinitions="Auto,Auto,*">
|
||||
<TextBlock Grid.Column="0" x:Name="SelectedGameTitle" Text="No game selected" FontSize="16"
|
||||
FontWeight="Bold" TextTrimming="CharacterEllipsis" VerticalAlignment="Center"
|
||||
MaxWidth="340" Margin="0,0,10,0" />
|
||||
|
||||
<!-- Title id / version / size badges, right next to the
|
||||
title. The title's own MaxWidth (not a "*" column) is
|
||||
what keeps them from drifting to the far right. -->
|
||||
<StackPanel Grid.Column="1" x:Name="SelectedBadgesRow" Orientation="Horizontal" Spacing="6"
|
||||
IsVisible="False" VerticalAlignment="Center">
|
||||
<Border Classes="pill" IsVisible="{Binding HasTitleId, FallbackValue=False}">
|
||||
<TextBlock Text="{Binding TitleId}" FontSize="10" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource MutedBrush}" />
|
||||
</Border>
|
||||
<Border Classes="pill" IsVisible="{Binding HasVersion, FallbackValue=False}">
|
||||
<TextBlock Text="{Binding VersionText}" FontSize="10" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource MutedBrush}" />
|
||||
</Border>
|
||||
<Border Classes="pill">
|
||||
<TextBlock Text="{Binding SizeText, FallbackValue=''}" FontSize="10" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource MutedBrush}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<TextBlock x:Name="SelectedGamePath" Text="Pick a game from the library, or open an eboot.bin directly."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextTrimming="CharacterEllipsis" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="7">
|
||||
@@ -213,56 +558,18 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||
<ToggleButton x:Name="OptionsToggle" Classes="ghost" Content="⚙ Options" />
|
||||
<ToggleButton x:Name="ConsoleToggle" Classes="ghost" Content="≡ Console" />
|
||||
<Button x:Name="LaunchButton" Classes="accent" Content="▶ Launch" IsEnabled="False" />
|
||||
<Button x:Name="StopButton" Classes="danger" Content="■ Stop" IsEnabled="False" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Launch options (collapsible) -->
|
||||
<WrapPanel x:Name="OptionsPanel" IsVisible="False">
|
||||
<StackPanel Margin="0,0,24,0">
|
||||
<TextBlock Classes="fieldLabel" Text="CPU engine" />
|
||||
<ComboBox x:Name="CpuEngineBox" Width="150" SelectedIndex="0" CornerRadius="8">
|
||||
<ComboBoxItem Content="Native" />
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,24,0">
|
||||
<TextBlock Classes="fieldLabel" Text="Log level" />
|
||||
<ComboBox x:Name="LogLevelBox" Width="150" SelectedIndex="2" CornerRadius="8">
|
||||
<ComboBoxItem Content="Trace" />
|
||||
<ComboBoxItem Content="Debug" />
|
||||
<ComboBoxItem Content="Info" />
|
||||
<ComboBoxItem Content="Warning" />
|
||||
<ComboBoxItem Content="Error" />
|
||||
<ComboBoxItem Content="Critical" />
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,24,0">
|
||||
<TextBlock Classes="fieldLabel" Text="Import trace limit (0 = off)" />
|
||||
<NumericUpDown x:Name="TraceImportsBox" Width="170" Minimum="0" Maximum="4096" Increment="16"
|
||||
Value="0" FormatString="0" CornerRadius="8" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,24,0">
|
||||
<TextBlock Classes="fieldLabel" Text="Strict dynlib resolution" />
|
||||
<ToggleSwitch x:Name="StrictToggle" OnContent="On" OffContent="Off" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,24,0">
|
||||
<TextBlock Classes="fieldLabel" Text="Log to file" />
|
||||
<ToggleSwitch x:Name="LogToFileToggle" OnContent="On" OffContent="Off" />
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Classes="fieldLabel" Text="Title music" />
|
||||
<ToggleSwitch x:Name="TitleMusicToggle" OnContent="On" OffContent="Off" IsChecked="True" />
|
||||
</StackPanel>
|
||||
</WrapPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Status bar -->
|
||||
<Grid Grid.Row="2" Background="{StaticResource ChromeBrush}" ColumnDefinitions="*,Auto">
|
||||
<Grid x:Name="StatusBar" Grid.Row="2" Height="32" Background="{StaticResource ChromeBrush}"
|
||||
ColumnDefinitions="*,Auto">
|
||||
<TextBlock x:Name="EmulatorPathText" Grid.Column="0" Text="Emulator: locating…" FontSize="11"
|
||||
Foreground="{StaticResource FaintBrush}" VerticalAlignment="Center" Margin="16,0"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -28,8 +28,18 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
|
||||
<AvaloniaResource Include="..\..\assets\images\github.png" Link="Assets/github.png" />
|
||||
<AvaloniaResource Include="..\..\assets\images\discord.png" Link="Assets/discord.png" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Languages\*.json">
|
||||
<LogicalName>Languages.%(Filename)%(Extension)</LogicalName>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
|
||||
<!-- The controller readers (DualSense raw HID + Xbox XInput) are shared
|
||||
with the emulator's pad HLE. They are dependency-free, so they are
|
||||
compiled in directly rather than pulling a reference to all of
|
||||
|
||||
@@ -47,7 +47,7 @@ internal sealed class SndPreviewPlayer
|
||||
{
|
||||
// Debounce so skimming through the library does not decode (or
|
||||
// start) a preview per tile.
|
||||
await Task.Delay(300).ConfigureAwait(false);
|
||||
await Task.Delay(120).ConfigureAwait(false);
|
||||
|
||||
byte[]? wav;
|
||||
lock (_sync)
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Linq;
|
||||
using SharpEmu.Logging;
|
||||
|
||||
namespace SharpEmu.HLE;
|
||||
|
||||
public sealed class Aerolib : ISymbolCatalog
|
||||
{
|
||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Aerolib");
|
||||
private static readonly Lazy<Aerolib> _instance = new(() => new Aerolib());
|
||||
private static readonly Aerolib EmptyCatalog = new Aerolib(empty: true);
|
||||
|
||||
@@ -108,14 +110,14 @@ public sealed class Aerolib : ISymbolCatalog
|
||||
|
||||
if (resourceName == null)
|
||||
{
|
||||
Console.Error.WriteLine("[AEROLIB] Embedded resource 'aerolib.bin' not found");
|
||||
Log.Error("Embedded resource 'aerolib.bin' not found");
|
||||
return;
|
||||
}
|
||||
|
||||
using var stream = assembly.GetManifestResourceStream(resourceName);
|
||||
if (stream == null)
|
||||
{
|
||||
Console.Error.WriteLine("[AEROLIB] Failed to open embedded resource stream");
|
||||
Log.Error("Failed to open embedded resource stream");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -145,11 +147,11 @@ public sealed class Aerolib : ISymbolCatalog
|
||||
_byExportName[name] = symbol;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[AEROLIB] Loaded {_byNid.Count} NID entries from binary resource");
|
||||
Log.Info($"Loaded {_byNid.Count} NID entries from binary resource");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[AEROLIB] Failed to load embedded aerolib.bin: {ex.Message}");
|
||||
Log.Error($"Failed to load embedded aerolib.bin: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,13 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.HLE;
|
||||
|
||||
[Flags]
|
||||
public enum GuestPageProtection
|
||||
{
|
||||
None = 0,
|
||||
Read = 1,
|
||||
Write = 2,
|
||||
Execute = 4,
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.HLE.Host;
|
||||
|
||||
/// <summary>
|
||||
/// General-purpose register snapshot of a suspended thread, produced by
|
||||
/// <see cref="IHostThreading.TryCaptureThreadRegisters"/>. Registers are named
|
||||
/// after the guest ISA (x86-64), which every supported host executes natively.
|
||||
/// </summary>
|
||||
public readonly record struct HostCapturedRegisters(
|
||||
ulong Rip,
|
||||
ulong Rsp,
|
||||
ulong Rbp,
|
||||
ulong Rax,
|
||||
ulong Rbx,
|
||||
ulong Rcx,
|
||||
ulong Rdx);
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.HLE.Host;
|
||||
|
||||
/// <summary>
|
||||
/// Platform-neutral page protection. Values intentionally enumerate the exact
|
||||
/// combinations the emulator uses today so each maps 1:1 onto a single native
|
||||
/// protection constant (PAGE_* on Windows, PROT_* elsewhere).
|
||||
/// </summary>
|
||||
public enum HostPageProtection
|
||||
{
|
||||
NoAccess,
|
||||
ReadOnly,
|
||||
ReadWrite,
|
||||
Execute,
|
||||
ReadExecute,
|
||||
ReadWriteExecute,
|
||||
ExecuteWriteCopy,
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.HLE.Host.Windows;
|
||||
|
||||
namespace SharpEmu.HLE.Host;
|
||||
|
||||
/// <summary>
|
||||
/// Process-wide access point for the host platform backend. Static HLE export
|
||||
/// classes (which cannot receive constructor injection) resolve host primitives
|
||||
/// through <see cref="Current"/>; injectable components should instead accept an
|
||||
/// <see cref="IHostPlatform"/> and merely default to this.
|
||||
/// </summary>
|
||||
public static class HostPlatform
|
||||
{
|
||||
private static readonly Lazy<IHostPlatform> Instance = new(Create);
|
||||
|
||||
public static IHostPlatform Current => Instance.Value;
|
||||
|
||||
private static IHostPlatform Create()
|
||||
{
|
||||
// The Windows backend executes guest x86-64 natively and emits x86-64
|
||||
// stubs, so a native ARM64 process must be rejected here rather than
|
||||
// crash undefined later (x64 processes under emulation report X64).
|
||||
if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.X64)
|
||||
{
|
||||
return new WindowsHostPlatform();
|
||||
}
|
||||
|
||||
throw new PlatformNotSupportedException(
|
||||
"SharpEmu native guest execution requires a host platform backend and none exists for this OS/architecture yet (currently Windows x64 only).");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.HLE.Host;
|
||||
|
||||
/// <summary>
|
||||
/// Result of <see cref="IHostMemory.Query"/>. The Raw* fields carry the
|
||||
/// untranslated OS values so call sites migrated from direct VirtualQuery use
|
||||
/// keep comparing (and logging) the exact native words they did before;
|
||||
/// <see cref="State"/> and <see cref="Protection"/> are neutral views.
|
||||
/// </summary>
|
||||
public readonly record struct HostRegionInfo(
|
||||
ulong BaseAddress,
|
||||
ulong AllocationBase,
|
||||
ulong RegionSize,
|
||||
HostRegionState State,
|
||||
uint RawState,
|
||||
HostPageProtection Protection,
|
||||
uint RawProtection,
|
||||
uint RawAllocationProtection);
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.HLE.Host;
|
||||
|
||||
public enum HostRegionState
|
||||
{
|
||||
Free,
|
||||
Reserved,
|
||||
Committed,
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.HLE.Host;
|
||||
|
||||
/// <summary>
|
||||
/// Host functions whose addresses the execution engine bakes into emitted
|
||||
/// stubs (spin-waits, worker run loops, TLS reads). Enum-keyed rather than a
|
||||
/// free-form name lookup: each platform's emitters need their own specific
|
||||
/// functions, and this set is exactly what the current emitters consume.
|
||||
/// </summary>
|
||||
public enum HostRuntimeFunction
|
||||
{
|
||||
TlsGetValue,
|
||||
QueryPerformanceCounter,
|
||||
SwitchToThread,
|
||||
Sleep,
|
||||
WaitForSingleObject,
|
||||
SetEvent,
|
||||
ExitThread,
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.HLE.Host;
|
||||
|
||||
/// <summary>
|
||||
/// Installation mechanics for the process-wide fault interception the execution
|
||||
/// engine relies on to catch guest faults. Deliberately thin: the managed
|
||||
/// handlers keep receiving the platform's raw exception data, and the emitted
|
||||
/// pre-filter thunk is an opaque per-platform unit. Implementations live next
|
||||
/// to the execution backend (SharpEmu.Core), not behind HostPlatform.Current.
|
||||
/// </summary>
|
||||
public interface IHostFaultHandling
|
||||
{
|
||||
/// <summary>
|
||||
/// Emits the native thunk that wraps a managed fault handler: it pre-filters
|
||||
/// exception codes that must never enter managed code and, when the fault
|
||||
/// happened on a guest stack, switches to the host stack saved in
|
||||
/// <paramref name="hostRspSwitchTlsSlot"/> before the call. Returns 0 on failure.
|
||||
/// </summary>
|
||||
nint CreateHandlerThunk(nint managedCallback, uint hostRspSwitchTlsSlot, nint tlsGetValueAddress);
|
||||
|
||||
void FreeThunk(nint thunk);
|
||||
|
||||
/// <summary>Installs a first-chance handler ahead of existing ones; returns a removal handle (0 on failure).</summary>
|
||||
nint AddFirstChanceHandler(nint thunk);
|
||||
|
||||
void RemoveHandler(nint handle);
|
||||
|
||||
/// <summary>Installs the last-resort filter; pass 0 to clear.</summary>
|
||||
void SetUnhandledFilter(nint thunk);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.HLE.Host;
|
||||
|
||||
/// <summary>
|
||||
/// Host page-allocation primitives used by the native execution engine.
|
||||
/// Allocate/Reserve/Commit are deliberately separate members (rather than a
|
||||
/// flags parameter) so every call site maps 1:1 onto the exact native call it
|
||||
/// replaced, keeping the Windows behavior byte-for-byte identical.
|
||||
/// </summary>
|
||||
public interface IHostMemory
|
||||
{
|
||||
/// <summary>
|
||||
/// Reserves and commits pages in one step. <paramref name="desiredAddress"/> of 0
|
||||
/// lets the OS choose the address. Returns the base address, or 0 on failure.
|
||||
/// The OS may satisfy the request at a different address than desired; callers
|
||||
/// that require an exact placement must check the result themselves.
|
||||
/// </summary>
|
||||
ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection);
|
||||
|
||||
/// <summary>Reserves address space without committing pages (lazy regions).</summary>
|
||||
ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection);
|
||||
|
||||
/// <summary>Commits pages inside a previously reserved range (fault-path lazy commit).</summary>
|
||||
bool Commit(ulong address, ulong size, HostPageProtection protection);
|
||||
|
||||
/// <summary>Releases an entire allocation or reservation by its base address.</summary>
|
||||
bool Free(ulong address);
|
||||
|
||||
/// <summary>
|
||||
/// Changes protection on committed pages. <paramref name="rawOldProtection"/> is the
|
||||
/// untranslated previous OS protection value (see <see cref="HostRegionInfo.RawProtection"/>).
|
||||
/// </summary>
|
||||
bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection);
|
||||
|
||||
/// <summary>
|
||||
/// Restores a raw protection value previously returned by <see cref="Protect"/> or
|
||||
/// <see cref="Query"/> on this same platform. Raw values are opaque to callers and
|
||||
/// must never cross platforms; this exists so save/restore protection sequences
|
||||
/// round-trip OS-specific modifier bits the neutral enum cannot represent.
|
||||
/// </summary>
|
||||
bool ProtectRaw(ulong address, ulong size, uint rawProtection, out uint rawOldProtection);
|
||||
|
||||
bool Query(ulong address, out HostRegionInfo info);
|
||||
|
||||
void FlushInstructionCache(ulong address, ulong size);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.HLE.Host;
|
||||
|
||||
/// <summary>
|
||||
/// Aggregates the host-OS primitives the native execution engine depends on.
|
||||
/// Each supported platform provides one implementation; consumers reach the
|
||||
/// process-wide instance through <see cref="HostPlatform.Current"/> or accept
|
||||
/// one by injection.
|
||||
/// </summary>
|
||||
public interface IHostPlatform
|
||||
{
|
||||
IHostMemory Memory { get; }
|
||||
|
||||
IHostThreading Threading { get; }
|
||||
|
||||
IHostSymbolResolver Symbols { get; }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.HLE.Host;
|
||||
|
||||
public interface IHostSymbolResolver
|
||||
{
|
||||
/// <summary>Returns the native address of the function, or 0 if unavailable.</summary>
|
||||
nint GetAddress(HostRuntimeFunction function);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.HLE.Host;
|
||||
|
||||
/// <summary>
|
||||
/// Raw host thread and native-TLS primitives for the execution engine. Guest
|
||||
/// code must run on threads the CLR did not create (no managed frames below
|
||||
/// guest frames), so thread creation takes a native entry point and is not
|
||||
/// expressible with managed threads.
|
||||
/// </summary>
|
||||
public interface IHostThreading
|
||||
{
|
||||
/// <summary>Allocates a native TLS slot; returns <see cref="uint.MaxValue"/> on failure.</summary>
|
||||
uint AllocateTlsSlot();
|
||||
|
||||
bool FreeTlsSlot(uint slot);
|
||||
|
||||
bool SetTlsValue(uint slot, nint value);
|
||||
|
||||
nint GetTlsValue(uint slot);
|
||||
|
||||
uint CurrentThreadId { get; }
|
||||
|
||||
bool TrySetCurrentThreadAffinity(nuint affinityMask);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a raw OS thread executing native code at <paramref name="entry"/> with
|
||||
/// <paramref name="stackReserveBytes"/> of reserved (not committed) stack.
|
||||
/// Returns the thread handle, or 0 on failure.
|
||||
/// </summary>
|
||||
nint CreateNativeThread(nint entry, nint parameter, nuint stackReserveBytes, out uint threadId);
|
||||
|
||||
/// <summary>Waits for the thread to exit; true when it did within the timeout.</summary>
|
||||
bool WaitForThreadExit(nint threadHandle, uint timeoutMilliseconds);
|
||||
|
||||
void CloseThreadHandle(nint threadHandle);
|
||||
|
||||
/// <summary>
|
||||
/// Suspends the thread, snapshots its general-purpose registers, and resumes it —
|
||||
/// one indivisible operation (diagnostics only). The caller must not pass the
|
||||
/// current thread. Returns false if the thread cannot be opened or suspended.
|
||||
/// </summary>
|
||||
bool TryCaptureThreadRegisters(uint threadId, out HostCapturedRegisters registers);
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SharpEmu.HLE.Host.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Windows implementation over VirtualAlloc/VirtualFree/VirtualProtect/VirtualQuery.
|
||||
/// Sealed so the JIT can devirtualize interface calls on fault-handling hot paths.
|
||||
/// </summary>
|
||||
internal sealed unsafe partial class WindowsHostMemory : IHostMemory
|
||||
{
|
||||
private const uint MEM_COMMIT = 0x1000;
|
||||
private const uint MEM_RESERVE = 0x2000;
|
||||
private const uint MEM_RELEASE = 0x8000;
|
||||
private const uint MEM_FREE = 0x10000;
|
||||
|
||||
private const uint PAGE_NOACCESS = 0x01;
|
||||
private const uint PAGE_READONLY = 0x02;
|
||||
private const uint PAGE_READWRITE = 0x04;
|
||||
private const uint PAGE_WRITECOPY = 0x08;
|
||||
private const uint PAGE_EXECUTE = 0x10;
|
||||
private const uint PAGE_EXECUTE_READ = 0x20;
|
||||
private const uint PAGE_EXECUTE_READWRITE = 0x40;
|
||||
private const uint PAGE_EXECUTE_WRITECOPY = 0x80;
|
||||
|
||||
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection)
|
||||
{
|
||||
return (ulong)VirtualAlloc((void*)desiredAddress, (nuint)size, MEM_COMMIT | MEM_RESERVE, ToNativeProtection(protection));
|
||||
}
|
||||
|
||||
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection)
|
||||
{
|
||||
return (ulong)VirtualAlloc((void*)desiredAddress, (nuint)size, MEM_RESERVE, ToNativeProtection(protection));
|
||||
}
|
||||
|
||||
public bool Commit(ulong address, ulong size, HostPageProtection protection)
|
||||
{
|
||||
return VirtualAlloc((void*)address, (nuint)size, MEM_COMMIT, ToNativeProtection(protection)) != null;
|
||||
}
|
||||
|
||||
public bool Free(ulong address)
|
||||
{
|
||||
return VirtualFree((void*)address, 0, MEM_RELEASE);
|
||||
}
|
||||
|
||||
public bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection)
|
||||
{
|
||||
return VirtualProtect((void*)address, (nuint)size, ToNativeProtection(protection), out rawOldProtection);
|
||||
}
|
||||
|
||||
public bool ProtectRaw(ulong address, ulong size, uint rawProtection, out uint rawOldProtection)
|
||||
{
|
||||
return VirtualProtect((void*)address, (nuint)size, rawProtection, out rawOldProtection);
|
||||
}
|
||||
|
||||
public bool Query(ulong address, out HostRegionInfo info)
|
||||
{
|
||||
if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MemoryBasicInformation64)) == 0)
|
||||
{
|
||||
info = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
info = new HostRegionInfo(
|
||||
mbi.BaseAddress,
|
||||
mbi.AllocationBase,
|
||||
mbi.RegionSize,
|
||||
ToRegionState(mbi.State),
|
||||
mbi.State,
|
||||
ToHostProtection(mbi.Protect),
|
||||
mbi.Protect,
|
||||
mbi.AllocationProtect);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void FlushInstructionCache(ulong address, ulong size)
|
||||
{
|
||||
FlushInstructionCache(GetCurrentProcess(), (void*)address, (nuint)size);
|
||||
}
|
||||
|
||||
private static uint ToNativeProtection(HostPageProtection protection) => protection switch
|
||||
{
|
||||
HostPageProtection.NoAccess => PAGE_NOACCESS,
|
||||
HostPageProtection.ReadOnly => PAGE_READONLY,
|
||||
HostPageProtection.ReadWrite => PAGE_READWRITE,
|
||||
HostPageProtection.Execute => PAGE_EXECUTE,
|
||||
HostPageProtection.ReadExecute => PAGE_EXECUTE_READ,
|
||||
HostPageProtection.ReadWriteExecute => PAGE_EXECUTE_READWRITE,
|
||||
HostPageProtection.ExecuteWriteCopy => PAGE_EXECUTE_WRITECOPY,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(protection), protection, null),
|
||||
};
|
||||
|
||||
private static HostRegionState ToRegionState(uint state) => state switch
|
||||
{
|
||||
MEM_COMMIT => HostRegionState.Committed,
|
||||
MEM_RESERVE => HostRegionState.Reserved,
|
||||
MEM_FREE => HostRegionState.Free,
|
||||
_ => HostRegionState.Free,
|
||||
};
|
||||
|
||||
private static HostPageProtection ToHostProtection(uint rawProtection)
|
||||
{
|
||||
// Strip PAGE_GUARD/PAGE_NOCACHE/PAGE_WRITECOMBINE modifiers; callers needing
|
||||
// them compare HostRegionInfo.RawProtection directly.
|
||||
return (rawProtection & 0xFF) switch
|
||||
{
|
||||
PAGE_READONLY => HostPageProtection.ReadOnly,
|
||||
PAGE_READWRITE => HostPageProtection.ReadWrite,
|
||||
PAGE_WRITECOPY => HostPageProtection.ReadWrite,
|
||||
PAGE_EXECUTE => HostPageProtection.Execute,
|
||||
PAGE_EXECUTE_READ => HostPageProtection.ReadExecute,
|
||||
PAGE_EXECUTE_READWRITE => HostPageProtection.ReadWriteExecute,
|
||||
PAGE_EXECUTE_WRITECOPY => HostPageProtection.ExecuteWriteCopy,
|
||||
_ => HostPageProtection.NoAccess,
|
||||
};
|
||||
}
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
private static partial void* VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool VirtualFree(void* lpAddress, nuint dwSize, uint dwFreeType);
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool VirtualProtect(void* lpAddress, nuint dwSize, uint flNewProtect, out uint lpflOldProtect);
|
||||
|
||||
[LibraryImport("kernel32.dll")]
|
||||
private static partial nuint VirtualQuery(void* lpAddress, out MemoryBasicInformation64 lpBuffer, nuint dwLength);
|
||||
|
||||
[LibraryImport("kernel32.dll")]
|
||||
private static partial void* GetCurrentProcess();
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
|
||||
|
||||
private struct MemoryBasicInformation64
|
||||
{
|
||||
public ulong BaseAddress;
|
||||
public ulong AllocationBase;
|
||||
public uint AllocationProtect;
|
||||
public uint Alignment1;
|
||||
public ulong RegionSize;
|
||||
public uint State;
|
||||
public uint Protect;
|
||||
public uint Type;
|
||||
public uint Alignment2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.HLE.Host.Windows;
|
||||
|
||||
internal sealed class WindowsHostPlatform : IHostPlatform
|
||||
{
|
||||
public IHostMemory Memory { get; } = new WindowsHostMemory();
|
||||
|
||||
public IHostThreading Threading { get; } = new WindowsHostThreading();
|
||||
|
||||
public IHostSymbolResolver Symbols { get; } = new WindowsHostSymbolResolver();
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SharpEmu.HLE.Host.Windows;
|
||||
|
||||
internal sealed partial class WindowsHostSymbolResolver : IHostSymbolResolver
|
||||
{
|
||||
public nint GetAddress(HostRuntimeFunction function)
|
||||
{
|
||||
var kernel32 = GetModuleHandle("kernel32.dll");
|
||||
if (kernel32 == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return GetProcAddress(kernel32, function switch
|
||||
{
|
||||
HostRuntimeFunction.TlsGetValue => "TlsGetValue",
|
||||
HostRuntimeFunction.QueryPerformanceCounter => "QueryPerformanceCounter",
|
||||
HostRuntimeFunction.SwitchToThread => "SwitchToThread",
|
||||
HostRuntimeFunction.Sleep => "Sleep",
|
||||
HostRuntimeFunction.WaitForSingleObject => "WaitForSingleObject",
|
||||
HostRuntimeFunction.SetEvent => "SetEvent",
|
||||
HostRuntimeFunction.ExitThread => "ExitThread",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(function), function, null),
|
||||
});
|
||||
}
|
||||
|
||||
// Utf16 marshalling pins the managed string and passes its address directly
|
||||
// (no copy); Utf8 stack-allocates the transient buffer for these short
|
||||
// ASCII export names. LibraryImport is exact-spelling, hence the W entry point.
|
||||
[LibraryImport("kernel32.dll", EntryPoint = "GetModuleHandleW", StringMarshalling = StringMarshalling.Utf16)]
|
||||
private static partial nint GetModuleHandle(string lpModuleName);
|
||||
|
||||
[LibraryImport("kernel32.dll", StringMarshalling = StringMarshalling.Utf8)]
|
||||
private static partial nint GetProcAddress(nint hModule, string procName);
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SharpEmu.HLE.Host.Windows;
|
||||
|
||||
internal sealed unsafe partial class WindowsHostThreading : IHostThreading
|
||||
{
|
||||
private const uint StackSizeParamIsAReservation = 0x00010000u;
|
||||
private const uint ThreadGetContext = 0x0008u;
|
||||
private const uint ThreadSuspendResume = 0x0002u;
|
||||
|
||||
// Win64 CONTEXT layout (CONTROL | INTEGER only — no XMM state is requested).
|
||||
private const int Win64ContextSize = 0x4D0;
|
||||
private const int Win64ContextFlagsOffset = 0x30;
|
||||
private const uint ContextAmd64ControlInteger = 0x00100003u;
|
||||
private const int CtxRax = 120;
|
||||
private const int CtxRcx = 128;
|
||||
private const int CtxRdx = 136;
|
||||
private const int CtxRbx = 144;
|
||||
private const int CtxRsp = 152;
|
||||
private const int CtxRbp = 160;
|
||||
private const int CtxRip = 248;
|
||||
|
||||
public uint AllocateTlsSlot() => TlsAlloc();
|
||||
|
||||
public bool FreeTlsSlot(uint slot) => TlsFree(slot);
|
||||
|
||||
public bool SetTlsValue(uint slot, nint value) => TlsSetValue(slot, value);
|
||||
|
||||
public nint GetTlsValue(uint slot) => TlsGetValue(slot);
|
||||
|
||||
public uint CurrentThreadId => GetCurrentThreadId();
|
||||
|
||||
public bool TrySetCurrentThreadAffinity(nuint affinityMask)
|
||||
{
|
||||
return SetThreadAffinityMask(GetCurrentThread(), affinityMask) != 0;
|
||||
}
|
||||
|
||||
public nint CreateNativeThread(nint entry, nint parameter, nuint stackReserveBytes, out uint threadId)
|
||||
{
|
||||
return CreateThread(0, stackReserveBytes, entry, parameter, StackSizeParamIsAReservation, out threadId);
|
||||
}
|
||||
|
||||
public bool WaitForThreadExit(nint threadHandle, uint timeoutMilliseconds)
|
||||
{
|
||||
return WaitForSingleObject(threadHandle, timeoutMilliseconds) == 0u;
|
||||
}
|
||||
|
||||
public void CloseThreadHandle(nint threadHandle)
|
||||
{
|
||||
_ = CloseHandle(threadHandle);
|
||||
}
|
||||
|
||||
public bool TryCaptureThreadRegisters(uint threadId, out HostCapturedRegisters registers)
|
||||
{
|
||||
registers = default;
|
||||
var threadHandle = OpenThread(ThreadGetContext | ThreadSuspendResume, false, threadId);
|
||||
if (threadHandle == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void* contextRecord = null;
|
||||
var suspended = false;
|
||||
try
|
||||
{
|
||||
if (SuspendThread(threadHandle) == uint.MaxValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
suspended = true;
|
||||
// CONTEXT requires 16-byte alignment (it embeds M128A fields);
|
||||
// NativeMemory.AllocZeroed guarantees max_align_t, stackalloc only
|
||||
// pointer-size — so this stays a native allocation.
|
||||
contextRecord = NativeMemory.AllocZeroed((nuint)Win64ContextSize);
|
||||
*(uint*)((byte*)contextRecord + Win64ContextFlagsOffset) = ContextAmd64ControlInteger;
|
||||
if (!GetThreadContext(threadHandle, contextRecord))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
registers = new HostCapturedRegisters(
|
||||
ReadU64(contextRecord, CtxRip),
|
||||
ReadU64(contextRecord, CtxRsp),
|
||||
ReadU64(contextRecord, CtxRbp),
|
||||
ReadU64(contextRecord, CtxRax),
|
||||
ReadU64(contextRecord, CtxRbx),
|
||||
ReadU64(contextRecord, CtxRcx),
|
||||
ReadU64(contextRecord, CtxRdx));
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (contextRecord != null)
|
||||
{
|
||||
NativeMemory.Free(contextRecord);
|
||||
}
|
||||
if (suspended)
|
||||
{
|
||||
_ = ResumeThread(threadHandle);
|
||||
}
|
||||
_ = CloseHandle(threadHandle);
|
||||
}
|
||||
}
|
||||
|
||||
private static ulong ReadU64(void* contextRecord, int offset)
|
||||
{
|
||||
return *(ulong*)((byte*)contextRecord + offset);
|
||||
}
|
||||
|
||||
[LibraryImport("kernel32.dll")]
|
||||
private static partial uint TlsAlloc();
|
||||
|
||||
[LibraryImport("kernel32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool TlsFree(uint dwTlsIndex);
|
||||
|
||||
[LibraryImport("kernel32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool TlsSetValue(uint dwTlsIndex, nint lpTlsValue);
|
||||
|
||||
[LibraryImport("kernel32.dll")]
|
||||
private static partial nint TlsGetValue(uint dwTlsIndex);
|
||||
|
||||
[LibraryImport("kernel32.dll")]
|
||||
private static partial uint GetCurrentThreadId();
|
||||
|
||||
[LibraryImport("kernel32.dll")]
|
||||
private static partial nint GetCurrentThread();
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
private static partial nuint SetThreadAffinityMask(nint hThread, nuint dwThreadAffinityMask);
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
private static partial nint CreateThread(
|
||||
nint lpThreadAttributes,
|
||||
nuint dwStackSize,
|
||||
nint lpStartAddress,
|
||||
nint lpParameter,
|
||||
uint dwCreationFlags,
|
||||
out uint lpThreadId);
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
private static partial uint WaitForSingleObject(nint hHandle, uint dwMilliseconds);
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
private static partial nint OpenThread(uint dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, uint dwThreadId);
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
private static partial uint SuspendThread(nint hThread);
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
private static partial uint ResumeThread(nint hThread);
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool GetThreadContext(nint hThread, void* lpContext);
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool CloseHandle(nint hObject);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.HLE;
|
||||
|
||||
/// <summary>
|
||||
/// Lets host-facing libraries (VideoOut, AudioOut) request cooperative guest
|
||||
/// shutdown without taking a dependency on SharpEmu.Core.
|
||||
/// </summary>
|
||||
public static class HostSessionControl
|
||||
{
|
||||
private static Action<string>? _shutdownHandler;
|
||||
|
||||
public static void SetShutdownHandler(Action<string>? handler)
|
||||
{
|
||||
Volatile.Write(ref _shutdownHandler, handler);
|
||||
}
|
||||
|
||||
public static void RequestShutdown(string reason)
|
||||
{
|
||||
try
|
||||
{
|
||||
Volatile.Read(ref _shutdownHandler)?.Invoke(reason);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Host shutdown handler failed: {exception.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,4 +8,6 @@ public interface ICpuMemory
|
||||
bool TryRead(ulong virtualAddress, Span<byte> destination);
|
||||
|
||||
bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source);
|
||||
|
||||
bool TryCompare(ulong virtualAddress, ReadOnlySpan<byte> expected) => false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.HLE;
|
||||
|
||||
/// <summary>
|
||||
/// Implemented by memories that decorate another <see cref="ICpuMemory"/>
|
||||
/// (e.g. access trackers) so capability lookups can unwrap to the real
|
||||
/// implementation without reflection.
|
||||
/// </summary>
|
||||
public interface ICpuMemoryWrapper
|
||||
{
|
||||
ICpuMemory Inner { get; }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.HLE;
|
||||
|
||||
/// <summary>
|
||||
/// Guest address-space manipulation beyond plain allocation: fixed-address
|
||||
/// mapping and page-protection changes. Guest addresses are identity-mapped
|
||||
/// onto host pages by the implementing memory, so HLE exports (mmap, mprotect)
|
||||
/// reach these operations through <c>ctx.Memory</c> instead of calling host
|
||||
/// APIs directly. Member signatures deliberately mirror the implementation in
|
||||
/// SharpEmu.Core so existing call sites migrate call-for-call.
|
||||
/// </summary>
|
||||
public interface IGuestAddressSpace : IGuestMemoryAllocator
|
||||
{
|
||||
ulong AllocateAt(ulong desiredAddress, ulong size, bool executable = true, bool allowAlternative = true);
|
||||
|
||||
bool TryAllocateAtOrAbove(ulong desiredAddress, ulong size, bool executable, ulong alignment, out ulong actualAddress);
|
||||
|
||||
bool TryProtect(ulong address, ulong size, GuestPageProtection protection);
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace SharpEmu.HLE;
|
||||
|
||||
@@ -12,6 +13,7 @@ public sealed class ModuleManager : IModuleManager
|
||||
private readonly ConcurrentDictionary<string, ExportedFunction> _exportTable = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, ExportedFunction> _exportNameTable = new(StringComparer.Ordinal);
|
||||
private readonly object _registrationGate = new();
|
||||
private readonly HashSet<(Assembly Assembly, Generation Generation)> _scannedAssemblies = new();
|
||||
private bool _isFrozen;
|
||||
|
||||
public int RegisterFromAssembly(Assembly assembly, Generation generation, ISymbolCatalog? symbolCatalog = null)
|
||||
@@ -25,6 +27,12 @@ public sealed class ModuleManager : IModuleManager
|
||||
throw new InvalidOperationException("Module registration is frozen.");
|
||||
}
|
||||
|
||||
// Deduplicated: one assembly is reached through many types.
|
||||
if (!_scannedAssemblies.Add((assembly, generation)))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var registeredCount = 0;
|
||||
var instances = new Dictionary<Type, object>();
|
||||
|
||||
@@ -73,8 +81,195 @@ public sealed class ModuleManager : IModuleManager
|
||||
{
|
||||
_isFrozen = true;
|
||||
}
|
||||
|
||||
WarmHleTypeInitializers();
|
||||
}
|
||||
|
||||
// A .cctor or first JIT running on a guest thread's hijacked stack fail-fasts the CLR.
|
||||
// Run every HLE type's initializer and JIT its methods here first, on a host thread.
|
||||
private void WarmHleTypeInitializers()
|
||||
{
|
||||
Assembly[] assemblies;
|
||||
lock (_registrationGate)
|
||||
{
|
||||
assemblies = _scannedAssemblies.Select(entry => entry.Assembly).Distinct().ToArray();
|
||||
}
|
||||
|
||||
assemblies = WithGuestReachableDependencies(assemblies);
|
||||
var bclWarmed = WarmFrameworkTypeInitializers();
|
||||
|
||||
var warmed = 0;
|
||||
var failed = 0;
|
||||
var jitted = 0;
|
||||
var jitFailed = 0;
|
||||
foreach (var assembly in assemblies)
|
||||
{
|
||||
Type[] types;
|
||||
try
|
||||
{
|
||||
types = assembly.GetTypes();
|
||||
}
|
||||
catch (ReflectionTypeLoadException ex)
|
||||
{
|
||||
types = ex.Types.Where(t => t is not null).ToArray()!;
|
||||
}
|
||||
|
||||
const BindingFlags allMembers = BindingFlags.Public | BindingFlags.NonPublic |
|
||||
BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
|
||||
|
||||
foreach (var type in types)
|
||||
{
|
||||
if (type is null || type.ContainsGenericParameters)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
RuntimeHelpers.RunClassConstructor(type.TypeHandle);
|
||||
warmed++;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// A throw here beats a guest-thread fail-fast later; swallow and continue.
|
||||
failed++;
|
||||
}
|
||||
|
||||
// Force-JIT (not execute) every method so no guest thread compiles one first.
|
||||
MethodBase[] members;
|
||||
try
|
||||
{
|
||||
members = type.GetConstructors(allMembers)
|
||||
.Concat<MethodBase>(type.GetMethods(allMembers))
|
||||
.ToArray();
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var member in members)
|
||||
{
|
||||
if (member.ContainsGenericParameters || member.IsAbstract || member.MethodImplementationFlags.HasFlag(MethodImplAttributes.InternalCall))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
RuntimeHelpers.PrepareMethod(member.MethodHandle);
|
||||
jitted++;
|
||||
}
|
||||
catch
|
||||
{
|
||||
jitFailed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[HLE] Warmed {warmed} type initializers ({failed} threw) + JIT-compiled {jitted} methods ({jitFailed} skipped) across {assemblies.Length} HLE assemblies, plus {bclWarmed} framework type initializers.");
|
||||
}
|
||||
|
||||
// Framework .cctors too (but not JIT — the BCL is too large).
|
||||
private static int WarmFrameworkTypeInitializers()
|
||||
{
|
||||
var warmed = 0;
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
var name = assembly.GetName().Name;
|
||||
if (name is null || !IsFrameworkAssembly(name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Type[] types;
|
||||
try
|
||||
{
|
||||
types = assembly.GetTypes();
|
||||
}
|
||||
catch (ReflectionTypeLoadException ex)
|
||||
{
|
||||
types = ex.Types.Where(t => t is not null).ToArray()!;
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var type in types)
|
||||
{
|
||||
if (type is null || type.ContainsGenericParameters)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
RuntimeHelpers.RunClassConstructor(type.TypeHandle);
|
||||
warmed++;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return warmed;
|
||||
}
|
||||
|
||||
private static bool IsFrameworkAssembly(string assemblyName) =>
|
||||
assemblyName.StartsWith("System", StringComparison.Ordinal) ||
|
||||
string.Equals(assemblyName, "netstandard", StringComparison.Ordinal);
|
||||
|
||||
// Warm the interop assemblies guest threads reach (e.g. Silk.NET via the flip path).
|
||||
private static Assembly[] WithGuestReachableDependencies(Assembly[] scanned)
|
||||
{
|
||||
var result = new Dictionary<string, Assembly>(StringComparer.Ordinal);
|
||||
foreach (var assembly in scanned)
|
||||
{
|
||||
result[assembly.FullName ?? assembly.GetName().Name ?? string.Empty] = assembly;
|
||||
}
|
||||
|
||||
foreach (var assembly in scanned)
|
||||
{
|
||||
AssemblyName[] references;
|
||||
try
|
||||
{
|
||||
references = assembly.GetReferencedAssemblies();
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var reference in references)
|
||||
{
|
||||
var name = reference.Name;
|
||||
if (name is null || !IsGuestReachableInterop(name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var loaded = Assembly.Load(reference);
|
||||
result[loaded.FullName ?? name] = loaded;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.Values.ToArray();
|
||||
}
|
||||
|
||||
private static bool IsGuestReachableInterop(string assemblyName) =>
|
||||
assemblyName.StartsWith("Silk.NET", StringComparison.Ordinal) ||
|
||||
assemblyName.StartsWith("SharpEmu", StringComparison.Ordinal);
|
||||
|
||||
public bool TryGetFunction(string nid, out Delegate function)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(nid);
|
||||
|
||||
@@ -12,6 +12,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Aerolib\aerolib.bin" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {}
|
||||
"net10.0": {
|
||||
"sharpemu.logging": {
|
||||
"type": "Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ public static class AgcExports
|
||||
private const uint ItDispatchDirect = 0x15;
|
||||
private const uint ItDispatchIndirect = 0x16;
|
||||
private const uint ItWaitRegMem = 0x3C;
|
||||
private const uint ItIndirectBuffer = 0x3F;
|
||||
private const uint ItEventWrite = 0x46;
|
||||
private const uint ItDmaData = 0x50;
|
||||
private const uint ItSetContextReg = 0x69;
|
||||
@@ -120,6 +121,9 @@ 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;
|
||||
@@ -154,7 +158,7 @@ public static class AgcExports
|
||||
private static readonly HashSet<uint> _tracedSubmittedDrawOpcodes = new();
|
||||
private static readonly Dictionary<(ulong Ps, ulong State, Gen5PixelOutputKind Output), byte[]> _pixelSpirvCache = new();
|
||||
private static readonly Dictionary<
|
||||
(ulong Es, ulong EsState, ulong Ps, ulong PsState, Gen5PixelOutputKind Output),
|
||||
(ulong Es, ulong EsState, ulong Ps, ulong PsState, string OutputLayout, uint Attributes),
|
||||
(byte[] Vertex, byte[] Pixel)> _graphicsSpirvCache = new();
|
||||
private static readonly Dictionary<
|
||||
(ulong Cs, ulong State, uint LocalX, uint LocalY, uint LocalZ),
|
||||
@@ -388,9 +392,26 @@ 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(
|
||||
@@ -1923,6 +1944,96 @@ public static class AgcExports
|
||||
return ReturnPointer(ctx, commandAddress);
|
||||
}
|
||||
|
||||
// Guest draw-command builders: emit valid (skippable) packets and return a live
|
||||
// command pointer so the guest's command-buffer build succeeds; full draw processing is TODO.
|
||||
[SysAbiExport(
|
||||
Nid = "8N2tmT3jmC8",
|
||||
ExportName = "sceAgcDcbSetIndexCount",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAgc")]
|
||||
public static int DcbSetIndexCount(CpuContext ctx)
|
||||
{
|
||||
var dcb = ctx[CpuRegister.Rdi];
|
||||
var indexCount = (uint)ctx[CpuRegister.Rsi];
|
||||
if (dcb == 0)
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
if (!TryAllocateCommandDwords(ctx, dcb, 2, out var cmd) ||
|
||||
!ctx.TryWriteUInt32(cmd, Pm4(2, ItNop, RZero)) ||
|
||||
!ctx.TryWriteUInt32(cmd + 4, indexCount))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
return ReturnPointer(ctx, cmd);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "xSAR0LTcRKM",
|
||||
ExportName = "sceAgcDcbJump",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAgc")]
|
||||
public static int DcbJump(CpuContext ctx)
|
||||
{
|
||||
var dcb = ctx[CpuRegister.Rdi];
|
||||
var target = ctx[CpuRegister.Rsi];
|
||||
var sizeDwords = (uint)ctx[CpuRegister.Rdx];
|
||||
if (dcb == 0)
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
if (!TryAllocateCommandDwords(ctx, dcb, 4, out var cmd) ||
|
||||
!ctx.TryWriteUInt32(cmd, Pm4(4, ItIndirectBuffer, RZero)) ||
|
||||
!ctx.TryWriteUInt32(cmd + 4, (uint)(target & 0xFFFF_FFFFUL)) ||
|
||||
!ctx.TryWriteUInt32(cmd + 8, (uint)((target >> 32) & 0xFFFFUL)) ||
|
||||
!ctx.TryWriteUInt32(cmd + 12, sizeDwords & 0xFFFFF))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
return ReturnPointer(ctx, cmd);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "bbFueFP+J4k",
|
||||
ExportName = "sceAgcDcbSetPredication",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAgc")]
|
||||
public static int DcbSetPredication(CpuContext ctx)
|
||||
{
|
||||
var dcb = ctx[CpuRegister.Rdi];
|
||||
var address = ctx[CpuRegister.Rsi];
|
||||
if (dcb == 0)
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
if (!TryAllocateCommandDwords(ctx, dcb, 3, out var cmd) ||
|
||||
!ctx.TryWriteUInt32(cmd, Pm4(3, ItNop, RZero)) ||
|
||||
!ctx.TryWriteUInt32(cmd + 4, (uint)(address & 0xFFFF_FFFFUL)) ||
|
||||
!ctx.TryWriteUInt32(cmd + 8, (uint)(address >> 32)))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
return ReturnPointer(ctx, cmd);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "w6Dj1VJt5qY",
|
||||
ExportName = "sceAgcSetPacketPredication",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAgc")]
|
||||
public static int SetPacketPredication(CpuContext ctx)
|
||||
{
|
||||
// Global predication toggle on a packet; a no-op is safe for rendering.
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "w2rJhmD+dsE",
|
||||
ExportName = "sceAgcDriverAddEqEvent",
|
||||
@@ -1999,8 +2110,16 @@ public static class AgcExports
|
||||
var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
||||
lock (gpuState.Gate)
|
||||
{
|
||||
ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets);
|
||||
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
||||
Gen5ShaderScalarEvaluator.BeginGlobalMemoryReadScope();
|
||||
try
|
||||
{
|
||||
ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets);
|
||||
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Gen5ShaderScalarEvaluator.EndGlobalMemoryReadScope();
|
||||
}
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
@@ -2030,27 +2149,35 @@ public static class AgcExports
|
||||
var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
||||
lock (gpuState.Gate)
|
||||
{
|
||||
for (uint i = 0; i < bufferCount; i++)
|
||||
Gen5ShaderScalarEvaluator.BeginGlobalMemoryReadScope();
|
||||
try
|
||||
{
|
||||
if (!ctx.TryReadUInt64(addressArray + i * 8, out var commandAddress) ||
|
||||
commandAddress == 0 ||
|
||||
!ctx.TryReadUInt32(sizeArray + i * 4, out var dwordCount) ||
|
||||
dwordCount == 0)
|
||||
for (uint i = 0; i < bufferCount; i++)
|
||||
{
|
||||
continue;
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Gen5ShaderScalarEvaluator.EndGlobalMemoryReadScope();
|
||||
}
|
||||
|
||||
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
@@ -2098,8 +2225,16 @@ public static class AgcExports
|
||||
gpuState.ComputeQueues.Add(ownerHandle, queueState);
|
||||
}
|
||||
|
||||
ParseSubmittedDcb(ctx, gpuState, queueState, commandAddress, dwordCount, tracePackets);
|
||||
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
||||
Gen5ShaderScalarEvaluator.BeginGlobalMemoryReadScope();
|
||||
try
|
||||
{
|
||||
ParseSubmittedDcb(ctx, gpuState, queueState, commandAddress, dwordCount, tracePackets);
|
||||
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Gen5ShaderScalarEvaluator.EndGlobalMemoryReadScope();
|
||||
}
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
@@ -2120,12 +2255,87 @@ public static class AgcExports
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt32(outAddress, 256))
|
||||
if (!ctx.TryWriteUInt32(outAddress, ResourceRegistrationMaxNameLength))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
TraceAgc($"agc.driver_get_resource_registration_max_name_length out=0x{outAddress:X16} value=256");
|
||||
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}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
@@ -2144,12 +2354,97 @@ public static class AgcExports
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt32(ownerAddress, DefaultAgcOwner))
|
||||
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
||||
uint owner;
|
||||
lock (state.Gate)
|
||||
{
|
||||
owner = state.DefaultOwner;
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt32(ownerAddress, owner))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
TraceAgc($"agc.driver_get_default_owner out=0x{ownerAddress:X16} owner={DefaultAgcOwner}");
|
||||
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)}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
@@ -2160,19 +2455,90 @@ public static class AgcExports
|
||||
LibraryName = "libSceAgc")]
|
||||
public static int DriverRegisterResource(CpuContext ctx)
|
||||
{
|
||||
var resourceAddress = ctx[CpuRegister.Rdi];
|
||||
var resourceHandleAddress = ctx[CpuRegister.Rdi];
|
||||
var owner = (uint)ctx[CpuRegister.Rsi];
|
||||
var nameAddress = ctx[CpuRegister.Rdx];
|
||||
var type = (uint)ctx[CpuRegister.R8];
|
||||
var flags = (uint)ctx[CpuRegister.R9];
|
||||
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);
|
||||
}
|
||||
|
||||
TraceAgc(
|
||||
$"agc.driver_register_resource resource=0x{resourceAddress:X16} owner={owner} " +
|
||||
$"name=0x{nameAddress:X16} type={type} flags={flags}");
|
||||
$"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}");
|
||||
|
||||
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",
|
||||
@@ -3054,12 +3420,13 @@ public static class AgcExports
|
||||
textures,
|
||||
globalMemoryBuffers,
|
||||
translatedDraw.AttributeCount,
|
||||
new VulkanGuestRenderTarget(
|
||||
firstTarget.Address,
|
||||
firstTarget.Width,
|
||||
firstTarget.Height,
|
||||
firstTarget.Format,
|
||||
firstTarget.NumberType),
|
||||
translatedDraw.RenderTargets.Select(target =>
|
||||
new VulkanGuestRenderTarget(
|
||||
target.Address,
|
||||
target.Width,
|
||||
target.Height,
|
||||
target.Format,
|
||||
target.NumberType)).ToArray(),
|
||||
translatedDraw.VertexSpirv,
|
||||
translatedDraw.VertexCount,
|
||||
translatedDraw.InstanceCount,
|
||||
@@ -3167,7 +3534,8 @@ public static class AgcExports
|
||||
exportState,
|
||||
out var exportEvaluation,
|
||||
out error,
|
||||
resolveVertexInputs: true) ||
|
||||
resolveVertexInputs: true,
|
||||
vertexRecordLimit: indexed ? null : vertexCount) ||
|
||||
!Gen5ShaderTranslator.TryCreateState(
|
||||
ctx,
|
||||
pixelShaderAddress,
|
||||
@@ -3186,19 +3554,47 @@ public static class AgcExports
|
||||
}
|
||||
|
||||
var renderTargets = GetRenderTargets(state.CxRegisters)
|
||||
.Where(target =>
|
||||
target.Slot == 0 &&
|
||||
HasPixelColorExport(pixelState, target.Slot))
|
||||
.Where(target => HasPixelColorExport(pixelState, target.Slot))
|
||||
.OrderBy(target => target.Slot)
|
||||
.ToArray();
|
||||
var outputKind = GetPixelOutputKind(renderTargets.FirstOrDefault().NumberType);
|
||||
var exportStateFingerprint = ComputeShaderStateFingerprint(exportEvaluation);
|
||||
var pixelStateFingerprint = ComputeShaderStateFingerprint(pixelEvaluation);
|
||||
var renderTargetFormats = new VulkanRenderTargetFormat[renderTargets.Length];
|
||||
for (var index = 0; index < renderTargets.Length; index++)
|
||||
{
|
||||
var target = renderTargets[index];
|
||||
if (!VulkanVideoPresenter.TryDecodeRenderTargetFormat(
|
||||
target.Format,
|
||||
target.NumberType,
|
||||
out renderTargetFormats[index]))
|
||||
{
|
||||
error =
|
||||
$"unsupported color target format={target.Format} number_type={target.NumberType}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var pixelOutputs = renderTargets
|
||||
.Select((target, location) => new Gen5PixelOutputBinding(
|
||||
target.Slot,
|
||||
(uint)location,
|
||||
renderTargetFormats[location].OutputKind))
|
||||
.ToArray();
|
||||
var outputLayout = string.Join(
|
||||
';',
|
||||
pixelOutputs.Select(output =>
|
||||
$"{output.GuestSlot}:{output.HostLocation}:{(int)output.Kind}"));
|
||||
var attributeCount = GetInterpolatedAttributeCount(pixelState);
|
||||
var exportStateFingerprint = ComputeShaderStructureFingerprint(exportEvaluation);
|
||||
var pixelStateFingerprint = ComputeShaderStructureFingerprint(pixelEvaluation);
|
||||
var shaderKey = (
|
||||
exportShaderAddress,
|
||||
exportStateFingerprint,
|
||||
pixelShaderAddress,
|
||||
pixelStateFingerprint,
|
||||
outputKind);
|
||||
outputLayout,
|
||||
attributeCount);
|
||||
var totalGlobalBuffers =
|
||||
pixelEvaluation.GlobalMemoryBindings.Count +
|
||||
exportEvaluation.GlobalMemoryBindings.Count;
|
||||
(byte[] Vertex, byte[] Pixel) compiled;
|
||||
lock (_submitTraceGate)
|
||||
{
|
||||
@@ -3207,26 +3603,25 @@ public static class AgcExports
|
||||
|
||||
if (compiled.Vertex is null || compiled.Pixel is null)
|
||||
{
|
||||
var totalGlobalBuffers =
|
||||
pixelEvaluation.GlobalMemoryBindings.Count +
|
||||
exportEvaluation.GlobalMemoryBindings.Count;
|
||||
if (!Gen5SpirvTranslator.TryCompilePixelShader(
|
||||
pixelState,
|
||||
pixelEvaluation,
|
||||
outputKind,
|
||||
pixelOutputs,
|
||||
out var pixelShader,
|
||||
out error,
|
||||
globalBufferBase: 0,
|
||||
totalGlobalBufferCount: totalGlobalBuffers,
|
||||
imageBindingBase: 0) ||
|
||||
totalGlobalBufferCount: totalGlobalBuffers + 2,
|
||||
imageBindingBase: 0,
|
||||
scalarRegisterBufferIndex: totalGlobalBuffers) ||
|
||||
!Gen5SpirvTranslator.TryCompileVertexShader(
|
||||
exportState,
|
||||
exportEvaluation,
|
||||
out var vertexShader,
|
||||
out error,
|
||||
globalBufferBase: pixelEvaluation.GlobalMemoryBindings.Count,
|
||||
totalGlobalBufferCount: totalGlobalBuffers,
|
||||
imageBindingBase: pixelEvaluation.ImageBindings.Count))
|
||||
totalGlobalBufferCount: totalGlobalBuffers + 2,
|
||||
imageBindingBase: pixelEvaluation.ImageBindings.Count,
|
||||
scalarRegisterBufferIndex: totalGlobalBuffers + 1))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -3278,6 +3673,8 @@ public static class AgcExports
|
||||
|
||||
var globalMemoryBindings = pixelEvaluation.GlobalMemoryBindings
|
||||
.Concat(exportEvaluation.GlobalMemoryBindings)
|
||||
.Append(CreateScalarRegisterBinding(pixelEvaluation))
|
||||
.Append(CreateScalarRegisterBinding(exportEvaluation))
|
||||
.ToArray();
|
||||
IReadOnlyList<Gen5VertexInputBinding> vertexInputs =
|
||||
exportEvaluation.VertexInputs ?? [];
|
||||
@@ -3288,7 +3685,7 @@ public static class AgcExports
|
||||
primitiveType,
|
||||
compiled.Vertex,
|
||||
compiled.Pixel,
|
||||
GetInterpolatedAttributeCount(pixelState),
|
||||
attributeCount,
|
||||
vertexCount,
|
||||
state.InstanceCount,
|
||||
indexed ? CreateVulkanIndexBuffer(ctx, state, vertexCount) : null,
|
||||
@@ -3296,10 +3693,66 @@ public static class AgcExports
|
||||
globalMemoryBindings,
|
||||
vertexInputs,
|
||||
renderTargets,
|
||||
CreateRenderState(state.CxRegisters, renderTargets.FirstOrDefault()));
|
||||
ApplyTransparentPremultipliedFillClear(
|
||||
CreateRenderState(state.CxRegisters, renderTargets, pixelState),
|
||||
textures,
|
||||
vertexInputs,
|
||||
pixelEvaluation.InitialScalarRegisters));
|
||||
return true;
|
||||
}
|
||||
|
||||
private static readonly bool _transparentFillClearEnabled = !string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_TRANSPARENT_FILL_CLEAR"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Chowdren resets its effect layers with an untextured transparent-black
|
||||
/// fill using premultiplied blending. With One/OneMinusSrcAlpha that draw
|
||||
/// is otherwise a no-op, causing fog and vignette layers to accumulate.
|
||||
/// Treat precisely that draw shape as an overwrite only when every MRT
|
||||
/// attachment uses the same premultiplied blend pattern.
|
||||
/// </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.Blends.All(IsTransparentPremultipliedFillBlend))
|
||||
{
|
||||
return renderState;
|
||||
}
|
||||
|
||||
for (var index = 0; index < 4; index++)
|
||||
{
|
||||
if ((pixelUserData[index] & 0x7FFF_FFFFu) != 0)
|
||||
{
|
||||
return renderState;
|
||||
}
|
||||
}
|
||||
|
||||
return renderState with
|
||||
{
|
||||
Blends = renderState.Blends
|
||||
.Select(blend => blend with { Enable = false })
|
||||
.ToArray(),
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsTransparentPremultipliedFillBlend(VulkanGuestBlendState blend) =>
|
||||
blend is
|
||||
{
|
||||
Enable: true,
|
||||
ColorSrcFactor: 1,
|
||||
ColorDstFactor: 5,
|
||||
ColorFunc: 0,
|
||||
};
|
||||
|
||||
private static VulkanGuestIndexBuffer? CreateVulkanIndexBuffer(
|
||||
CpuContext ctx,
|
||||
SubmittedDcbState state,
|
||||
@@ -3322,19 +3775,15 @@ public static class AgcExports
|
||||
: null;
|
||||
}
|
||||
|
||||
private static Gen5PixelOutputKind GetPixelOutputKind(uint numberType) =>
|
||||
numberType switch
|
||||
{
|
||||
4 => Gen5PixelOutputKind.Uint,
|
||||
5 => Gen5PixelOutputKind.Sint,
|
||||
_ => Gen5PixelOutputKind.Float,
|
||||
};
|
||||
|
||||
private static bool HasPixelColorExport(Gen5ShaderState state, uint target) =>
|
||||
state.Program.Instructions.Any(instruction =>
|
||||
instruction.Control is Gen5ExportControl export &&
|
||||
export.Target == target &&
|
||||
export.EnableMask != 0);
|
||||
GetPixelColorExportMask(state, target) != 0;
|
||||
|
||||
private static uint GetPixelColorExportMask(Gen5ShaderState state, uint target) =>
|
||||
state.Program.Instructions
|
||||
.Select(instruction => instruction.Control)
|
||||
.OfType<Gen5ExportControl>()
|
||||
.Where(export => export.Target == target)
|
||||
.Aggregate(0u, (mask, export) => mask | (export.EnableMask & 0xFu));
|
||||
|
||||
private static uint GetInterpolatedAttributeCount(Gen5ShaderState state)
|
||||
{
|
||||
@@ -3350,6 +3799,88 @@ public static class AgcExports
|
||||
return (uint)(maxAttribute + 1);
|
||||
}
|
||||
|
||||
private const int ShaderScalarRegisterCount = 256;
|
||||
|
||||
private static Gen5GlobalMemoryBinding CreateScalarRegisterBinding(
|
||||
Gen5ShaderEvaluation evaluation)
|
||||
{
|
||||
var data = new byte[ShaderScalarRegisterCount * sizeof(uint)];
|
||||
var registers = evaluation.InitialScalarRegisters;
|
||||
var count = Math.Min(registers.Count, ShaderScalarRegisterCount);
|
||||
for (var index = 0; index < count; index++)
|
||||
{
|
||||
var value = registers[index];
|
||||
var offset = index * sizeof(uint);
|
||||
data[offset] = (byte)value;
|
||||
data[offset + 1] = (byte)(value >> 8);
|
||||
data[offset + 2] = (byte)(value >> 16);
|
||||
data[offset + 3] = (byte)(value >> 24);
|
||||
}
|
||||
|
||||
return new Gen5GlobalMemoryBinding(0, 0, [], data);
|
||||
}
|
||||
|
||||
private static ulong ComputeShaderStructureFingerprint(Gen5ShaderEvaluation evaluation)
|
||||
{
|
||||
const ulong offsetBasis = 14695981039346656037UL;
|
||||
const ulong prime = 1099511628211UL;
|
||||
var hash = offsetBasis;
|
||||
void Mix(ulong value) => hash = (hash ^ value) * prime;
|
||||
|
||||
Mix((ulong)evaluation.GlobalMemoryBindings.Count);
|
||||
foreach (var binding in evaluation.GlobalMemoryBindings)
|
||||
{
|
||||
Mix(binding.ScalarAddress);
|
||||
Mix((ulong)binding.InstructionPcs.Count);
|
||||
foreach (var pc in binding.InstructionPcs)
|
||||
{
|
||||
Mix(pc);
|
||||
}
|
||||
}
|
||||
|
||||
Mix((ulong)evaluation.ImageBindings.Count);
|
||||
foreach (var image in evaluation.ImageBindings)
|
||||
{
|
||||
Mix(image.Pc);
|
||||
Mix((ulong)(uint)image.Opcode.GetHashCode());
|
||||
foreach (var word in image.ResourceDescriptor)
|
||||
{
|
||||
Mix(word);
|
||||
}
|
||||
|
||||
foreach (var word in image.SamplerDescriptor)
|
||||
{
|
||||
Mix(word);
|
||||
}
|
||||
|
||||
Mix(image.MipLevel ?? uint.MaxValue);
|
||||
}
|
||||
|
||||
if (evaluation.VertexInputs is { } vertexInputs)
|
||||
{
|
||||
Mix((ulong)vertexInputs.Count);
|
||||
foreach (var input in vertexInputs)
|
||||
{
|
||||
Mix(input.Pc);
|
||||
Mix(input.Location);
|
||||
Mix(input.ComponentCount);
|
||||
Mix(input.DataFormat);
|
||||
Mix(input.NumberFormat);
|
||||
Mix(input.Stride);
|
||||
}
|
||||
}
|
||||
|
||||
if (evaluation.ComputeSystemRegisters is { } computeSystemRegisters)
|
||||
{
|
||||
Mix(computeSystemRegisters.WorkGroupXRegister ?? uint.MaxValue);
|
||||
Mix(computeSystemRegisters.WorkGroupYRegister ?? uint.MaxValue);
|
||||
Mix(computeSystemRegisters.WorkGroupZRegister ?? uint.MaxValue);
|
||||
Mix(computeSystemRegisters.ThreadGroupSizeRegister ?? uint.MaxValue);
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
private static ulong ComputeShaderStateFingerprint(Gen5ShaderEvaluation evaluation)
|
||||
{
|
||||
const ulong offsetBasis = 14695981039346656037UL;
|
||||
@@ -3410,11 +3941,25 @@ public static class AgcExports
|
||||
|
||||
private static VulkanGuestRenderState CreateRenderState(
|
||||
IReadOnlyDictionary<uint, uint> registers,
|
||||
RenderTargetDescriptor target)
|
||||
IReadOnlyList<RenderTargetDescriptor> targets,
|
||||
Gen5ShaderState pixelState)
|
||||
{
|
||||
if (targets.Count == 0)
|
||||
{
|
||||
return VulkanGuestRenderState.Default;
|
||||
}
|
||||
|
||||
var target = targets[0];
|
||||
var scissor = DecodeScissor(registers, target.Width, target.Height);
|
||||
return new VulkanGuestRenderState(
|
||||
DecodeBlendState(registers, target.Slot),
|
||||
targets.Select(target =>
|
||||
{
|
||||
var blend = DecodeBlendState(registers, target.Slot);
|
||||
return blend with
|
||||
{
|
||||
WriteMask = blend.WriteMask & GetPixelColorExportMask(pixelState, target.Slot),
|
||||
};
|
||||
}).ToArray(),
|
||||
scissor,
|
||||
DecodeViewport(registers, target.Width, target.Height, scissor));
|
||||
}
|
||||
@@ -3439,7 +3984,7 @@ public static class AgcExports
|
||||
(control >> 24) & 0x1Fu,
|
||||
(control >> 21) & 0x7u,
|
||||
((control >> 29) & 1u) != 0,
|
||||
writeMask == 0 ? 0xFu : writeMask);
|
||||
writeMask);
|
||||
}
|
||||
|
||||
private static VulkanGuestRect? DecodeScissor(
|
||||
@@ -4200,7 +4745,7 @@ public static class AgcExports
|
||||
var localSizeZ = GetComputeLocalSize(state.ShRegisters, ComputeNumThreadZ);
|
||||
var gpuDispatch = false;
|
||||
var computeError = string.Empty;
|
||||
if (hasStorageBinding &&
|
||||
if ((hasStorageBinding || evaluation.GlobalMemoryBindings.Count != 0) &&
|
||||
(ulong)localSizeX * localSizeY * localSizeZ <= 1024)
|
||||
{
|
||||
var shaderKey = (
|
||||
@@ -4270,7 +4815,8 @@ 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}" +
|
||||
$"gpu={gpuDispatch} blits={blitCount} " +
|
||||
$"globals={evaluation.GlobalMemoryBindings.Count}" +
|
||||
(computeError.Length == 0 ? string.Empty : $" error={computeError}") +
|
||||
$" bindings=[{string.Join(',', descriptions)}]");
|
||||
}
|
||||
@@ -4776,7 +5322,7 @@ public static class AgcExports
|
||||
if (Gen5SpirvTranslator.TryCompilePixelShader(
|
||||
pixelState,
|
||||
evaluation,
|
||||
Gen5PixelOutputKind.Float,
|
||||
[new(0, 0, Gen5PixelOutputKind.Float)],
|
||||
out var compiledPixel,
|
||||
out var compileError))
|
||||
{
|
||||
|
||||
@@ -49,6 +49,11 @@ internal enum Gen5PixelOutputKind
|
||||
Sint,
|
||||
}
|
||||
|
||||
internal readonly record struct Gen5PixelOutputBinding(
|
||||
uint GuestSlot,
|
||||
uint HostLocation,
|
||||
Gen5PixelOutputKind Kind);
|
||||
|
||||
internal enum Gen5SpirvStage
|
||||
{
|
||||
Vertex,
|
||||
|
||||
@@ -13,6 +13,24 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
private const int ImageDescriptorDwords = 8;
|
||||
private const int SamplerDescriptorDwords = 4;
|
||||
private const int MaxGlobalMemoryBindingBytes = 16 * 1024 * 1024;
|
||||
private static readonly int DefaultGlobalMemoryBindingBytes =
|
||||
int.TryParse(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_GLOBAL_BINDING_BYTES"),
|
||||
out var configured) && configured >= sizeof(uint)
|
||||
? Math.Min(configured, MaxGlobalMemoryBindingBytes)
|
||||
: 1 * 1024 * 1024;
|
||||
|
||||
internal static long GlobalMemoryReadCount;
|
||||
internal static long GlobalMemoryReadBytes;
|
||||
internal static long GlobalMemoryReadCacheHits;
|
||||
internal static long GlobalMemoryReadPvmBytes;
|
||||
internal static long GlobalMemoryReadLibcBytes;
|
||||
internal static long GlobalMemoryReadReuses;
|
||||
|
||||
private const long CrossFrameReadCacheMaxBytes = 1024L * 1024 * 1024;
|
||||
private static readonly object _crossFrameReadGate = new();
|
||||
private static readonly Dictionary<(ulong BaseAddress, int SizeBytes), byte[]> _crossFrameReadCache = new();
|
||||
private static long _crossFrameReadCacheBytes;
|
||||
private const ulong RdnaWaveMask = 0xFFFF_FFFFUL;
|
||||
|
||||
private readonly record struct BufferDescriptor(
|
||||
@@ -44,7 +62,8 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
Gen5ShaderState state,
|
||||
out Gen5ShaderEvaluation evaluation,
|
||||
out string error,
|
||||
bool resolveVertexInputs = false)
|
||||
bool resolveVertexInputs = false,
|
||||
uint? vertexRecordLimit = null)
|
||||
{
|
||||
evaluation = default!;
|
||||
error = string.Empty;
|
||||
@@ -255,10 +274,28 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
if (resolveVertexInputs &&
|
||||
IsVertexFetchCandidate(instruction, bufferMemory, bufferDescriptor))
|
||||
{
|
||||
var vertexReadSize = bufferDescriptor.SizeBytes;
|
||||
if (vertexRecordLimit is { } recordLimit &&
|
||||
instruction.Sources.Count > 2 &&
|
||||
TryEvaluateScalarOperand(
|
||||
instruction.Sources[2],
|
||||
scalarRegisters,
|
||||
out var scalarOffset))
|
||||
{
|
||||
var bindingOffset = unchecked((uint)bufferMemory.OffsetBytes + scalarOffset);
|
||||
var requiredBytes =
|
||||
(ulong)bindingOffset +
|
||||
(ulong)(Math.Max(recordLimit, 1u) - 1u) * bufferDescriptor.Stride +
|
||||
(ulong)bufferMemory.DwordCount * sizeof(uint);
|
||||
vertexReadSize = Math.Min(
|
||||
bufferDescriptor.SizeBytes,
|
||||
Math.Max(requiredBytes, sizeof(uint)));
|
||||
}
|
||||
|
||||
if (!TryReadGlobalMemory(
|
||||
ctx,
|
||||
bufferDescriptor.BaseAddress,
|
||||
bufferDescriptor.SizeBytes,
|
||||
vertexReadSize,
|
||||
out var vertexData))
|
||||
{
|
||||
error =
|
||||
@@ -533,22 +570,29 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
return false;
|
||||
}
|
||||
|
||||
[ThreadStatic]
|
||||
private static Dictionary<(ulong BaseAddress, int SizeBytes), byte[]>? _globalMemoryReadCache;
|
||||
|
||||
internal static void BeginGlobalMemoryReadScope()
|
||||
{
|
||||
_globalMemoryReadCache = new Dictionary<(ulong, int), byte[]>();
|
||||
}
|
||||
|
||||
internal static void EndGlobalMemoryReadScope()
|
||||
{
|
||||
_globalMemoryReadCache = null;
|
||||
}
|
||||
|
||||
private static bool TryReadGlobalMemory(
|
||||
CpuContext ctx,
|
||||
ulong baseAddress,
|
||||
out byte[] data)
|
||||
{
|
||||
for (var size = MaxGlobalMemoryBindingBytes; size >= 4096; size >>= 1)
|
||||
{
|
||||
data = GC.AllocateUninitializedArray<byte>(size);
|
||||
if (ctx.Memory.TryRead(baseAddress, data))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
data = [];
|
||||
return false;
|
||||
return TryReadGlobalMemory(
|
||||
ctx,
|
||||
baseAddress,
|
||||
(ulong)DefaultGlobalMemoryBindingBytes,
|
||||
out data);
|
||||
}
|
||||
|
||||
private static bool TryReadGlobalMemory(
|
||||
@@ -570,13 +614,74 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
return false;
|
||||
}
|
||||
|
||||
var cache = _globalMemoryReadCache;
|
||||
var cacheKey = (baseAddress, (int)cappedSize);
|
||||
if (cache is not null && cache.TryGetValue(cacheKey, out var cached))
|
||||
{
|
||||
Interlocked.Increment(ref GlobalMemoryReadCacheHits);
|
||||
data = cached;
|
||||
return true;
|
||||
}
|
||||
|
||||
byte[]? previous;
|
||||
lock (_crossFrameReadGate)
|
||||
{
|
||||
_crossFrameReadCache.TryGetValue(cacheKey, out previous);
|
||||
}
|
||||
|
||||
if (previous is not null && ctx.Memory.TryCompare(baseAddress, previous))
|
||||
{
|
||||
Interlocked.Increment(ref GlobalMemoryReadReuses);
|
||||
if (cache is not null)
|
||||
{
|
||||
cache[cacheKey] = previous;
|
||||
}
|
||||
|
||||
data = previous;
|
||||
return true;
|
||||
}
|
||||
|
||||
var candidateSize = (int)cappedSize;
|
||||
while (candidateSize >= sizeof(uint))
|
||||
{
|
||||
data = GC.AllocateUninitializedArray<byte>(candidateSize);
|
||||
if (ctx.Memory.TryRead(baseAddress, data) ||
|
||||
var readFromPvm = ctx.Memory.TryRead(baseAddress, data);
|
||||
if (readFromPvm ||
|
||||
KernelMemoryCompatExports.TryReadTrackedLibcHeap(baseAddress, data))
|
||||
{
|
||||
Interlocked.Increment(ref GlobalMemoryReadCount);
|
||||
Interlocked.Add(ref GlobalMemoryReadBytes, data.Length);
|
||||
if (readFromPvm)
|
||||
{
|
||||
Interlocked.Add(ref GlobalMemoryReadPvmBytes, data.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
Interlocked.Add(ref GlobalMemoryReadLibcBytes, data.Length);
|
||||
}
|
||||
|
||||
if (cache is not null)
|
||||
{
|
||||
cache[cacheKey] = data;
|
||||
}
|
||||
|
||||
lock (_crossFrameReadGate)
|
||||
{
|
||||
if (_crossFrameReadCache.TryGetValue(cacheKey, out var replaced))
|
||||
{
|
||||
_crossFrameReadCacheBytes -= replaced.Length;
|
||||
}
|
||||
|
||||
if (_crossFrameReadCacheBytes + data.Length > CrossFrameReadCacheMaxBytes)
|
||||
{
|
||||
_crossFrameReadCache.Clear();
|
||||
_crossFrameReadCacheBytes = 0;
|
||||
}
|
||||
|
||||
_crossFrameReadCache[cacheKey] = data;
|
||||
_crossFrameReadCacheBytes += data.Length;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -621,6 +621,8 @@ internal static class Gen5ShaderTranslator
|
||||
0x10 => "SSendmsg",
|
||||
0x16 => "STtraceData",
|
||||
0x20 => "SInstPrefetch",
|
||||
0x21 => "SClause",
|
||||
0x23 => "SWaitcntDepctr",
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
@@ -718,7 +720,7 @@ internal static class Gen5ShaderTranslator
|
||||
}
|
||||
|
||||
var src0 = word & 0x1FF;
|
||||
sizeDwords = opcode is 0x20 or 0x21 || src0 is 0xF9 or 0xFA or 0xFF ? 2u : 1u;
|
||||
sizeDwords = opcode is 0x20 or 0x21 or 0x2C or 0x2D || src0 is 0xF9 or 0xFA or 0xFF ? 2u : 1u;
|
||||
error = string.Empty;
|
||||
name = opcode switch
|
||||
{
|
||||
@@ -757,7 +759,9 @@ internal static class Gen5ShaderTranslator
|
||||
0x28 => "VAddcU32",
|
||||
0x29 => "VSubbU32",
|
||||
0x2A => "VSubbrevU32",
|
||||
0x2B => "VLdexpF32",
|
||||
0x2B => "VFmacF32",
|
||||
0x2C => "VFmamkF32",
|
||||
0x2D => "VFmaakF32",
|
||||
0x2F => "VCvtPkrtzF16F32",
|
||||
0x30 => "VCvtPkU16U32",
|
||||
0x31 => "VCvtPkI16I32",
|
||||
@@ -798,8 +802,15 @@ internal static class Gen5ShaderTranslator
|
||||
0x14 => "VCmpxGtF32",
|
||||
0x15 => "VCmpxLgF32",
|
||||
0x16 => "VCmpxGeF32",
|
||||
0x17 => "VCmpxOF32",
|
||||
0x18 => "VCmpxUF32",
|
||||
0x19 => "VCmpxNgeF32",
|
||||
0x1A => "VCmpxNlgF32",
|
||||
0x1B => "VCmpxNgtF32",
|
||||
0x1C => "VCmpxNleF32",
|
||||
0x1D => "VCmpxNeqF32",
|
||||
0x1E => "VCmpxNltF32",
|
||||
0x1F => "VCmpxTruF32",
|
||||
0x80 => "VCmpFI32",
|
||||
0x81 => "VCmpLtI32",
|
||||
0x82 => "VCmpEqI32",
|
||||
@@ -870,6 +881,7 @@ internal static class Gen5ShaderTranslator
|
||||
0x10F => "VMinF32",
|
||||
0x110 => "VMaxF32",
|
||||
0x11F => "VMacF32",
|
||||
0x12B => "VFmacF32",
|
||||
0x12F => "VCvtPkrtzF16F32",
|
||||
0x141 => "VMadF32",
|
||||
0x143 => "VMadU32U24",
|
||||
@@ -897,6 +909,7 @@ internal static class Gen5ShaderTranslator
|
||||
0x169 => "VMulLoU32",
|
||||
0x16A => "VMulHiU32",
|
||||
0x16B => "VMulLoI32",
|
||||
0x16C => "VMulHiI32",
|
||||
0x360 => "VMadU32U16",
|
||||
0x361 => "VMulLoU32",
|
||||
0x362 => "VLdexpF32",
|
||||
@@ -1379,7 +1392,7 @@ internal static class Gen5ShaderTranslator
|
||||
Gen5Operand.Source(word & 0x1FF, literal),
|
||||
Gen5Operand.Vector((word >> 9) & 0xFF),
|
||||
];
|
||||
if (opcode == "VMadMkF32" && literal.HasValue)
|
||||
if (opcode is "VMadMkF32" or "VFmamkF32" && literal.HasValue)
|
||||
{
|
||||
sources =
|
||||
[
|
||||
@@ -1388,7 +1401,7 @@ internal static class Gen5ShaderTranslator
|
||||
sources[1],
|
||||
];
|
||||
}
|
||||
else if (opcode == "VMadAkF32" && literal.HasValue)
|
||||
else if (opcode is "VMadAkF32" or "VFmaakF32" && literal.HasValue)
|
||||
{
|
||||
sources =
|
||||
[
|
||||
|
||||
@@ -255,6 +255,8 @@ internal static partial class Gen5SpirvTranslator
|
||||
case "VFmaF32":
|
||||
case "VMadMkF32":
|
||||
case "VMadAkF32":
|
||||
case "VFmamkF32":
|
||||
case "VFmaakF32":
|
||||
result = EmitFloatResult(
|
||||
instruction,
|
||||
Ext(
|
||||
@@ -265,6 +267,7 @@ internal static partial class Gen5SpirvTranslator
|
||||
GetFloatSource(instruction, 2)));
|
||||
break;
|
||||
case "VMacF32":
|
||||
case "VFmacF32":
|
||||
{
|
||||
var addend = Bitcast(_floatType, LoadV(destination));
|
||||
result = EmitFloatResult(
|
||||
@@ -334,6 +337,7 @@ internal static partial class Gen5SpirvTranslator
|
||||
result = EmitSubtractWithBorrow(instruction, reverse: true);
|
||||
break;
|
||||
case "VMulLoU32":
|
||||
case "VMulLoI32":
|
||||
case "VMulU32U24":
|
||||
result = EmitIntegerBinary(instruction, SpirvOp.IMul);
|
||||
break;
|
||||
@@ -369,6 +373,37 @@ 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(
|
||||
@@ -566,6 +601,18 @@ 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));
|
||||
@@ -675,6 +722,14 @@ 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;
|
||||
@@ -806,6 +861,25 @@ internal static partial class Gen5SpirvTranslator
|
||||
{
|
||||
condition = _module.ConstantBool(true);
|
||||
}
|
||||
else if (opcode is "VCmpOF32" or "VCmpxOF32" or "VCmpUF32" or "VCmpxUF32")
|
||||
{
|
||||
// The ordered/unordered predicates only test whether either
|
||||
// operand is NaN. SPIR-V's OpOrdered/OpUnordered are Kernel-only,
|
||||
// so build the same result from OpIsNan, which needs no extra
|
||||
// capability: unordered = isnan(a) || isnan(b), ordered = !that.
|
||||
var left = GetFloatSource(instruction, 0);
|
||||
var right = GetFloatSource(instruction, 1);
|
||||
var nanLeft = _module.AddInstruction(SpirvOp.IsNan, _boolType, left);
|
||||
var nanRight = _module.AddInstruction(SpirvOp.IsNan, _boolType, right);
|
||||
var unordered = _module.AddInstruction(
|
||||
SpirvOp.LogicalOr,
|
||||
_boolType,
|
||||
nanLeft,
|
||||
nanRight);
|
||||
condition = opcode is "VCmpUF32" or "VCmpxUF32"
|
||||
? unordered
|
||||
: _module.AddInstruction(SpirvOp.LogicalNot, _boolType, unordered);
|
||||
}
|
||||
else if (opcode is not ("VCmpClassF32" or "VCmpxClassF32") &&
|
||||
opcode.EndsWith("F32", StringComparison.Ordinal))
|
||||
{
|
||||
@@ -820,6 +894,7 @@ internal static partial class Gen5SpirvTranslator
|
||||
"VCmpLgF32" or "VCmpxLgF32" => SpirvOp.FOrdNotEqual,
|
||||
"VCmpGeF32" or "VCmpxGeF32" => SpirvOp.FOrdGreaterThanEqual,
|
||||
"VCmpNeqF32" or "VCmpxNeqF32" => SpirvOp.FUnordNotEqual,
|
||||
"VCmpNlgF32" or "VCmpxNlgF32" => SpirvOp.FUnordEqual,
|
||||
"VCmpNltF32" or "VCmpxNltF32" => SpirvOp.FUnordGreaterThanEqual,
|
||||
"VCmpNleF32" or "VCmpxNleF32" => SpirvOp.FUnordGreaterThan,
|
||||
"VCmpNgtF32" or "VCmpxNgtF32" => SpirvOp.FUnordLessThanEqual,
|
||||
@@ -870,7 +945,8 @@ internal static partial class Gen5SpirvTranslator
|
||||
condition = _module.AddInstruction(operation, _boolType, left, right);
|
||||
}
|
||||
|
||||
StoreWaveMask(106, condition);
|
||||
// On gfx10, VCmpx writes EXEC only and preserves VCC; the sdst
|
||||
// operand was removed from the cmpx encodings on this generation.
|
||||
if (opcode.StartsWith("VCmpx", StringComparison.Ordinal))
|
||||
{
|
||||
var active = _module.AddInstruction(
|
||||
@@ -880,6 +956,10 @@ internal static partial class Gen5SpirvTranslator
|
||||
condition);
|
||||
StoreWaveMask(126, active);
|
||||
}
|
||||
else
|
||||
{
|
||||
StoreWaveMask(106, condition);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1109,6 +1189,52 @@ internal static partial class Gen5SpirvTranslator
|
||||
left,
|
||||
right);
|
||||
break;
|
||||
case "SMulHiU32":
|
||||
{
|
||||
var wideLeft = _module.AddInstruction(
|
||||
SpirvOp.UConvert,
|
||||
_ulongType,
|
||||
left);
|
||||
var wideRight = _module.AddInstruction(
|
||||
SpirvOp.UConvert,
|
||||
_ulongType,
|
||||
right);
|
||||
var product = _module.AddInstruction(
|
||||
SpirvOp.IMul,
|
||||
_ulongType,
|
||||
wideLeft,
|
||||
wideRight);
|
||||
result = _module.AddInstruction(
|
||||
SpirvOp.UConvert,
|
||||
_uintType,
|
||||
ShiftRightLogical64(
|
||||
product,
|
||||
_module.Constant64(_ulongType, 32)));
|
||||
break;
|
||||
}
|
||||
case "SMulHiI32":
|
||||
{
|
||||
var wideLeft = _module.AddInstruction(
|
||||
SpirvOp.SConvert,
|
||||
_longType,
|
||||
Bitcast(_intType, left));
|
||||
var wideRight = _module.AddInstruction(
|
||||
SpirvOp.SConvert,
|
||||
_longType,
|
||||
Bitcast(_intType, right));
|
||||
var product = _module.AddInstruction(
|
||||
SpirvOp.IMul,
|
||||
_longType,
|
||||
wideLeft,
|
||||
wideRight);
|
||||
result = _module.AddInstruction(
|
||||
SpirvOp.UConvert,
|
||||
_uintType,
|
||||
ShiftRightLogical64(
|
||||
Bitcast(_ulongType, product),
|
||||
_module.Constant64(_ulongType, 32)));
|
||||
break;
|
||||
}
|
||||
case "SAndB32":
|
||||
result = BitwiseAnd(left, right);
|
||||
Store(_scc, IsNotZero(result));
|
||||
@@ -1230,6 +1356,43 @@ 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,
|
||||
|
||||
@@ -18,19 +18,67 @@ internal static partial class Gen5SpirvTranslator
|
||||
out string error,
|
||||
int globalBufferBase = 0,
|
||||
int totalGlobalBufferCount = -1,
|
||||
int imageBindingBase = 0)
|
||||
int imageBindingBase = 0,
|
||||
int scalarRegisterBufferIndex = -1) =>
|
||||
TryCompilePixelShader(
|
||||
state,
|
||||
evaluation,
|
||||
[new Gen5PixelOutputBinding(0, 0, outputKind)],
|
||||
out shader,
|
||||
out error,
|
||||
globalBufferBase,
|
||||
totalGlobalBufferCount,
|
||||
imageBindingBase,
|
||||
scalarRegisterBufferIndex);
|
||||
|
||||
public static bool TryCompilePixelShader(
|
||||
Gen5ShaderState state,
|
||||
Gen5ShaderEvaluation evaluation,
|
||||
IReadOnlyList<Gen5PixelOutputBinding> outputs,
|
||||
out Gen5SpirvShader shader,
|
||||
out string error,
|
||||
int globalBufferBase = 0,
|
||||
int totalGlobalBufferCount = -1,
|
||||
int imageBindingBase = 0,
|
||||
int scalarRegisterBufferIndex = -1)
|
||||
{
|
||||
if (outputs.Count > 8 || outputs.Any(output => output.GuestSlot > 7))
|
||||
{
|
||||
shader = default!;
|
||||
error = "pixel outputs must contain at most eight guest slots in the 0..7 range";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (outputs.Select(output => output.GuestSlot).Distinct().Count() != outputs.Count ||
|
||||
outputs.Select(output => output.HostLocation).Distinct().Count() != outputs.Count)
|
||||
{
|
||||
shader = default!;
|
||||
error = "pixel output guest slots and host locations must be unique";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!outputs
|
||||
.OrderBy(output => output.HostLocation)
|
||||
.Select((output, index) => output.HostLocation == (uint)index)
|
||||
.All(isDense => isDense))
|
||||
{
|
||||
shader = default!;
|
||||
error = "pixel output host locations must be dense in the 0..N-1 range";
|
||||
return false;
|
||||
}
|
||||
|
||||
var context = new CompilationContext(
|
||||
Gen5SpirvStage.Pixel,
|
||||
state,
|
||||
evaluation,
|
||||
outputKind,
|
||||
outputs,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
globalBufferBase,
|
||||
totalGlobalBufferCount,
|
||||
imageBindingBase);
|
||||
imageBindingBase,
|
||||
scalarRegisterBufferIndex);
|
||||
return context.TryCompile(out shader, out error);
|
||||
}
|
||||
|
||||
@@ -41,19 +89,21 @@ internal static partial class Gen5SpirvTranslator
|
||||
out string error,
|
||||
int globalBufferBase = 0,
|
||||
int totalGlobalBufferCount = -1,
|
||||
int imageBindingBase = 0)
|
||||
int imageBindingBase = 0,
|
||||
int scalarRegisterBufferIndex = -1)
|
||||
{
|
||||
var context = new CompilationContext(
|
||||
Gen5SpirvStage.Vertex,
|
||||
state,
|
||||
evaluation,
|
||||
Gen5PixelOutputKind.Float,
|
||||
[],
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
globalBufferBase,
|
||||
totalGlobalBufferCount,
|
||||
imageBindingBase);
|
||||
imageBindingBase,
|
||||
scalarRegisterBufferIndex);
|
||||
return context.TryCompile(out shader, out error);
|
||||
}
|
||||
|
||||
@@ -70,13 +120,14 @@ internal static partial class Gen5SpirvTranslator
|
||||
Gen5SpirvStage.Compute,
|
||||
state,
|
||||
evaluation,
|
||||
Gen5PixelOutputKind.Float,
|
||||
[],
|
||||
Math.Max(localSizeX, 1),
|
||||
Math.Max(localSizeY, 1),
|
||||
Math.Max(localSizeZ, 1),
|
||||
0,
|
||||
-1,
|
||||
0);
|
||||
0,
|
||||
-1);
|
||||
return context.TryCompile(out shader, out error);
|
||||
}
|
||||
|
||||
@@ -86,15 +137,17 @@ internal static partial class Gen5SpirvTranslator
|
||||
private readonly Gen5SpirvStage _stage;
|
||||
private readonly Gen5ShaderState _state;
|
||||
private readonly Gen5ShaderEvaluation _evaluation;
|
||||
private readonly Gen5PixelOutputKind _outputKind;
|
||||
private readonly IReadOnlyList<Gen5PixelOutputBinding> _pixelOutputBindings;
|
||||
private readonly uint _localSizeX;
|
||||
private readonly uint _localSizeY;
|
||||
private readonly uint _localSizeZ;
|
||||
private readonly int _globalBufferBase;
|
||||
private readonly int _totalGlobalBufferCount;
|
||||
private readonly int _imageBindingBase;
|
||||
private readonly int _scalarRegisterBufferIndex;
|
||||
private readonly List<uint> _interfaces = [];
|
||||
private readonly Dictionary<uint, uint> _pixelInputs = [];
|
||||
private readonly Dictionary<uint, SpirvPixelOutput> _pixelOutputs = [];
|
||||
private readonly Dictionary<uint, uint> _vertexOutputs = [];
|
||||
private readonly Dictionary<uint, SpirvVertexInput> _vertexInputsByPc = [];
|
||||
private readonly List<SpirvImageResource> _imageResources = [];
|
||||
@@ -127,7 +180,6 @@ internal static partial class Gen5SpirvTranslator
|
||||
private uint _lds;
|
||||
private uint _workgroupUintPointer;
|
||||
private uint _positionOutput;
|
||||
private uint _pixelOutput;
|
||||
private uint _vertexIndexInput;
|
||||
private uint _instanceIndexInput;
|
||||
private uint _fragCoordInput;
|
||||
@@ -157,22 +209,28 @@ internal static partial class Gen5SpirvTranslator
|
||||
uint Type,
|
||||
uint ComponentCount);
|
||||
|
||||
private readonly record struct SpirvPixelOutput(
|
||||
uint Variable,
|
||||
uint Type,
|
||||
Gen5PixelOutputKind Kind);
|
||||
|
||||
public CompilationContext(
|
||||
Gen5SpirvStage stage,
|
||||
Gen5ShaderState state,
|
||||
Gen5ShaderEvaluation evaluation,
|
||||
Gen5PixelOutputKind outputKind,
|
||||
IReadOnlyList<Gen5PixelOutputBinding> pixelOutputBindings,
|
||||
uint localSizeX,
|
||||
uint localSizeY,
|
||||
uint localSizeZ,
|
||||
int globalBufferBase,
|
||||
int totalGlobalBufferCount,
|
||||
int imageBindingBase)
|
||||
int imageBindingBase,
|
||||
int scalarRegisterBufferIndex)
|
||||
{
|
||||
_stage = stage;
|
||||
_state = state;
|
||||
_evaluation = evaluation;
|
||||
_outputKind = outputKind;
|
||||
_pixelOutputBindings = pixelOutputBindings;
|
||||
_localSizeX = localSizeX;
|
||||
_localSizeY = localSizeY;
|
||||
_localSizeZ = localSizeZ;
|
||||
@@ -181,6 +239,7 @@ internal static partial class Gen5SpirvTranslator
|
||||
? evaluation.GlobalMemoryBindings.Count
|
||||
: totalGlobalBufferCount;
|
||||
_imageBindingBase = imageBindingBase;
|
||||
_scalarRegisterBufferIndex = scalarRegisterBufferIndex;
|
||||
}
|
||||
|
||||
public bool TryCompile(out Gen5SpirvShader shader, out string error)
|
||||
@@ -692,19 +751,24 @@ internal static partial class Gen5SpirvTranslator
|
||||
(uint)SpirvBuiltIn.FragCoord);
|
||||
_interfaces.Add(_fragCoordInput);
|
||||
|
||||
var outputType = _outputKind switch
|
||||
foreach (var binding in _pixelOutputBindings)
|
||||
{
|
||||
Gen5PixelOutputKind.Uint => _uvec4Type,
|
||||
Gen5PixelOutputKind.Sint => _module.TypeVector(_intType, 4),
|
||||
_ => _vec4Type,
|
||||
};
|
||||
var outputPointer =
|
||||
_module.TypePointer(SpirvStorageClass.Output, outputType);
|
||||
_pixelOutput = _module.AddGlobalVariable(
|
||||
outputPointer,
|
||||
SpirvStorageClass.Output);
|
||||
_module.AddDecoration(_pixelOutput, SpirvDecoration.Location, 0);
|
||||
_interfaces.Add(_pixelOutput);
|
||||
var outputType = GetPixelOutputType(binding.Kind);
|
||||
var outputPointer =
|
||||
_module.TypePointer(SpirvStorageClass.Output, outputType);
|
||||
var variable = _module.AddGlobalVariable(
|
||||
outputPointer,
|
||||
SpirvStorageClass.Output);
|
||||
_module.AddName(variable, $"mrt{binding.GuestSlot}");
|
||||
_module.AddDecoration(
|
||||
variable,
|
||||
SpirvDecoration.Location,
|
||||
binding.HostLocation);
|
||||
_pixelOutputs.Add(
|
||||
binding.GuestSlot,
|
||||
new SpirvPixelOutput(variable, outputType, binding.Kind));
|
||||
_interfaces.Add(variable);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -767,15 +831,25 @@ internal static partial class Gen5SpirvTranslator
|
||||
|
||||
private void EmitInitialState()
|
||||
{
|
||||
for (uint index = 0;
|
||||
index < _evaluation.InitialScalarRegisters.Count &&
|
||||
index < ScalarRegisterCount;
|
||||
index++)
|
||||
if (_scalarRegisterBufferIndex >= 0)
|
||||
{
|
||||
var value = _evaluation.InitialScalarRegisters[(int)index];
|
||||
if (value != 0)
|
||||
for (uint index = 0; index < ScalarRegisterCount; index++)
|
||||
{
|
||||
StoreS(index, UInt(value));
|
||||
StoreS(index, LoadBufferWord(_scalarRegisterBufferIndex, UInt(index)));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (uint index = 0;
|
||||
index < _evaluation.InitialScalarRegisters.Count &&
|
||||
index < ScalarRegisterCount;
|
||||
index++)
|
||||
{
|
||||
var value = _evaluation.InitialScalarRegisters[(int)index];
|
||||
if (value != 0)
|
||||
{
|
||||
StoreS(index, UInt(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -813,7 +887,10 @@ internal static partial class Gen5SpirvTranslator
|
||||
1);
|
||||
StoreV(2, Bitcast(_uintType, x), guardWithExec: false);
|
||||
StoreV(3, Bitcast(_uintType, y), guardWithExec: false);
|
||||
Store(_pixelOutput, _module.ConstantNull(GetPixelOutputType()));
|
||||
foreach (var output in _pixelOutputs.Values)
|
||||
{
|
||||
Store(output.Variable, _module.ConstantNull(output.Type));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2066,21 +2143,27 @@ internal static partial class Gen5SpirvTranslator
|
||||
|
||||
if (_stage == Gen5SpirvStage.Pixel)
|
||||
{
|
||||
if (export.Target != 0)
|
||||
if (!_pixelOutputs.TryGetValue(export.Target, out var output))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var outputType = GetPixelOutputType();
|
||||
var values = new uint[4];
|
||||
for (var component = 0; component < 4; component++)
|
||||
{
|
||||
var enabled = (export.EnableMask & (1u << component)) != 0;
|
||||
if (!enabled)
|
||||
{
|
||||
values[component] = _outputKind == Gen5PixelOutputKind.Sint
|
||||
? Bitcast(_intType, UInt(0))
|
||||
: UInt(0);
|
||||
values[component] = _module.AddInstruction(
|
||||
SpirvOp.CompositeExtract,
|
||||
output.Kind switch
|
||||
{
|
||||
Gen5PixelOutputKind.Uint => _uintType,
|
||||
Gen5PixelOutputKind.Sint => _intType,
|
||||
_ => _floatType,
|
||||
},
|
||||
Load(output.Type, output.Variable),
|
||||
(uint)component);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -2089,7 +2172,7 @@ internal static partial class Gen5SpirvTranslator
|
||||
var value = LoadCompressedExportComponent(
|
||||
instruction,
|
||||
component);
|
||||
values[component] = _outputKind switch
|
||||
values[component] = output.Kind switch
|
||||
{
|
||||
Gen5PixelOutputKind.Uint => _module.AddInstruction(
|
||||
SpirvOp.ConvertFToU,
|
||||
@@ -2105,7 +2188,7 @@ internal static partial class Gen5SpirvTranslator
|
||||
}
|
||||
|
||||
var raw = LoadV(instruction.Sources[component].Value);
|
||||
values[component] = _outputKind switch
|
||||
values[component] = output.Kind switch
|
||||
{
|
||||
Gen5PixelOutputKind.Uint => raw,
|
||||
Gen5PixelOutputKind.Sint => Bitcast(_intType, raw),
|
||||
@@ -2115,15 +2198,15 @@ internal static partial class Gen5SpirvTranslator
|
||||
|
||||
var vector = _module.AddInstruction(
|
||||
SpirvOp.CompositeConstruct,
|
||||
outputType,
|
||||
output.Type,
|
||||
values);
|
||||
vector = _module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
outputType,
|
||||
output.Type,
|
||||
Load(_boolType, _exec),
|
||||
vector,
|
||||
Load(outputType, _pixelOutput));
|
||||
Store(_pixelOutput, vector);
|
||||
Load(output.Type, output.Variable));
|
||||
Store(output.Variable, vector);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2191,8 +2274,8 @@ internal static partial class Gen5SpirvTranslator
|
||||
(uint)(component & 1));
|
||||
}
|
||||
|
||||
private uint GetPixelOutputType() =>
|
||||
_outputKind switch
|
||||
private uint GetPixelOutputType(Gen5PixelOutputKind kind) =>
|
||||
kind switch
|
||||
{
|
||||
Gen5PixelOutputKind.Uint => _uvec4Type,
|
||||
Gen5PixelOutputKind.Sint => _module.TypeVector(_intType, 4),
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@@ -348,6 +348,18 @@ public static class AmprExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Zi3dBUjgyXI",
|
||||
ExportName = "sceAmprMeasureCommandSizeWriteKernelEventQueueOnCompletion",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAmpr")]
|
||||
public static int MeasureCommandSizeWriteKernelEventQueueOnCompletion(CpuContext ctx)
|
||||
{
|
||||
TraceAmpr(ctx, "measure_write_equeue_complete", 0, KernelEventQueueRecordSize, 0);
|
||||
ctx[CpuRegister.Rax] = KernelEventQueueRecordSize;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "C+IEj+BsAFM",
|
||||
ExportName = "sceAmprMeasureCommandSizeWriteAddressOnCompletion",
|
||||
@@ -440,6 +452,40 @@ public static class AmprExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "o67gODLFpls",
|
||||
ExportName = "sceAmprCommandBufferWriteKernelEventQueueOnCompletion",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAmpr")]
|
||||
public static int CommandBufferWriteKernelEventQueueOnCompletion(CpuContext ctx)
|
||||
{
|
||||
var commandBuffer = ctx[CpuRegister.Rdi];
|
||||
var equeue = ctx[CpuRegister.Rsi];
|
||||
var ident = ctx[CpuRegister.Rdx];
|
||||
var completionToken = ctx[CpuRegister.Rcx];
|
||||
var userData = ctx[CpuRegister.R8];
|
||||
|
||||
if (commandBuffer == 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (!AppendKernelEventQueueRecord(
|
||||
ctx,
|
||||
commandBuffer,
|
||||
equeue,
|
||||
ident,
|
||||
completionToken,
|
||||
userData))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
TraceAmpr(ctx, "write_equeue_complete", commandBuffer, ident, completionToken);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "sJXyWHjP-F8",
|
||||
ExportName = "sceAmprCommandBufferWriteAddressOnCompletion",
|
||||
|
||||
@@ -21,7 +21,7 @@ internal static class AmprFileRegistry
|
||||
return _hostPathsById.TryGetValue(id, out hostPath!);
|
||||
}
|
||||
|
||||
private static uint ComputeFileId(string guestPath)
|
||||
internal static uint ComputeFileId(string guestPath)
|
||||
{
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes(guestPath);
|
||||
|
||||
|
||||
@@ -54,20 +54,41 @@ internal static class PakDirectoryTracker
|
||||
|
||||
if (state.Directory is { } entries)
|
||||
{
|
||||
// Several unrelated archived files can share a byte count (e.g. a head model and a bot
|
||||
// navigation file both 0x3A34 bytes). Directory order alone then picks whichever comes
|
||||
// first, which is wrong when the game reads them out of order. id archives cluster
|
||||
// related assets, and the guest streams them with locality, so disambiguate collisions
|
||||
// by choosing the unconsumed match nearest the running read cursor.
|
||||
DirectoryEntry? best = null;
|
||||
var bestDistance = ulong.MaxValue;
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (!entry.Consumed && entry.FileLen == requestedSize)
|
||||
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;
|
||||
continue;
|
||||
}
|
||||
|
||||
var distance = entry.FilePos >= state.NextOffset
|
||||
? entry.FilePos - state.NextOffset
|
||||
: state.NextOffset - entry.FilePos;
|
||||
if (distance < bestDistance)
|
||||
{
|
||||
bestDistance = distance;
|
||||
best = entry;
|
||||
}
|
||||
}
|
||||
|
||||
if (best is not null)
|
||||
{
|
||||
best.Consumed = true;
|
||||
if (_trace)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] pak.directory_match: id=0x{fileId:X8} name='{best.Name}' " +
|
||||
$"filepos=0x{best.FilePos:X8} filelen=0x{best.FileLen:X8}");
|
||||
}
|
||||
|
||||
return best.FilePos;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,9 +8,14 @@ namespace SharpEmu.Libs.Audio;
|
||||
|
||||
public static class AudioOut2Exports
|
||||
{
|
||||
private const int AudioOut2ContextParamSize = 0x80;
|
||||
// Sized from guest evidence, not SDK headers: Quake keeps its
|
||||
// SceAudioOut2ContextParam on the stack with the frame canary at param+0x60,
|
||||
// and an earlier 0x80-byte ResetParam write zeroed that canary
|
||||
// (__stack_chk_fail right after sceAudioOut2UserCreate, which silently killed
|
||||
// the whole audio init). Stay well below 0x60 and only write the prefix we
|
||||
// populate.
|
||||
private const int AudioOut2ContextParamSize = 0x30;
|
||||
private const int AudioOut2ContextMemorySize = 0x10000;
|
||||
private const int AudioOut2ContextMemoryAlignment = 0x10000;
|
||||
private static long _nextContextHandle = 1;
|
||||
private static long _nextUserHandle = 1;
|
||||
private static int _nextPortId;
|
||||
@@ -59,20 +64,13 @@ public static class AudioOut2Exports
|
||||
public static int AudioOut2ContextQueryMemory(CpuContext ctx)
|
||||
{
|
||||
var paramAddress = ctx[CpuRegister.Rdi];
|
||||
var memoryInfoAddress = ctx[CpuRegister.Rsi];
|
||||
if (paramAddress == 0 || memoryInfoAddress == 0)
|
||||
var outMemorySizeAddress = ctx[CpuRegister.Rsi];
|
||||
if (paramAddress == 0 || outMemorySizeAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> memoryInfo = stackalloc byte[0x20];
|
||||
memoryInfo.Clear();
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x00..], AudioOut2ContextMemorySize);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x08..], AudioOut2ContextMemoryAlignment);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x10..], AudioOut2ContextMemorySize);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x18..], AudioOut2ContextMemoryAlignment);
|
||||
|
||||
return ctx.Memory.TryWrite(memoryInfoAddress, memoryInfo)
|
||||
|
||||
return ctx.TryWriteUInt64(outMemorySizeAddress, AudioOut2ContextMemorySize)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
@@ -204,7 +202,7 @@ public static class AudioOut2Exports
|
||||
{
|
||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var outUserAddress = ctx[CpuRegister.Rsi];
|
||||
if ((userId != 0 && userId != 1 && userId != 255) || outUserAddress == 0)
|
||||
if ((userId != 0 && userId != 1 && userId != 1000 && userId != 255) || outUserAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
@@ -213,6 +213,17 @@ public static class AudioOutExports
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
public static void ShutdownAllPorts()
|
||||
{
|
||||
foreach (var handle in Ports.Keys)
|
||||
{
|
||||
if (Ports.TryRemove(handle, out var port))
|
||||
{
|
||||
port.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetFormat(
|
||||
int rawFormat,
|
||||
out int channels,
|
||||
|
||||
@@ -35,6 +35,17 @@ public static class AvPlayerExports
|
||||
return unchecked((int)handle);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "KMcEa+rHsIo",
|
||||
ExportName = "sceAvPlayerAddSource",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAvPlayer")]
|
||||
public static int AvPlayerAddSource(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "JdksQu8pNdQ",
|
||||
ExportName = "sceAvPlayerGetVideoDataEx",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.CommonDialog;
|
||||
@@ -10,10 +11,21 @@ public static class MsgDialogExports
|
||||
{
|
||||
private const int StatusNone = 0;
|
||||
private const int StatusInitialized = 1;
|
||||
private const int StatusRunning = 2;
|
||||
private const int StatusFinished = 3;
|
||||
private const int ResultSize = 0x20;
|
||||
|
||||
private static int _initialized;
|
||||
private const int ErrorOk = 0;
|
||||
private const int ErrorNotInitialized = unchecked((int)0x80B80003);
|
||||
private const int ErrorNotFinished = unchecked((int)0x80B80005);
|
||||
private const int ErrorBusy = unchecked((int)0x80B80007);
|
||||
private const int ErrorNotRunning = unchecked((int)0x80B8000B);
|
||||
private const int ErrorArgNull = unchecked((int)0x80B8000D);
|
||||
|
||||
// Result buffer layout follows the common dialog convention: mode at +0x00,
|
||||
// result at +0x04, buttonId at +0x08. The affirmative button (OK/YES) is 1.
|
||||
private const int ResultSize = 0x20;
|
||||
private const int ButtonIdAffirmative = 1;
|
||||
|
||||
private static int _status;
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -24,11 +36,10 @@ public static class MsgDialogExports
|
||||
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;
|
||||
// this HLE implementation and has no per-call resources to recreate. Only promote
|
||||
// from NONE so re-initializing mid-flow cannot clobber a running/finished dialog.
|
||||
Interlocked.CompareExchange(ref _status, StatusInitialized, StatusNone);
|
||||
return ctx.SetReturn(ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -38,9 +49,12 @@ public static class MsgDialogExports
|
||||
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);
|
||||
if (Interlocked.Exchange(ref _status, StatusNone) == StatusNone)
|
||||
{
|
||||
return ctx.SetReturn(ErrorNotInitialized);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -50,13 +64,29 @@ public static class MsgDialogExports
|
||||
LibraryName = "libSceMsgDialog")]
|
||||
public static int MsgDialogOpen(CpuContext ctx)
|
||||
{
|
||||
LogDialogMessage(ctx, ctx[CpuRegister.Rdi]);
|
||||
var paramAddress = ctx[CpuRegister.Rdi];
|
||||
if (paramAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(ErrorArgNull);
|
||||
}
|
||||
|
||||
// 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);
|
||||
var status = Volatile.Read(ref _status);
|
||||
if (status == StatusNone)
|
||||
{
|
||||
return ctx.SetReturn(ErrorNotInitialized);
|
||||
}
|
||||
|
||||
if (status == StatusRunning)
|
||||
{
|
||||
return ctx.SetReturn(ErrorBusy);
|
||||
}
|
||||
|
||||
LogDialogMessage(ctx, paramAddress);
|
||||
|
||||
// There is no host popup to actually show. Enter RUNNING so close/cancel paths see
|
||||
// a live dialog; the guest's next status poll auto-dismisses it (see PollStatus).
|
||||
Interlocked.Exchange(ref _status, StatusRunning);
|
||||
return ctx.SetReturn(ErrorOk);
|
||||
}
|
||||
|
||||
// Best-effort extraction of the dialog text so fatal-error popups are visible in the
|
||||
@@ -65,12 +95,6 @@ public static class MsgDialogExports
|
||||
// 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];
|
||||
@@ -134,14 +158,24 @@ public static class MsgDialogExports
|
||||
ExportName = "sceMsgDialogGetStatus",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceMsgDialog")]
|
||||
public static int MsgDialogGetStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
|
||||
public static int MsgDialogGetStatus(CpuContext ctx) => ctx.SetReturn(PollStatus());
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "6fIC3XKt2k0",
|
||||
ExportName = "sceMsgDialogUpdateStatus",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceMsgDialog")]
|
||||
public static int MsgDialogUpdateStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
|
||||
public static int MsgDialogUpdateStatus(CpuContext ctx) => ctx.SetReturn(PollStatus());
|
||||
|
||||
// With no host UI the dialog cannot wait for user input: the first status poll after
|
||||
// Open observes the dialog as already dismissed. Advancing on both UpdateStatus and
|
||||
// GetStatus keeps every guest polling pattern free of infinite RUNNING loops, while
|
||||
// an Open -> Close sequence with no poll in between still exercises the close path.
|
||||
private static int PollStatus()
|
||||
{
|
||||
Interlocked.CompareExchange(ref _status, StatusFinished, StatusRunning);
|
||||
return Volatile.Read(ref _status);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Lr8ovHH9l6A",
|
||||
@@ -153,17 +187,27 @@ public static class MsgDialogExports
|
||||
var resultAddress = ctx[CpuRegister.Rdi];
|
||||
if (resultAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn(ErrorArgNull);
|
||||
}
|
||||
|
||||
if (Volatile.Read(ref _status) != StatusFinished)
|
||||
{
|
||||
return ctx.SetReturn(ErrorNotFinished);
|
||||
}
|
||||
|
||||
// Report the affirmative button so yes/no prompts take the confirming branch;
|
||||
// buttonId 0 is the "invalid" sentinel and games may treat it as an error.
|
||||
Span<byte> result = stackalloc byte[ResultSize];
|
||||
result.Clear();
|
||||
BinaryPrimitives.WriteInt32LittleEndian(result[0x04..], 0);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(result[0x08..], ButtonIdAffirmative);
|
||||
|
||||
if (!ctx.Memory.TryWrite(resultAddress, result))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -173,13 +217,44 @@ public static class MsgDialogExports
|
||||
LibraryName = "libSceMsgDialog")]
|
||||
public static int MsgDialogClose(CpuContext ctx)
|
||||
{
|
||||
Interlocked.Exchange(ref _status, StatusFinished);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
if (Interlocked.CompareExchange(ref _status, StatusFinished, StatusRunning) != StatusRunning)
|
||||
{
|
||||
return ctx.SetReturn(ErrorNotRunning);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(ErrorOk);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
[SysAbiExport(
|
||||
Nid = "wTpfglkmv34",
|
||||
ExportName = "sceMsgDialogProgressBarSetValue",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceMsgDialog")]
|
||||
public static int MsgDialogProgressBarSetValue(CpuContext ctx) => ProgressBarNoOp(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Gc5k1qcK4fs",
|
||||
ExportName = "sceMsgDialogProgressBarInc",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceMsgDialog")]
|
||||
public static int MsgDialogProgressBarInc(CpuContext ctx) => ProgressBarNoOp(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "6H-71OdrpXM",
|
||||
ExportName = "sceMsgDialogProgressBarSetMsg",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceMsgDialog")]
|
||||
public static int MsgDialogProgressBarSetMsg(CpuContext ctx) => ProgressBarNoOp(ctx);
|
||||
|
||||
// There is no visible bar to update. Accept the call whenever the service is alive so
|
||||
// save/install loops that report progress do not abort on an unexpected error.
|
||||
private static int ProgressBarNoOp(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
if (Volatile.Read(ref _status) == StatusNone)
|
||||
{
|
||||
return ctx.SetReturn(ErrorNotInitialized);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(ErrorOk);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.DiscMap;
|
||||
|
||||
/// <summary>
|
||||
/// HLE implementation of libSceDiscMap, ported from Kyty's LibDiscMap.cpp
|
||||
/// (https://github.com/InoriRus/Kyty, MIT). Titles installed to internal
|
||||
/// storage query these entry points on nearly every file access to decide
|
||||
/// whether a read must be redirected to the Blu-ray drive. Reporting every
|
||||
/// request as already resident on HDD lets the regular file system path
|
||||
/// service the read.
|
||||
/// </summary>
|
||||
public static class DiscMapExports
|
||||
{
|
||||
private const int DiscMapErrorInvalidArgument = unchecked((int)0x81100001);
|
||||
private const int DiscMapErrorLocationNotMapped = unchecked((int)0x81100002);
|
||||
private const int DiscMapErrorFileNotFound = unchecked((int)0x81100003);
|
||||
private const int DiscMapErrorNoBitmapInfo = unchecked((int)0x81100004);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "lbQKqsERhtE",
|
||||
ExportName = "sceDiscMapIsRequestOnHDD",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceDiscMap")]
|
||||
public static int DiscMapIsRequestOnHDD(CpuContext ctx)
|
||||
{
|
||||
var pathAddress = ctx[CpuRegister.Rdi];
|
||||
var offset = ctx[CpuRegister.Rsi];
|
||||
var size = ctx[CpuRegister.Rdx];
|
||||
var resultAddress = ctx[CpuRegister.Rcx];
|
||||
|
||||
if (pathAddress == 0 || resultAddress == 0)
|
||||
{
|
||||
return DiscMapErrorInvalidArgument;
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteInt32(resultAddress, 1))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
TraceDiscMap(ctx, "sceDiscMapIsRequestOnHDD", pathAddress, offset, size);
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "fJgP+wqifno",
|
||||
ExportName = "sceDiscMapUnknownFJgP",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceDiscMap")]
|
||||
public static int DiscMapUnknownFJgP(CpuContext ctx) => WriteMappingTriple(ctx, "sceDiscMapUnknownFJgP");
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "ioKMruft1ek",
|
||||
ExportName = "sceDiscMapUnknownIoKM",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceDiscMap")]
|
||||
public static int DiscMapUnknownIoKM(CpuContext ctx) => WriteMappingTriple(ctx, "sceDiscMapUnknownIoKM");
|
||||
|
||||
private static int WriteMappingTriple(CpuContext ctx, string exportName)
|
||||
{
|
||||
var pathAddress = ctx[CpuRegister.Rdi];
|
||||
var offset = ctx[CpuRegister.Rsi];
|
||||
var size = ctx[CpuRegister.Rdx];
|
||||
var flagsAddress = ctx[CpuRegister.Rcx];
|
||||
var outAddress1 = ctx[CpuRegister.R8];
|
||||
var outAddress2 = ctx[CpuRegister.R9];
|
||||
|
||||
if (pathAddress == 0 || flagsAddress == 0 || outAddress1 == 0 || outAddress2 == 0)
|
||||
{
|
||||
return DiscMapErrorInvalidArgument;
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt64(flagsAddress, 0) ||
|
||||
!ctx.TryWriteUInt64(outAddress1, 0) ||
|
||||
!ctx.TryWriteUInt64(outAddress2, 0))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
TraceDiscMap(ctx, exportName, pathAddress, offset, size);
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static void TraceDiscMap(CpuContext ctx, string exportName, ulong pathAddress, ulong offset, ulong size)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_DISCMAP"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var path = ctx.TryReadNullTerminatedUtf8(pathAddress, 1024, out var value)
|
||||
? value
|
||||
: $"<unreadable 0x{pathAddress:X16}>";
|
||||
Console.Error.WriteLine($"[HLE][DISCMAP] {exportName} path={path} offset=0x{offset:X} size=0x{size:X}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using 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);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Ime;
|
||||
|
||||
public static class ImeExports
|
||||
{
|
||||
// Quake (KEX) calls this from its main loop and from the audio bring-up path with
|
||||
// an event-handler pointer. No IME session ever exists here, so report success
|
||||
// without invoking the handler ("no pending IME events"). This NID was previously
|
||||
// misbound as an sceNgs2VoiceControl alias, which fed the game NGS2 errors.
|
||||
[SysAbiExport(
|
||||
Nid = "-4GCfYdNF1s",
|
||||
ExportName = "sceImeUpdate",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceIme")]
|
||||
public static int ImeUpdate(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "eaFXjfJv3xs",
|
||||
ExportName = "sceImeKeyboardOpen",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceIme")]
|
||||
public static int ImeKeyboardOpen(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "dKadqZFgKKQ",
|
||||
ExportName = "sceImeKeyboardGetResourceId",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceIme")]
|
||||
public static int ImeKeyboardGetResourceId(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
}
|
||||
@@ -78,6 +78,82 @@ public static class JsonExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// sce::Json::Initializer::setGlobalNullAccessCallback(const Value& (*)(ValueType, const Value*, void*), void*)
|
||||
// Registers the guest hook invoked when a Value is accessed as the wrong type. Quake calls it
|
||||
// during kexPSNWebAPI::Initialize and treats a non-zero return as a fatal init failure.
|
||||
[SysAbiExport(
|
||||
Nid = "+drDFyAS6u4",
|
||||
ExportName = "_ZN3sce4Json11Initializer27setGlobalNullAccessCallbackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceJson")]
|
||||
public static int InitializerSetGlobalNullAccessCallback(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
if (thisAddress == 0)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
JsonObjectHeap.GlobalNullAccessCallback = ctx[CpuRegister.Rsi];
|
||||
JsonObjectHeap.GlobalNullAccessCallbackContext = ctx[CpuRegister.Rdx];
|
||||
TraceJson("Initializer.setGlobalNullAccessCallback", thisAddress, ctx[CpuRegister.Rsi]);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
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))
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Json;
|
||||
|
||||
// sce::Json::Value and sce::Json::String constructors, setters and destructors. Prospero titles
|
||||
// (Quake among them) build a Value tree and populate it through these before serializing it for a
|
||||
// web request; without them the imports resolve to nothing and the guest faults on the call. The
|
||||
// payload is modelled in JsonObjectHeap; here we only translate the C++ ABI (registers in, `this`
|
||||
// back out) and never write the guest object, so a stack-allocated Value/String is left intact.
|
||||
//
|
||||
// Only the complete-object variants (C1/D1) are bound — those are what standalone locals emit and
|
||||
// what the observed Prospero imports use. The base-object variants (C2/D2) are left for a title
|
||||
// that actually imports them.
|
||||
public static class JsonValueExports
|
||||
{
|
||||
private const int MaxStringLength = 0x10000;
|
||||
|
||||
private static int ReturnThis(CpuContext ctx, ulong thisAddress)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = thisAddress;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static int ReturnVoid(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static double ReadDoubleArg(CpuContext ctx)
|
||||
{
|
||||
ctx.GetXmmRegister(0, out var low, out _);
|
||||
return BitConverter.Int64BitsToDouble(unchecked((long)low));
|
||||
}
|
||||
|
||||
private static string ReadCString(CpuContext ctx, ulong address) =>
|
||||
ctx.TryReadNullTerminatedUtf8(address, MaxStringLength, out var text) ? text : string.Empty;
|
||||
|
||||
// ---- sce::Json::Value constructors ----
|
||||
|
||||
[SysAbiExport(Nid = "qBMjqyBn3OM", ExportName = "_ZN3sce4Json5ValueC1Ev",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueDefaultConstructor(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.Null);
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "UeuWT+yNdCQ", ExportName = "_ZN3sce4Json5ValueC1Eb",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueBooleanConstructor(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromBoolean((ctx[CpuRegister.Rsi] & 0xFF) != 0));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "0lLK8+kDqmE", ExportName = "_ZN3sce4Json5ValueC1El",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueIntegerConstructor(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromInteger(unchecked((long)ctx[CpuRegister.Rsi])));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "x4AUdbhpRB0", ExportName = "_ZN3sce4Json5ValueC1Em",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueUnsignedConstructor(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromUnsignedInteger(ctx[CpuRegister.Rsi]));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "sOmU4vnx3s0", ExportName = "_ZN3sce4Json5ValueC1Ed",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueRealConstructor(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromReal(ReadDoubleArg(ctx)));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "b9V6fmppLXY", ExportName = "_ZN3sce4Json5ValueC1EPKc",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueCStringConstructor(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromString(ReadCString(ctx, ctx[CpuRegister.Rsi])));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "CbrT3dwDILo", ExportName = "_ZN3sce4Json5ValueC1ENS0_9ValueTypeE",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueTypeConstructor(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromExplicitType(unchecked((uint)ctx[CpuRegister.Rsi])));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "sZIoMRGO+jk", ExportName = "_ZN3sce4Json5ValueC1ERKNS0_6StringE",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueStringConstructor(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromString(JsonObjectHeap.GetStringOrEmpty(ctx[CpuRegister.Rsi])));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "WTtYf+cNnXI", ExportName = "_ZN3sce4Json5ValueD1Ev",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueDestructor(CpuContext ctx)
|
||||
{
|
||||
JsonObjectHeap.RemoveValue(ctx[CpuRegister.Rdi]);
|
||||
return ReturnVoid(ctx);
|
||||
}
|
||||
|
||||
// ---- sce::Json::Value setters (return Value&, i.e. `this`) ----
|
||||
|
||||
[SysAbiExport(Nid = "5yHuiWXo2gg", ExportName = "_ZN3sce4Json5Value3setEb",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueSetBoolean(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromBoolean((ctx[CpuRegister.Rsi] & 0xFF) != 0));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "QxVVYhP-mvg", ExportName = "_ZN3sce4Json5Value3setEl",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueSetInteger(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromInteger(unchecked((long)ctx[CpuRegister.Rsi])));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "SIe1ZmW7e7s", ExportName = "_ZN3sce4Json5Value3setEm",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueSetUnsigned(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromUnsignedInteger(ctx[CpuRegister.Rsi]));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "BSmWDIkV4w4", ExportName = "_ZN3sce4Json5Value3setEd",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueSetReal(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromReal(ReadDoubleArg(ctx)));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "IKQimvG9Wqs", ExportName = "_ZN3sce4Json5Value3setENS0_9ValueTypeE",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueSetType(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromExplicitType(unchecked((uint)ctx[CpuRegister.Rsi])));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "n6FC+l9DU70", ExportName = "_ZN3sce4Json5Value3setEPKc",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueSetCString(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromString(ReadCString(ctx, ctx[CpuRegister.Rsi])));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "6l3Bv2gysNc", ExportName = "_ZN3sce4Json5Value3setERKNS0_6StringE",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueSetString(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromString(JsonObjectHeap.GetStringOrEmpty(ctx[CpuRegister.Rsi])));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "FIjXN2TkuTs", ExportName = "_ZN3sce4Json5Value5clearEv",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueClear(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.Null);
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
// ---- sce::Json::String ----
|
||||
|
||||
[SysAbiExport(Nid = "9KUZFjI1IxA", ExportName = "_ZN3sce4Json6StringC1EPKc",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int StringCStringConstructor(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetString(thisAddress, ReadCString(ctx, ctx[CpuRegister.Rsi]));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "qSmqLXXCPas", ExportName = "_ZN3sce4Json6StringC1Ev",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int StringDefaultConstructor(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetString(thisAddress, string.Empty);
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "0CAesfH963Q", ExportName = "_ZN3sce4Json6StringC1ERKS1_",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int StringCopyConstructor(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetString(thisAddress, JsonObjectHeap.GetStringOrEmpty(ctx[CpuRegister.Rsi]));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "cG1VE2HMl6c", ExportName = "_ZN3sce4Json6StringD1Ev",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int StringDestructor(CpuContext ctx)
|
||||
{
|
||||
JsonObjectHeap.RemoveString(ctx[CpuRegister.Rdi]);
|
||||
return ReturnVoid(ctx);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user