mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ad9836d13 | |||
| 62e1775c5c | |||
| 6dacd59a08 | |||
| 9d88542efd | |||
| 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 |
@@ -100,6 +100,9 @@ jobs:
|
|||||||
- name: Build solution
|
- name: Build solution
|
||||||
run: dotnet build SharpEmu.slnx -c Release --no-restore
|
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
|
- 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}"
|
run: dotnet publish src/SharpEmu.CLI/SharpEmu.CLI.csproj -c Release -r win-x64 --self-contained true --no-restore -p:PublishDir="${env:PUBLISH_DIR}"
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,14 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.3.18" />
|
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.3.18" />
|
||||||
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.3.18" />
|
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.3.18" />
|
||||||
<PackageVersion Include="Iced" Version="1.21.0" />
|
<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" Version="2.23.0" />
|
||||||
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" 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.Vulkan.Extensions.KHR" Version="2.23.0" />
|
||||||
<PackageVersion Include="Silk.NET.Windowing" Version="2.23.0" />
|
<PackageVersion Include="Silk.NET.Windowing" Version="2.23.0" />
|
||||||
<!-- Transitive of Avalonia.Desktop; pinned to fix GHSA-xrw6-gwf8-vvr9 -->
|
<!-- Transitive of Avalonia.Desktop; pinned to fix GHSA-xrw6-gwf8-vvr9 -->
|
||||||
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.21.3" />
|
<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>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -12,4 +12,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<Project Path="src/SharpEmu.Libs/SharpEmu.Libs.csproj" />
|
<Project Path="src/SharpEmu.Libs/SharpEmu.Libs.csproj" />
|
||||||
<Project Path="src/SharpEmu.Logging/SharpEmu.Logging.csproj" />
|
<Project Path="src/SharpEmu.Logging/SharpEmu.Logging.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
|
<Folder Name="/tests/">
|
||||||
|
<Project Path="tests/SharpEmu.Libs.Tests/SharpEmu.Libs.Tests.csproj" />
|
||||||
|
</Folder>
|
||||||
</Solution>
|
</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())
|
||||||
@@ -154449,3 +154449,9 @@ vector_str_substr
|
|||||||
WTFAnnotateBenignRaceSized
|
WTFAnnotateBenignRaceSized
|
||||||
WTFAnnotateHappensAfter
|
WTFAnnotateHappensAfter
|
||||||
WTFAnnotateHappensBefore
|
WTFAnnotateHappensBefore
|
||||||
|
_sceUlobjmgrRegisterObject
|
||||||
|
_sceUlobjmgrUnregisterObject
|
||||||
|
sceNpEAAccessInitialize
|
||||||
|
sceNpEAAccessTerminate
|
||||||
|
sceNpHasEAAccessSubscription
|
||||||
|
sceNpHasEAAccessSubscriptionAbortRequest
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using SharpEmu.Core.Runtime;
|
|||||||
using SharpEmu.Core.Cpu;
|
using SharpEmu.Core.Cpu;
|
||||||
using SharpEmu.GUI;
|
using SharpEmu.GUI;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.VideoOut;
|
||||||
using SharpEmu.Logging;
|
using SharpEmu.Logging;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -111,8 +112,16 @@ internal static partial class Program
|
|||||||
using var runtime = SharpEmuRuntime.CreateDefault(runtimeOptions);
|
using var runtime = SharpEmuRuntime.CreateDefault(runtimeOptions);
|
||||||
|
|
||||||
OrbisGen2Result result;
|
OrbisGen2Result result;
|
||||||
|
ConsoleCancelEventHandler? cancelHandler = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
cancelHandler = (_, eventArgs) =>
|
||||||
|
{
|
||||||
|
eventArgs.Cancel = true;
|
||||||
|
VideoOutExports.NotifyHostInterrupt();
|
||||||
|
};
|
||||||
|
Console.CancelKeyPress += cancelHandler;
|
||||||
|
|
||||||
Console.Error.WriteLine($"[DEBUG] Running: {ebootPath}");
|
Console.Error.WriteLine($"[DEBUG] Running: {ebootPath}");
|
||||||
result = runtime.Run(ebootPath);
|
result = runtime.Run(ebootPath);
|
||||||
Console.Error.WriteLine($"[DEBUG] Result: {result}");
|
Console.Error.WriteLine($"[DEBUG] Result: {result}");
|
||||||
@@ -123,6 +132,13 @@ internal static partial class Program
|
|||||||
Log.Error("SharpEmu failed to run.", ex);
|
Log.Error("SharpEmu failed to run.", ex);
|
||||||
return 3;
|
return 3;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (cancelHandler is not null)
|
||||||
|
{
|
||||||
|
Console.CancelKeyPress -= cancelHandler;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Log.Info($"SharpEmu execution completed. Result={result} (0x{(int)result:X8})");
|
Log.Info($"SharpEmu execution completed. Result={result} (0x{(int)result:X8})");
|
||||||
if (!string.IsNullOrWhiteSpace(runtime.LastSessionSummary))
|
if (!string.IsNullOrWhiteSpace(runtime.LastSessionSummary))
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using SharpEmu.Core.Cpu.Native;
|
|||||||
using SharpEmu.Core.Loader;
|
using SharpEmu.Core.Loader;
|
||||||
using SharpEmu.Core.Memory;
|
using SharpEmu.Core.Memory;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
using SharpEmu.Logging;
|
using SharpEmu.Logging;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu;
|
namespace SharpEmu.Core.Cpu;
|
||||||
@@ -41,16 +42,19 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
];
|
];
|
||||||
private readonly IVirtualMemory _virtualMemory;
|
private readonly IVirtualMemory _virtualMemory;
|
||||||
private readonly IModuleManager _moduleManager;
|
private readonly IModuleManager _moduleManager;
|
||||||
|
private readonly IHostPlatform? _hostPlatform;
|
||||||
private INativeCpuBackend? _nativeCpuBackend;
|
private INativeCpuBackend? _nativeCpuBackend;
|
||||||
|
|
||||||
public CpuDispatcher(
|
public CpuDispatcher(
|
||||||
IVirtualMemory virtualMemory,
|
IVirtualMemory virtualMemory,
|
||||||
IModuleManager moduleManager,
|
IModuleManager moduleManager,
|
||||||
INativeCpuBackend? nativeCpuBackend = null)
|
INativeCpuBackend? nativeCpuBackend = null,
|
||||||
|
IHostPlatform? hostPlatform = null)
|
||||||
{
|
{
|
||||||
_virtualMemory = virtualMemory ?? throw new ArgumentNullException(nameof(virtualMemory));
|
_virtualMemory = virtualMemory ?? throw new ArgumentNullException(nameof(virtualMemory));
|
||||||
_moduleManager = moduleManager ?? throw new ArgumentNullException(nameof(moduleManager));
|
_moduleManager = moduleManager ?? throw new ArgumentNullException(nameof(moduleManager));
|
||||||
_nativeCpuBackend = nativeCpuBackend;
|
_nativeCpuBackend = nativeCpuBackend;
|
||||||
|
_hostPlatform = hostPlatform;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ulong? LastEntryPoint { get; private set; }
|
public ulong? LastEntryPoint { get; private set; }
|
||||||
@@ -266,7 +270,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
entryFrameDiagnostic,
|
entryFrameDiagnostic,
|
||||||
Environment.NewLine,
|
Environment.NewLine,
|
||||||
"CpuEngine: native-only");
|
"CpuEngine: native-only");
|
||||||
_nativeCpuBackend ??= new DirectExecutionBackend(_moduleManager);
|
_nativeCpuBackend ??= new DirectExecutionBackend(_moduleManager, _hostPlatform);
|
||||||
if (_nativeCpuBackend.TryExecute(
|
if (_nativeCpuBackend.TryExecute(
|
||||||
context,
|
context,
|
||||||
entryPoint,
|
entryPoint,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using System.Linq;
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
using SharpEmu.Logging;
|
using SharpEmu.Logging;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
@@ -134,8 +135,9 @@ public sealed partial class DirectExecutionBackend
|
|||||||
int num2 = 0;
|
int num2 = 0;
|
||||||
List<ulong> list = new List<ulong>(16);
|
List<ulong> list = new List<ulong>(16);
|
||||||
ulong num3 = scanStart;
|
ulong num3 = scanStart;
|
||||||
MEMORY_BASIC_INFORMATION64 lpBuffer;
|
var hostMemory = ResolveDiagnosticsHostMemory();
|
||||||
while (num3 < scanEnd && VirtualQuery((void*)num3, out lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) != 0)
|
HostRegionInfo lpBuffer;
|
||||||
|
while (num3 < scanEnd && hostMemory.Query(num3, out lpBuffer))
|
||||||
{
|
{
|
||||||
ulong baseAddress = lpBuffer.BaseAddress;
|
ulong baseAddress = lpBuffer.BaseAddress;
|
||||||
ulong num4 = baseAddress + lpBuffer.RegionSize;
|
ulong num4 = baseAddress + lpBuffer.RegionSize;
|
||||||
@@ -145,7 +147,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
ulong value = Math.Max(num3, baseAddress);
|
ulong value = Math.Max(num3, baseAddress);
|
||||||
ulong num5 = Math.Min(num4, scanEnd);
|
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);
|
ulong num6 = AlignUp(value, 8uL);
|
||||||
for (ulong num7 = num6; num7 + 8 <= num5; num7 += 8)
|
for (ulong num7 = num6; num7 + 8 <= num5; num7 += 8)
|
||||||
@@ -350,7 +352,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
if (!ResolveDiagnosticsHostMemory().Query(address, out var lpBuffer))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -359,7 +361,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (lpBuffer.State != 4096 || !IsReadableProtection(lpBuffer.Protect))
|
if (lpBuffer.State != HostRegionState.Committed || !IsReadableProtection(lpBuffer.RawProtection))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -391,12 +393,12 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
if (!ResolveDiagnosticsHostMemory().Query(address, out var lpBuffer))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var executable = lpBuffer.State == 4096 && IsExecutableProtection(lpBuffer.Protect);
|
var executable = lpBuffer.State == HostRegionState.Committed && IsExecutableProtection(lpBuffer.RawProtection);
|
||||||
if (executable)
|
if (executable)
|
||||||
{
|
{
|
||||||
_knownExecutablePages.TryAdd(pageAddress, 0);
|
_knownExecutablePages.TryAdd(pageAddress, 0);
|
||||||
@@ -415,6 +417,14 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return (value + num) & ~num;
|
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)
|
private static bool IsReadableProtection(uint protect)
|
||||||
{
|
{
|
||||||
if ((protect & 0x100) != 0 || (protect & 1) != 0)
|
if ((protect & 0x100) != 0 || (protect & 1) != 0)
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ using System.Reflection;
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using SharpEmu.Core.Cpu.Disasm;
|
using SharpEmu.Core.Cpu.Disasm;
|
||||||
|
using SharpEmu.Core.Cpu.Native.Windows;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
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))
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_RAW_HANDLER"), "1", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
_rawExceptionHandlerStub = CreateExceptionHandlerTrampoline(RawVectoredHandlerPtrManaged);
|
_rawExceptionHandlerStub = _faultHandling.CreateHandlerThunk(RawVectoredHandlerPtrManaged, _hostRspSlotTlsIndex, _tlsGetValueAddress);
|
||||||
if (_rawExceptionHandlerStub == 0)
|
if (_rawExceptionHandlerStub == 0)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Failed to create raw exception handler trampoline");
|
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}");
|
Console.Error.WriteLine($"[LOADER][INFO] Raw exception handler installed: 0x{_rawExceptionHandler:X16}");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -37,22 +39,22 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
_handlerDelegate = VectoredHandler;
|
_handlerDelegate = VectoredHandler;
|
||||||
_handlerHandle = GCHandle.Alloc(_handlerDelegate);
|
_handlerHandle = GCHandle.Alloc(_handlerDelegate);
|
||||||
_exceptionHandlerStub = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_handlerDelegate));
|
_exceptionHandlerStub = _faultHandling.CreateHandlerThunk(Marshal.GetFunctionPointerForDelegate(_handlerDelegate), _hostRspSlotTlsIndex, _tlsGetValueAddress);
|
||||||
if (_exceptionHandlerStub == 0)
|
if (_exceptionHandlerStub == 0)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Failed to create exception handler trampoline");
|
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}");
|
Console.Error.WriteLine($"[LOADER][INFO] Exception handler installed: 0x{_exceptionHandler:X16}");
|
||||||
|
|
||||||
_unhandledFilterDelegate = UnhandledExceptionFilter;
|
_unhandledFilterDelegate = UnhandledExceptionFilter;
|
||||||
_unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate);
|
_unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate);
|
||||||
_unhandledFilterStub = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate));
|
_unhandledFilterStub = _faultHandling.CreateHandlerThunk(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate), _hostRspSlotTlsIndex, _tlsGetValueAddress);
|
||||||
if (_unhandledFilterStub == 0)
|
if (_unhandledFilterStub == 0)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Failed to create unhandled exception filter trampoline");
|
throw new InvalidOperationException("Failed to create unhandled exception filter trampoline");
|
||||||
}
|
}
|
||||||
SetUnhandledExceptionFilter(_unhandledFilterStub);
|
_faultHandling.SetUnhandledFilter(_unhandledFilterStub);
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe int UnhandledExceptionFilter(void* exceptionInfo)
|
private unsafe int UnhandledExceptionFilter(void* exceptionInfo)
|
||||||
@@ -60,8 +62,8 @@ public sealed partial class DirectExecutionBackend
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
|
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
|
||||||
ulong rip = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, 248);
|
ulong rip = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, CTX_RIP);
|
||||||
ulong rsp = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, 152);
|
ulong rsp = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, CTX_RSP);
|
||||||
Console.Error.WriteLine("[LOADER][FATAL] Unhandled exception filter fired.");
|
Console.Error.WriteLine("[LOADER][FATAL] Unhandled exception filter fired.");
|
||||||
Console.Error.WriteLine($"[LOADER][FATAL] Code: 0x{exceptionRecord->ExceptionCode:X8}");
|
Console.Error.WriteLine($"[LOADER][FATAL] Code: 0x{exceptionRecord->ExceptionCode:X8}");
|
||||||
Console.Error.WriteLine($"[LOADER][FATAL] Exception Address: 0x{(ulong)(nint)exceptionRecord->ExceptionAddress:X16}");
|
Console.Error.WriteLine($"[LOADER][FATAL] Exception Address: 0x{(ulong)(nint)exceptionRecord->ExceptionAddress:X16}");
|
||||||
@@ -100,8 +102,8 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong rip = ReadCtxU64(contextRecord, 248);
|
ulong rip = ReadCtxU64(contextRecord, CTX_RIP);
|
||||||
ulong rsp = ReadCtxU64(contextRecord, 152);
|
ulong rsp = ReadCtxU64(contextRecord, CTX_RSP);
|
||||||
|
|
||||||
// Thread-mode probe: a hardware exception raised while this thread is inside
|
// Thread-mode probe: a hardware exception raised while this thread is inside
|
||||||
// the managed import gateway means the VEH->managed reentry happened from
|
// the managed import gateway means the VEH->managed reentry happened from
|
||||||
@@ -112,7 +114,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
$"veh_in_gateway code=0x{exceptionCode:X8} rip=0x{rip:X16} gateway_depth={_threadModeGatewayDepth}");
|
$"veh_in_gateway code=0x{exceptionCode:X8} rip=0x{rip:X16} gateway_depth={_threadModeGatewayDepth}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
|
if (exceptionCode == WindowsFaultCodes.AccessViolation && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -127,10 +129,10 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
switch (exceptionCode)
|
switch (exceptionCode)
|
||||||
{
|
{
|
||||||
case 3221225477u:
|
case WindowsFaultCodes.AccessViolation:
|
||||||
LogAccessViolationTrace(exceptionAddress, exceptionRecord);
|
LogAccessViolationTrace(exceptionAddress, exceptionRecord);
|
||||||
break;
|
break;
|
||||||
case 3221226505u:
|
case WindowsFaultCodes.FastFail:
|
||||||
{
|
{
|
||||||
ulong p0 = exceptionRecord->NumberParameters >= 1 ? (*exceptionRecord->ExceptionInformation) : 0;
|
ulong p0 = exceptionRecord->NumberParameters >= 1 ? (*exceptionRecord->ExceptionInformation) : 0;
|
||||||
ulong p1 = exceptionRecord->NumberParameters >= 2 ? exceptionRecord->ExceptionInformation[1] : 0;
|
ulong p1 = exceptionRecord->NumberParameters >= 2 ? exceptionRecord->ExceptionInformation[1] : 0;
|
||||||
@@ -140,21 +142,21 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong rax = ReadCtxU64(contextRecord, 120);
|
ulong rax = ReadCtxU64(contextRecord, CTX_RAX);
|
||||||
ulong rbx = ReadCtxU64(contextRecord, 144);
|
ulong rbx = ReadCtxU64(contextRecord, CTX_RBX);
|
||||||
ulong rcx = ReadCtxU64(contextRecord, 128);
|
ulong rcx = ReadCtxU64(contextRecord, CTX_RCX);
|
||||||
ulong rdx = ReadCtxU64(contextRecord, 136);
|
ulong rdx = ReadCtxU64(contextRecord, CTX_RDX);
|
||||||
ulong rsi = ReadCtxU64(contextRecord, 168);
|
ulong rsi = ReadCtxU64(contextRecord, CTX_RSI);
|
||||||
ulong rdi = ReadCtxU64(contextRecord, 176);
|
ulong rdi = ReadCtxU64(contextRecord, CTX_RDI);
|
||||||
ulong rbp = ReadCtxU64(contextRecord, 160);
|
ulong rbp = ReadCtxU64(contextRecord, CTX_RBP);
|
||||||
ulong r8 = ReadCtxU64(contextRecord, 184);
|
ulong r8 = ReadCtxU64(contextRecord, CTX_R8);
|
||||||
ulong r9 = ReadCtxU64(contextRecord, 192);
|
ulong r9 = ReadCtxU64(contextRecord, CTX_R9);
|
||||||
ulong r10 = ReadCtxU64(contextRecord, 200);
|
ulong r10 = ReadCtxU64(contextRecord, CTX_R10);
|
||||||
ulong r11 = ReadCtxU64(contextRecord, 208);
|
ulong r11 = ReadCtxU64(contextRecord, CTX_R11);
|
||||||
ulong r12 = ReadCtxU64(contextRecord, 216);
|
ulong r12 = ReadCtxU64(contextRecord, CTX_R12);
|
||||||
ulong r13 = ReadCtxU64(contextRecord, 224);
|
ulong r13 = ReadCtxU64(contextRecord, CTX_R13);
|
||||||
ulong r14 = ReadCtxU64(contextRecord, 232);
|
ulong r14 = ReadCtxU64(contextRecord, CTX_R14);
|
||||||
ulong r15 = ReadCtxU64(contextRecord, 240);
|
ulong r15 = ReadCtxU64(contextRecord, CTX_R15);
|
||||||
|
|
||||||
Console.Error.WriteLine("[LOADER][INFO] =========================================");
|
Console.Error.WriteLine("[LOADER][INFO] =========================================");
|
||||||
Console.Error.WriteLine("[LOADER][INFO] NATIVE EXCEPTION CAUGHT!");
|
Console.Error.WriteLine("[LOADER][INFO] NATIVE EXCEPTION CAUGHT!");
|
||||||
@@ -185,7 +187,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
ulong accessType = 0;
|
ulong accessType = 0;
|
||||||
ulong target = 0;
|
ulong target = 0;
|
||||||
if (exceptionCode == 3221225477u && exceptionRecord->NumberParameters >= 2)
|
if (exceptionCode == WindowsFaultCodes.AccessViolation && exceptionRecord->NumberParameters >= 2)
|
||||||
{
|
{
|
||||||
accessType = *exceptionRecord->ExceptionInformation;
|
accessType = *exceptionRecord->ExceptionInformation;
|
||||||
target = exceptionRecord->ExceptionInformation[1];
|
target = exceptionRecord->ExceptionInformation[1];
|
||||||
@@ -198,9 +200,9 @@ public sealed partial class DirectExecutionBackend
|
|||||||
};
|
};
|
||||||
Console.Error.WriteLine("[LOADER][INFO] AV access: " + accessText);
|
Console.Error.WriteLine("[LOADER][INFO] AV access: " + accessText);
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] AV target: 0x{target:X16}");
|
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}");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -248,7 +250,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
switch (exceptionCode)
|
switch (exceptionCode)
|
||||||
{
|
{
|
||||||
case 3221225477u:
|
case WindowsFaultCodes.AccessViolation:
|
||||||
Console.Error.WriteLine("[LOADER][ERROR] Type: Access Violation");
|
Console.Error.WriteLine("[LOADER][ERROR] Type: Access Violation");
|
||||||
Console.Error.WriteLine("[LOADER][ERROR] This usually means:");
|
Console.Error.WriteLine("[LOADER][ERROR] This usually means:");
|
||||||
Console.Error.WriteLine("[LOADER][ERROR] - Guest code called an unmapped import");
|
Console.Error.WriteLine("[LOADER][ERROR] - Guest code called an unmapped import");
|
||||||
@@ -295,11 +297,11 @@ public sealed partial class DirectExecutionBackend
|
|||||||
DumpGuestReferenceDiagnostics();
|
DumpGuestReferenceDiagnostics();
|
||||||
DumpGuestPointerWindowDiagnostics();
|
DumpGuestPointerWindowDiagnostics();
|
||||||
break;
|
break;
|
||||||
case 2147483651u:
|
case WindowsFaultCodes.Breakpoint:
|
||||||
Console.Error.WriteLine("[LOADER][WARNING] Type: Breakpoint (int3)");
|
Console.Error.WriteLine("[LOADER][WARNING] Type: Breakpoint (int3)");
|
||||||
Console.Error.WriteLine("[LOADER][WARNING] Unexpected breakpoint in direct-bridge mode");
|
Console.Error.WriteLine("[LOADER][WARNING] Unexpected breakpoint in direct-bridge mode");
|
||||||
break;
|
break;
|
||||||
case 3221225501u:
|
case WindowsFaultCodes.IllegalInstruction:
|
||||||
Console.Error.WriteLine("[LOADER][INFO] Type: Illegal Instruction");
|
Console.Error.WriteLine("[LOADER][INFO] Type: Illegal Instruction");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -332,8 +334,8 @@ public sealed partial class DirectExecutionBackend
|
|||||||
EXCEPTION_POINTERS* pointers = (EXCEPTION_POINTERS*)exceptionInfo;
|
EXCEPTION_POINTERS* pointers = (EXCEPTION_POINTERS*)exceptionInfo;
|
||||||
EXCEPTION_RECORD* record = pointers->ExceptionRecord;
|
EXCEPTION_RECORD* record = pointers->ExceptionRecord;
|
||||||
void* contextRecord = pointers->ContextRecord;
|
void* contextRecord = pointers->ContextRecord;
|
||||||
ulong rip = contextRecord != null ? ReadCtxU64(contextRecord, 248) : 0;
|
ulong rip = contextRecord != null ? ReadCtxU64(contextRecord, CTX_RIP) : 0;
|
||||||
ulong rsp = contextRecord != null ? ReadCtxU64(contextRecord, 152) : 0;
|
ulong rsp = contextRecord != null ? ReadCtxU64(contextRecord, CTX_RSP) : 0;
|
||||||
ulong accessType = record->NumberParameters >= 1 ? *record->ExceptionInformation : 0;
|
ulong accessType = record->NumberParameters >= 1 ? *record->ExceptionInformation : 0;
|
||||||
ulong target = record->NumberParameters >= 2 ? record->ExceptionInformation[1] : 0;
|
ulong target = record->NumberParameters >= 2 ? record->ExceptionInformation[1] : 0;
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
@@ -479,7 +481,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
ulong address = scanBase;
|
ulong address = scanBase;
|
||||||
while (address < scanEnd)
|
while (address < scanEnd)
|
||||||
{
|
{
|
||||||
if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
if (!_hostMemory.Query(address, out var mbi))
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -491,9 +493,9 @@ public sealed partial class DirectExecutionBackend
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mbi.State == MEM_COMMIT &&
|
if (mbi.State == HostRegionState.Committed &&
|
||||||
IsReadableProtection(mbi.Protect) &&
|
IsReadableProtection(mbi.RawProtection) &&
|
||||||
IsExecutableProtection(mbi.Protect))
|
IsExecutableProtection(mbi.RawProtection))
|
||||||
{
|
{
|
||||||
ScanExecutableRegionForTargetReferences(regionBase, regionEnd, targetList, hitCounts, maxHitsPerTarget);
|
ScanExecutableRegionForTargetReferences(regionBase, regionEnd, targetList, hitCounts, maxHitsPerTarget);
|
||||||
}
|
}
|
||||||
@@ -798,13 +800,13 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
if (!_hostMemory.Query(address, out var mbi))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong regionEnd = mbi.BaseAddress + mbi.RegionSize;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -916,25 +918,25 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (VirtualQuery((void*)faultAddress, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
if (!_hostMemory.Query(faultAddress, out var mbi))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong pageBase = faultAddress & 0xFFFFFFFFFFFFF000uL;
|
ulong pageBase = faultAddress & 0xFFFFFFFFFFFFF000uL;
|
||||||
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.AllocationProtect);
|
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.RawAllocationProtection);
|
||||||
int traceIndex = Interlocked.Increment(ref _lazyCommitTraceCount);
|
int traceIndex = Interlocked.Increment(ref _lazyCommitTraceCount);
|
||||||
bool traceLazyCommit = ShouldTraceLazyCommit(traceIndex);
|
bool traceLazyCommit = ShouldTraceLazyCommit(traceIndex);
|
||||||
if (traceLazyCommit)
|
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)
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -943,10 +945,10 @@ public sealed partial class DirectExecutionBackend
|
|||||||
ulong committedBase = 0;
|
ulong committedBase = 0;
|
||||||
ulong committedSize = 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) &&
|
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;
|
committed = true;
|
||||||
committedBase = windowBase;
|
committedBase = windowBase;
|
||||||
@@ -955,7 +957,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
||||||
if (TryReserveThenCommit(largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
|
if (TryReserveThenCommit(_hostMemory, largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = largeBase;
|
committedBase = largeBase;
|
||||||
@@ -966,13 +968,13 @@ public sealed partial class DirectExecutionBackend
|
|||||||
if (!committed)
|
if (!committed)
|
||||||
{
|
{
|
||||||
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
||||||
if (TryReserveThenCommit(region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
|
if (TryReserveThenCommit(_hostMemory, region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = region64kBase;
|
committedBase = region64kBase;
|
||||||
committedSize = 65536uL;
|
committedSize = 65536uL;
|
||||||
}
|
}
|
||||||
else if (TryReserveThenCommit(pageBase, 4096uL, pageBase, 4096uL, commitProtect))
|
else if (TryReserveThenCommit(_hostMemory, pageBase, 4096uL, pageBase, 4096uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = pageBase;
|
committedBase = pageBase;
|
||||||
@@ -985,7 +987,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
|
TryCommitRange(_hostMemory, pageBase + 4096, 4096uL, commitProtect);
|
||||||
if (traceLazyCommit)
|
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}");
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-reserve-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
||||||
@@ -993,13 +995,13 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mbi.State != 8192)
|
if (mbi.State != HostRegionState.Reserved)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var commitWindowBase, out var commitWindowSize) &&
|
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var commitWindowBase, out var commitWindowSize) &&
|
||||||
TryCommitRange(commitWindowBase, commitWindowSize, commitProtect))
|
TryCommitRange(_hostMemory, commitWindowBase, commitWindowSize, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = commitWindowBase;
|
committedBase = commitWindowBase;
|
||||||
@@ -1008,7 +1010,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
||||||
if (TryCommitRange(largeCommitBase, 2097152uL, commitProtect))
|
if (TryCommitRange(_hostMemory, largeCommitBase, 2097152uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = largeCommitBase;
|
committedBase = largeCommitBase;
|
||||||
@@ -1019,19 +1021,19 @@ public sealed partial class DirectExecutionBackend
|
|||||||
if (!committed)
|
if (!committed)
|
||||||
{
|
{
|
||||||
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
||||||
if (TryCommitRange(region64kBase, 65536uL, commitProtect))
|
if (TryCommitRange(_hostMemory, region64kBase, 65536uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = region64kBase;
|
committedBase = region64kBase;
|
||||||
committedSize = 65536uL;
|
committedSize = 65536uL;
|
||||||
}
|
}
|
||||||
else if (TryCommitRange(pageBase, 8192uL, commitProtect))
|
else if (TryCommitRange(_hostMemory, pageBase, 8192uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = pageBase;
|
committedBase = pageBase;
|
||||||
committedSize = 8192uL;
|
committedSize = 8192uL;
|
||||||
}
|
}
|
||||||
else if (TryCommitRange(pageBase, 4096uL, commitProtect))
|
else if (TryCommitRange(_hostMemory, pageBase, 4096uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = pageBase;
|
committedBase = pageBase;
|
||||||
@@ -1044,7 +1046,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
|
TryCommitRange(_hostMemory, pageBase + 4096, 4096uL, commitProtect);
|
||||||
if (traceLazyCommit)
|
if (traceLazyCommit)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
||||||
@@ -1085,31 +1087,33 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return true;
|
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)
|
if (length == 0)
|
||||||
{
|
{
|
||||||
return false;
|
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)
|
if (length == 0)
|
||||||
{
|
{
|
||||||
return false;
|
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 false;
|
||||||
}
|
}
|
||||||
return TryCommitRange(commitAddress, commitSize, protection);
|
return TryCommitRange(hostMemory, commitAddress, commitSize, protection);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool IsAccessCompatible(ulong accessType, uint protection)
|
static bool IsAccessCompatible(ulong accessType, uint protection)
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ using System.Linq;
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using SharpEmu.Core.Cpu;
|
using SharpEmu.Core.Cpu;
|
||||||
|
using SharpEmu.Core.Cpu.Native.Windows;
|
||||||
using SharpEmu.Core.Loader;
|
using SharpEmu.Core.Loader;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
@@ -69,23 +71,23 @@ public sealed partial class DirectExecutionBackend
|
|||||||
private unsafe static int TryRecoverUnresolvedSentinel(void* exceptionInfo)
|
private unsafe static int TryRecoverUnresolvedSentinel(void* exceptionInfo)
|
||||||
{
|
{
|
||||||
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
|
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
|
||||||
if (exceptionRecord->ExceptionCode != 3221225477u)
|
if (exceptionRecord->ExceptionCode != WindowsFaultCodes.AccessViolation)
|
||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
void* contextRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord;
|
void* contextRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord;
|
||||||
ulong value = ReadCtxU64(contextRecord, 248);
|
ulong value = ReadCtxU64(contextRecord, CTX_RIP);
|
||||||
ulong value2 = (ulong)exceptionRecord->ExceptionAddress;
|
ulong value2 = (ulong)exceptionRecord->ExceptionAddress;
|
||||||
if (!IsUnresolvedSentinel(value) && !IsUnresolvedSentinel(value2))
|
if (!IsUnresolvedSentinel(value) && !IsUnresolvedSentinel(value2))
|
||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
ulong rsp = ReadCtxU64(contextRecord, 152);
|
ulong rsp = ReadCtxU64(contextRecord, CTX_RSP);
|
||||||
WriteCtxU64(contextRecord, 120, 0uL);
|
WriteCtxU64(contextRecord, CTX_RAX, 0uL);
|
||||||
if (TryGetPlausibleReturnFromStack(rsp, out var returnRip, out var nextRsp))
|
if (TryGetPlausibleReturnFromStack(rsp, out var returnRip, out var nextRsp))
|
||||||
{
|
{
|
||||||
WriteCtxU64(contextRecord, 152, nextRsp);
|
WriteCtxU64(contextRecord, CTX_RSP, nextRsp);
|
||||||
WriteCtxU64(contextRecord, 248, returnRip);
|
WriteCtxU64(contextRecord, CTX_RIP, returnRip);
|
||||||
Interlocked.Increment(ref _rawSentinelRecoveries);
|
Interlocked.Increment(ref _rawSentinelRecoveries);
|
||||||
if (LogThreadMode)
|
if (LogThreadMode)
|
||||||
{
|
{
|
||||||
@@ -238,6 +240,22 @@ public sealed partial class DirectExecutionBackend
|
|||||||
cpuContext[CpuRegister.Rax] = 0uL;
|
cpuContext[CpuRegister.Rax] = 0uL;
|
||||||
return 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 flag0 = ShouldSuppressStrlenTrace(importStubEntry.Nid);
|
||||||
bool flag = num7 >= 2156221920u && num7 <= 2156225024u;
|
bool flag = num7 >= 2156221920u && num7 <= 2156225024u;
|
||||||
bool flag2 = num7 >= 2156351360u && num7 <= 2156352080u;
|
bool flag2 = num7 >= 2156351360u && num7 <= 2156352080u;
|
||||||
@@ -517,8 +535,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
out var blockContinuation,
|
out var blockContinuation,
|
||||||
out var hasBlockContinuation,
|
out var hasBlockContinuation,
|
||||||
out var blockWakeKey,
|
out var blockWakeKey,
|
||||||
out var blockResumeHandler,
|
out var blockWaiter,
|
||||||
out var blockWakeHandler,
|
|
||||||
out var blockDeadlineTimestamp) &&
|
out var blockDeadlineTimestamp) &&
|
||||||
TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, blockReason))
|
TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, blockReason))
|
||||||
{
|
{
|
||||||
@@ -528,8 +545,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
GuestThreadExecution.CurrentGuestThreadHandle,
|
GuestThreadExecution.CurrentGuestThreadHandle,
|
||||||
blockContinuation,
|
blockContinuation,
|
||||||
blockWakeKey,
|
blockWakeKey,
|
||||||
blockResumeHandler,
|
blockWaiter,
|
||||||
blockWakeHandler,
|
|
||||||
blockDeadlineTimestamp);
|
blockDeadlineTimestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -660,8 +676,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
out var blockContinuation,
|
out var blockContinuation,
|
||||||
out var hasBlockContinuation,
|
out var hasBlockContinuation,
|
||||||
out var blockWakeKey,
|
out var blockWakeKey,
|
||||||
out var blockResumeHandler,
|
out var blockWaiter,
|
||||||
out var blockWakeHandler,
|
|
||||||
out var blockDeadlineTimestamp) &&
|
out var blockDeadlineTimestamp) &&
|
||||||
TryYieldGuestThreadToHostStub(argPackPtr, dispatchIndex, returnRip, importStubEntry.Nid, blockReason))
|
TryYieldGuestThreadToHostStub(argPackPtr, dispatchIndex, returnRip, importStubEntry.Nid, blockReason))
|
||||||
{
|
{
|
||||||
@@ -671,8 +686,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
GuestThreadExecution.CurrentGuestThreadHandle,
|
GuestThreadExecution.CurrentGuestThreadHandle,
|
||||||
blockContinuation,
|
blockContinuation,
|
||||||
blockWakeKey,
|
blockWakeKey,
|
||||||
blockResumeHandler,
|
blockWaiter,
|
||||||
blockWakeHandler,
|
|
||||||
blockDeadlineTimestamp);
|
blockDeadlineTimestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -814,10 +828,10 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return !_logUsleep;
|
return !_logUsleep;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only mutex/rwlock *lock* is excluded: it may block a contended acquire, which the
|
// Mutex lock uses this block-capable leaf path. Keep it out of the no-block subset.
|
||||||
// leaf path can't. unlock never blocks and stays here — routing it off the fast path
|
|
||||||
// slows guest spinlocks enough to livelock (Demon's Souls).
|
|
||||||
return nid is
|
return nid is
|
||||||
|
"9UK1vLZQft4" or // scePthreadMutexLock
|
||||||
|
"7H0iTOciTLo" or // pthread_mutex_lock
|
||||||
"tn3VlD0hG60" or // scePthreadMutexUnlock
|
"tn3VlD0hG60" or // scePthreadMutexUnlock
|
||||||
"2Z+PpY6CaJg" or // pthread_mutex_unlock
|
"2Z+PpY6CaJg" or // pthread_mutex_unlock
|
||||||
"EgmLo6EWgso" or // pthread_rwlock_unlock
|
"EgmLo6EWgso" or // pthread_rwlock_unlock
|
||||||
@@ -881,7 +895,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
"Q2V+iqvjgC0" or // vsnprintf
|
"Q2V+iqvjgC0" or // vsnprintf
|
||||||
"j4ViWNHEgww" or // strlen
|
"j4ViWNHEgww" or // strlen
|
||||||
"5jNubw4vlAA" or // strnlen
|
"5jNubw4vlAA" or // strnlen
|
||||||
"LHMrG7e8G78" or // wcslen
|
"LHMrG7e8G78" or // wcsmisc
|
||||||
"WkkeywLJcgU" or // wcslen
|
"WkkeywLJcgU" or // wcslen
|
||||||
"Ovb2dSJOAuE" or // strcmp
|
"Ovb2dSJOAuE" or // strcmp
|
||||||
"aesyjrHVWy4" or // strncmp
|
"aesyjrHVWy4" or // strncmp
|
||||||
@@ -975,6 +989,31 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return true;
|
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)
|
private unsafe bool TryCompleteGuestEntryToHostStub(nint argPackPtr, long dispatchIndex, ulong returnRip, string nid, string reason, ulong value)
|
||||||
{
|
{
|
||||||
ulong hostExit = ActiveEntryReturnSentinelRip;
|
ulong hostExit = ActiveEntryReturnSentinelRip;
|
||||||
@@ -1684,9 +1723,9 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
var candidateBase = ImportStubRegionCanonicalBase -
|
var candidateBase = ImportStubRegionCanonicalBase -
|
||||||
(ulong)candidateIndex * ImportStubRegionAddressStride;
|
(ulong)candidateIndex * ImportStubRegionAddressStride;
|
||||||
if (VirtualQuery((void*)candidateBase, out var memoryInfo, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0 ||
|
if (!_hostMemory.Query(candidateBase, out var memoryInfo) ||
|
||||||
memoryInfo.RegionSize == 0 ||
|
memoryInfo.RegionSize == 0 ||
|
||||||
memoryInfo.State != 4096)
|
memoryInfo.State != HostRegionState.Committed)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1927,23 +1966,36 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<byte> list = new List<byte>(Math.Min(maxLength, 256));
|
// Reads stay byte-by-byte through TryReadByteCompat (its Marshal.ReadByte
|
||||||
Span<byte> destination = stackalloc byte[1];
|
// fallback must probe exactly up to the terminator), but the bytes land in a
|
||||||
for (int i = 0; i < maxLength; i++)
|
// stack buffer instead of a List<byte> + ToArray per symbol resolution.
|
||||||
|
const int StackBufferLength = 512;
|
||||||
|
byte[]? rented = maxLength > StackBufferLength ? System.Buffers.ArrayPool<byte>.Shared.Rent(maxLength) : null;
|
||||||
|
Span<byte> buffer = rented is null ? stackalloc byte[StackBufferLength] : rented;
|
||||||
|
try
|
||||||
{
|
{
|
||||||
if (!TryReadByteCompat(address + (ulong)i, destination))
|
for (int i = 0; i < maxLength; i++)
|
||||||
{
|
{
|
||||||
return false;
|
if (!TryReadByteCompat(address + (ulong)i, buffer.Slice(i, 1)))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (buffer[i] == 0)
|
||||||
|
{
|
||||||
|
value = System.Text.Encoding.ASCII.GetString(buffer[..i]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value = System.Text.Encoding.ASCII.GetString(buffer[..maxLength]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (rented is not null)
|
||||||
|
{
|
||||||
|
System.Buffers.ArrayPool<byte>.Shared.Return(rented);
|
||||||
}
|
}
|
||||||
if (destination[0] == 0)
|
|
||||||
{
|
|
||||||
value = System.Text.Encoding.ASCII.GetString(list.ToArray());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
list.Add(destination[0]);
|
|
||||||
}
|
}
|
||||||
value = System.Text.Encoding.ASCII.GetString(list.ToArray());
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryReadByteCompat(ulong address, Span<byte> destination)
|
private bool TryReadByteCompat(ulong address, Span<byte> destination)
|
||||||
@@ -2025,7 +2077,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
uint flNewProtect = default(uint);
|
uint flNewProtect = default(uint);
|
||||||
try
|
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;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2033,7 +2085,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
Marshal.WriteByte(num2 + i, 144);
|
Marshal.WriteByte(num2 + i, 144);
|
||||||
}
|
}
|
||||||
FlushInstructionCache(GetCurrentProcess(), (void*)num, 5u);
|
_hostMemory.FlushInstructionCache((ulong)(void*)num, 5u);
|
||||||
_patchedEa020eLookupCall = true;
|
_patchedEa020eLookupCall = true;
|
||||||
Console.Error.WriteLine($"[LOADER][WARNING] Import#{dispatchIndex}: patched hash-lookup call at 0x{num:X16} -> NOP*5");
|
Console.Error.WriteLine($"[LOADER][WARNING] Import#{dispatchIndex}: patched hash-lookup call at 0x{num:X16} -> NOP*5");
|
||||||
}
|
}
|
||||||
@@ -2044,7 +2096,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
if (flNewProtect != 0)
|
if (flNewProtect != 0)
|
||||||
{
|
{
|
||||||
VirtualProtect((void*)num, 5u, flNewProtect, &flNewProtect);
|
_hostMemory.ProtectRaw((ulong)(void*)num, 5u, flNewProtect, out flNewProtect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System.Collections.Generic;
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
@@ -35,20 +36,6 @@ public sealed partial class DirectExecutionBackend
|
|||||||
private bool _nativeWorkersDisposed;
|
private bool _nativeWorkersDisposed;
|
||||||
private int _nativeWorkerCreationFailedLogged;
|
private int _nativeWorkerCreationFailedLogged;
|
||||||
|
|
||||||
private const uint StackSizeParamIsAReservation = 0x00010000u;
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
private static extern nint CreateThread(
|
|
||||||
nint lpThreadAttributes,
|
|
||||||
nuint dwStackSize,
|
|
||||||
nint lpStartAddress,
|
|
||||||
nint lpParameter,
|
|
||||||
uint dwCreationFlags,
|
|
||||||
out uint lpThreadId);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
private static extern uint WaitForSingleObject(nint hHandle, uint dwMilliseconds);
|
|
||||||
|
|
||||||
// Runs an emitted guest entry stub. Preferred path is a pooled native worker
|
// 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; falls back to the historical inline calli (guest frames above this
|
||||||
// thread's managed frames) when workers are disabled or unavailable.
|
// thread's managed frames) when workers are disabled or unavailable.
|
||||||
@@ -61,7 +48,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
var worker = RentNativeGuestExecutor();
|
var worker = RentNativeGuestExecutor();
|
||||||
if (worker is null)
|
if (worker is null)
|
||||||
{
|
{
|
||||||
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
_hostThreading.SetTlsValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
||||||
return CallNativeEntry(entryStub);
|
return CallNativeEntry(entryStub);
|
||||||
}
|
}
|
||||||
try
|
try
|
||||||
@@ -229,7 +216,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
public static NativeGuestExecutor? TryCreate(DirectExecutionBackend backend)
|
public static NativeGuestExecutor? TryCreate(DirectExecutionBackend backend)
|
||||||
{
|
{
|
||||||
if (!EnsureKernel32Exports())
|
if (!EnsureHostRuntimeExports(backend._hostSymbols))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -242,32 +229,27 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return executor;
|
return executor;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool EnsureKernel32Exports()
|
private static bool EnsureHostRuntimeExports(IHostSymbolResolver symbols)
|
||||||
{
|
{
|
||||||
if (_exitThreadAddress != 0)
|
if (_exitThreadAddress != 0)
|
||||||
{
|
{
|
||||||
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0;
|
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0;
|
||||||
}
|
}
|
||||||
nint kernel32 = GetModuleHandle("kernel32.dll");
|
_waitForSingleObjectAddress = symbols.GetAddress(HostRuntimeFunction.WaitForSingleObject);
|
||||||
if (kernel32 == 0)
|
_setEventAddress = symbols.GetAddress(HostRuntimeFunction.SetEvent);
|
||||||
{
|
_exitThreadAddress = symbols.GetAddress(HostRuntimeFunction.ExitThread);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
_waitForSingleObjectAddress = GetProcAddress(kernel32, "WaitForSingleObject");
|
|
||||||
_setEventAddress = GetProcAddress(kernel32, "SetEvent");
|
|
||||||
_exitThreadAddress = GetProcAddress(kernel32, "ExitThread");
|
|
||||||
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0 && _exitThreadAddress != 0;
|
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0 && _exitThreadAddress != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool Initialize()
|
private bool Initialize()
|
||||||
{
|
{
|
||||||
_selfHandle = GCHandle.Alloc(this);
|
_selfHandle = GCHandle.Alloc(this);
|
||||||
_controlBlock = VirtualAlloc(null, 4096u, 12288u, 4u);
|
_controlBlock = (void*)_backend._hostMemory.Allocate(0, 4096u, HostPageProtection.ReadWrite);
|
||||||
if (_controlBlock == null)
|
if (_controlBlock == null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
_loopStub = VirtualAlloc(null, LoopStubSize, 12288u, 64u);
|
_loopStub = (void*)_backend._hostMemory.Allocate(0, LoopStubSize, HostPageProtection.ReadWriteExecute);
|
||||||
if (_loopStub == null)
|
if (_loopStub == null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -349,17 +331,15 @@ public sealed partial class DirectExecutionBackend
|
|||||||
*(int*)(code + skipJump) = skipEntryOffset - (skipJump + sizeof(int));
|
*(int*)(code + skipJump) = skipEntryOffset - (skipJump + sizeof(int));
|
||||||
|
|
||||||
uint oldProtect = 0;
|
uint oldProtect = 0;
|
||||||
if (!VirtualProtect(_loopStub, LoopStubSize, 32u, &oldProtect))
|
if (!_backend._hostMemory.Protect((ulong)_loopStub, LoopStubSize, HostPageProtection.ReadExecute, out oldProtect))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
FlushInstructionCache(GetCurrentProcess(), _loopStub, LoopStubSize);
|
_backend._hostMemory.FlushInstructionCache((ulong)_loopStub, LoopStubSize);
|
||||||
_threadHandle = CreateThread(
|
_threadHandle = _backend._hostThreading.CreateNativeThread(
|
||||||
0,
|
|
||||||
WorkerStackReservation,
|
|
||||||
(nint)_loopStub,
|
(nint)_loopStub,
|
||||||
0,
|
0,
|
||||||
StackSizeParamIsAReservation,
|
WorkerStackReservation,
|
||||||
out _nativeThreadId);
|
out _nativeThreadId);
|
||||||
if (_threadHandle == 0)
|
if (_threadHandle == 0)
|
||||||
{
|
{
|
||||||
@@ -465,7 +445,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
_prevYieldRequested = _activeGuestThreadYieldRequested;
|
_prevYieldRequested = _activeGuestThreadYieldRequested;
|
||||||
_prevYieldReason = _activeGuestThreadYieldReason;
|
_prevYieldReason = _activeGuestThreadYieldReason;
|
||||||
_prevState = _activeGuestThreadState;
|
_prevState = _activeGuestThreadState;
|
||||||
_prevHostRspSlot = TlsGetValue(backend._hostRspSlotTlsIndex);
|
_prevHostRspSlot = backend._hostThreading.GetTlsValue(backend._hostRspSlotTlsIndex);
|
||||||
_prevGuestThreadHandle = GuestThreadExecution.EnterGuestThread(_runGuestThreadHandle);
|
_prevGuestThreadHandle = GuestThreadExecution.EnterGuestThread(_runGuestThreadHandle);
|
||||||
_entered = true;
|
_entered = true;
|
||||||
_activeExecutionBackend = backend;
|
_activeExecutionBackend = backend;
|
||||||
@@ -477,11 +457,11 @@ public sealed partial class DirectExecutionBackend
|
|||||||
_activeGuestThreadYieldReason = null;
|
_activeGuestThreadYieldReason = null;
|
||||||
_activeGuestThreadState = _runState;
|
_activeGuestThreadState = _runState;
|
||||||
backend.BindTlsBase(_runContext!);
|
backend.BindTlsBase(_runContext!);
|
||||||
TlsSetValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
|
backend._hostThreading.SetTlsValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
|
||||||
if (_runState is { } state)
|
if (_runState is { } state)
|
||||||
{
|
{
|
||||||
_prevHostThreadId = Volatile.Read(ref state.HostThreadId);
|
_prevHostThreadId = Volatile.Read(ref state.HostThreadId);
|
||||||
Volatile.Write(ref state.HostThreadId, unchecked((int)GetCurrentThreadId()));
|
Volatile.Write(ref state.HostThreadId, unchecked((int)backend._hostThreading.CurrentThreadId));
|
||||||
}
|
}
|
||||||
if (_runAffinityMask != 0)
|
if (_runAffinityMask != 0)
|
||||||
{
|
{
|
||||||
@@ -511,7 +491,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
Volatile.Write(ref state.HostThreadId, _prevHostThreadId);
|
Volatile.Write(ref state.HostThreadId, _prevHostThreadId);
|
||||||
}
|
}
|
||||||
TlsSetValue(_backend._hostRspSlotTlsIndex, _prevHostRspSlot);
|
_backend._hostThreading.SetTlsValue(_backend._hostRspSlotTlsIndex, _prevHostRspSlot);
|
||||||
GuestThreadExecution.RestoreGuestThread(_prevGuestThreadHandle);
|
GuestThreadExecution.RestoreGuestThread(_prevGuestThreadHandle);
|
||||||
_activeExecutionBackend = _prevBackend;
|
_activeExecutionBackend = _prevBackend;
|
||||||
_activeCpuContext = _prevContext;
|
_activeCpuContext = _prevContext;
|
||||||
@@ -548,8 +528,8 @@ public sealed partial class DirectExecutionBackend
|
|||||||
var exited = _threadHandle == 0;
|
var exited = _threadHandle == 0;
|
||||||
if (_threadHandle != 0)
|
if (_threadHandle != 0)
|
||||||
{
|
{
|
||||||
exited = WaitForSingleObject(_threadHandle, 1000u) == 0u;
|
exited = _backend._hostThreading.WaitForThreadExit(_threadHandle, 1000u);
|
||||||
CloseHandle(_threadHandle);
|
_backend._hostThreading.CloseThreadHandle(_threadHandle);
|
||||||
_threadHandle = 0;
|
_threadHandle = 0;
|
||||||
}
|
}
|
||||||
if (!exited)
|
if (!exited)
|
||||||
@@ -563,12 +543,12 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
if (_loopStub != null)
|
if (_loopStub != null)
|
||||||
{
|
{
|
||||||
VirtualFree(_loopStub, 0u, 32768u);
|
_backend._hostMemory.Free((ulong)_loopStub);
|
||||||
_loopStub = null;
|
_loopStub = null;
|
||||||
}
|
}
|
||||||
if (_controlBlock != null)
|
if (_controlBlock != null)
|
||||||
{
|
{
|
||||||
VirtualFree(_controlBlock, 0u, 32768u);
|
_backend._hostMemory.Free((ulong)_controlBlock);
|
||||||
_controlBlock = null;
|
_controlBlock = null;
|
||||||
}
|
}
|
||||||
if (_selfHandle.IsAllocated)
|
if (_selfHandle.IsAllocated)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
@@ -11,17 +12,15 @@ public sealed unsafe class StubManager : IDisposable
|
|||||||
private readonly List<nint> _allocatedStubs = new();
|
private readonly List<nint> _allocatedStubs = new();
|
||||||
private readonly Dictionary<string, nint> _importHandlers = new();
|
private readonly Dictionary<string, nint> _importHandlers = new();
|
||||||
private readonly Dictionary<ulong, nint> _stubAddresses = new();
|
private readonly Dictionary<ulong, nint> _stubAddresses = new();
|
||||||
|
private readonly IHostMemory _hostMemory;
|
||||||
private byte* _pltMemory;
|
private byte* _pltMemory;
|
||||||
private int _pltOffset;
|
private int _pltOffset;
|
||||||
private const int PltMemorySize = 1024 * 1024; // 1MB for stubs
|
private const int PltMemorySize = 1024 * 1024; // 1MB for stubs
|
||||||
|
|
||||||
public StubManager()
|
public StubManager(IHostMemory? hostMemory = null)
|
||||||
{
|
{
|
||||||
_pltMemory = (byte*)VirtualAlloc(
|
_hostMemory = hostMemory ?? HostPlatform.Current.Memory;
|
||||||
null,
|
_pltMemory = (byte*)_hostMemory.Allocate(0, PltMemorySize, HostPageProtection.ReadWriteExecute);
|
||||||
(nuint)PltMemorySize,
|
|
||||||
AllocationType.Reserve | AllocationType.Commit,
|
|
||||||
MemoryProtection.ExecuteReadWrite);
|
|
||||||
|
|
||||||
if (_pltMemory == null)
|
if (_pltMemory == null)
|
||||||
{
|
{
|
||||||
@@ -185,7 +184,7 @@ public sealed unsafe class StubManager : IDisposable
|
|||||||
{
|
{
|
||||||
if (_pltMemory != null)
|
if (_pltMemory != null)
|
||||||
{
|
{
|
||||||
VirtualFree(_pltMemory, 0, FreeType.Release);
|
_hostMemory.Free((ulong)_pltMemory);
|
||||||
_pltMemory = null;
|
_pltMemory = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,29 +193,5 @@ public sealed unsafe class StubManager : IDisposable
|
|||||||
_stubAddresses.Clear();
|
_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);
|
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;
|
namespace SharpEmu.Core.Cpu;
|
||||||
|
|
||||||
public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemoryAllocator
|
public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemoryAllocator, ICpuMemoryWrapper
|
||||||
{
|
{
|
||||||
private readonly ICpuMemory _inner;
|
private readonly ICpuMemory _inner;
|
||||||
|
|
||||||
@@ -50,4 +50,9 @@ public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemo
|
|||||||
address = 0;
|
address = 0;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool TryFreeGuestMemory(ulong address)
|
||||||
|
{
|
||||||
|
return _inner is IGuestMemoryAllocator allocator && allocator.TryFreeGuestMemory(address);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,13 @@ public static class Ps5ParamJsonReader
|
|||||||
|
|
||||||
try
|
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);
|
return TryReadPs5Param(doc.RootElement);
|
||||||
}
|
}
|
||||||
catch (JsonException)
|
catch (JsonException)
|
||||||
@@ -56,12 +62,15 @@ public static class Ps5ParamJsonReader
|
|||||||
|
|
||||||
private static (string? Title, string? TitleId, string? Version) TryReadPs5Param(JsonElement root)
|
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 =
|
string? ver =
|
||||||
(root.TryGetProperty("contentVersion", out var cv) ? cv.GetString() : null)
|
GetString(root, "contentVersion")
|
||||||
?? (root.TryGetProperty("masterVersion", out var mv) ? mv.GetString() : null)
|
?? GetString(root, "masterVersion")
|
||||||
?? (root.TryGetProperty("targetContentVersion", out var tv) ? tv.GetString() : null);
|
?? GetString(root, "targetContentVersion");
|
||||||
|
|
||||||
string? title = ExtractTitleName(root);
|
string? title = ExtractTitleName(root);
|
||||||
|
|
||||||
@@ -70,34 +79,49 @@ public static class Ps5ParamJsonReader
|
|||||||
|
|
||||||
private static string? ExtractTitleName(JsonElement root)
|
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)
|
if (lp.ValueKind != JsonValueKind.Object)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
string? defLang = lp.TryGetProperty("defaultLanguage", out var dl) ? dl.GetString() : null;
|
var defLang = GetString(lp, "defaultLanguage");
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(defLang))
|
if (!string.IsNullOrEmpty(defLang))
|
||||||
{
|
{
|
||||||
if (lp.TryGetProperty(defLang, out var langObj) && langObj.ValueKind == JsonValueKind.Object)
|
if (lp.TryGetProperty(defLang, out var langObj) && langObj.ValueKind == JsonValueKind.Object)
|
||||||
{
|
{
|
||||||
if (langObj.TryGetProperty("titleName", out var tn))
|
var title = GetString(langObj, "titleName");
|
||||||
return tn.GetString();
|
if (!string.IsNullOrWhiteSpace(title))
|
||||||
|
return title;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lp.TryGetProperty("en-US", out var en) && en.ValueKind == JsonValueKind.Object)
|
if (lp.TryGetProperty("en-US", out var en) && en.ValueKind == JsonValueKind.Object)
|
||||||
{
|
{
|
||||||
if (en.TryGetProperty("titleName", out var tn2))
|
var title = GetString(en, "titleName");
|
||||||
return tn2.GetString();
|
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;
|
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
|
public sealed class SelfLoader : ISelfLoader
|
||||||
{
|
{
|
||||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Loader");
|
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 ulong SelfSegmentFlag = 0x800;
|
||||||
private const int PageSize = 0x1000;
|
private const int PageSize = 0x1000;
|
||||||
private const ulong ImportStubBaseAddress = 0x0000_7000_0000_0000UL;
|
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.");
|
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);
|
var selfHeader = ReadUnmanaged<SelfHeader>(imageData, 0);
|
||||||
if (!selfHeader.HasKnownLayout || selfHeader.Unknown != 0x22)
|
if (!selfHeader.HasKnownLayout || selfHeader.Unknown != 0x22)
|
||||||
@@ -345,6 +348,12 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
return new LoadContext(IsSelf: true, elfOffset, selfHeader.FileSize, segments);
|
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>());
|
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 ulong FileSize => _fileSize;
|
||||||
|
|
||||||
public bool HasKnownLayout =>
|
public bool HasKnownLayout =>
|
||||||
_ident0 == 0x4F &&
|
((_ident0 == 0x4F &&
|
||||||
_ident1 == 0x15 &&
|
_ident1 == 0x15 &&
|
||||||
_ident2 == 0x3D &&
|
_ident2 == 0x3D &&
|
||||||
_ident3 == 0x1D &&
|
_ident3 == 0x1D) ||
|
||||||
|
(_ident0 == 0x54 &&
|
||||||
|
_ident1 == 0x14 &&
|
||||||
|
_ident2 == 0xF5 &&
|
||||||
|
_ident3 == 0xEE)) &&
|
||||||
_ident4 == 0x00 &&
|
_ident4 == 0x00 &&
|
||||||
_ident5 == 0x01 &&
|
_ident5 == 0x01 &&
|
||||||
_ident6 == 0x01 &&
|
_ident6 == 0x01 &&
|
||||||
|
|||||||
@@ -4,11 +4,12 @@
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using SharpEmu.Core.Loader;
|
using SharpEmu.Core.Loader;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
using SharpEmu.Logging;
|
using SharpEmu.Logging;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Memory;
|
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");
|
private static readonly SharpEmuLogger Log = SharpEmuLog.For("VMEM");
|
||||||
|
|
||||||
@@ -28,41 +29,27 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
private const ulong DefaultLazyReservePrimeBytes = 0x0400_0000UL; // 64 MiB
|
private const ulong DefaultLazyReservePrimeBytes = 0x0400_0000UL; // 64 MiB
|
||||||
private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB
|
private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB
|
||||||
|
|
||||||
private const uint MEM_COMMIT = 0x1000;
|
// Raw Windows PAGE_* values retained for the internal region/protection
|
||||||
private const uint MEM_RESERVE = 0x2000;
|
// bookkeeping: regions and saved old-protection values always carry the raw
|
||||||
private const uint MEM_RELEASE = 0x8000;
|
// 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_READ = 0x20;
|
||||||
private const uint PAGE_EXECUTE_READWRITE = 0x40;
|
private const uint PAGE_EXECUTE_READWRITE = 0x40;
|
||||||
private const uint PAGE_EXECUTE = 0x10;
|
private const uint PAGE_EXECUTE = 0x10;
|
||||||
private const uint PAGE_EXECUTE_WRITECOPY = 0x80;
|
private const uint PAGE_EXECUTE_WRITECOPY = 0x80;
|
||||||
private const uint PAGE_NOACCESS = 0x01;
|
|
||||||
private const uint PAGE_READWRITE = 0x04;
|
private const uint PAGE_READWRITE = 0x04;
|
||||||
private const uint PAGE_READONLY = 0x02;
|
private const uint PAGE_READONLY = 0x02;
|
||||||
|
|
||||||
|
private readonly IHostMemory _hostMemory;
|
||||||
private ulong _guestAllocationArenaBase;
|
private ulong _guestAllocationArenaBase;
|
||||||
private ulong _guestAllocationOffset;
|
private readonly SortedDictionary<ulong, ulong> _guestAllocationFreeRanges = new();
|
||||||
|
private readonly Dictionary<ulong, (ulong Offset, ulong Size)> _guestAllocations = new();
|
||||||
private static readonly ulong LazyReservePrimeBytes = ResolveLazyReservePrimeBytes();
|
private static readonly ulong LazyReservePrimeBytes = ResolveLazyReservePrimeBytes();
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
public PhysicalVirtualMemory(IHostMemory? hostMemory = null)
|
||||||
private static extern void* VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
{
|
||||||
|
_hostMemory = hostMemory ?? HostPlatform.Current.Memory;
|
||||||
[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 bool TryAllocateAtExact(ulong desiredAddress, ulong size, bool executable, out ulong actualAddress)
|
public bool TryAllocateAtExact(ulong desiredAddress, ulong size, bool executable, out ulong actualAddress)
|
||||||
{
|
{
|
||||||
@@ -74,17 +61,17 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
var alignedSize = (size + 0xFFF) & ~0xFFFUL;
|
var alignedSize = (size + 0xFFF) & ~0xFFFUL;
|
||||||
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
||||||
var allocationType = MEM_COMMIT | MEM_RESERVE;
|
var hostProtection = executable ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite;
|
||||||
var result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, allocationType, protection);
|
var result = _hostMemory.Allocate(desiredAddress, alignedSize, hostProtection);
|
||||||
if (result == null)
|
if (result == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
actualAddress = (ulong)result;
|
actualAddress = result;
|
||||||
if (actualAddress != desiredAddress)
|
if (actualAddress != desiredAddress)
|
||||||
{
|
{
|
||||||
VirtualFree(result, 0, MEM_RELEASE);
|
_hostMemory.Free(result);
|
||||||
actualAddress = 0;
|
actualAddress = 0;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -119,33 +106,33 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
var alignedSize = (size + 0xFFF) & ~0xFFFUL;
|
var alignedSize = (size + 0xFFF) & ~0xFFFUL;
|
||||||
|
|
||||||
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
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 reservedOnly = false;
|
||||||
var preferReserveOnly = !executable &&
|
var preferReserveOnly = !executable &&
|
||||||
alignedSize >= LargeDataReserveThreshold &&
|
alignedSize >= LargeDataReserveThreshold &&
|
||||||
alignedSize > FullCommitRegionLimit;
|
alignedSize > FullCommitRegionLimit;
|
||||||
|
|
||||||
void* result = null;
|
ulong result = 0;
|
||||||
if (preferReserveOnly)
|
if (preferReserveOnly)
|
||||||
{
|
{
|
||||||
result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, MEM_RESERVE, PAGE_READWRITE);
|
result = _hostMemory.Reserve(desiredAddress, alignedSize, HostPageProtection.ReadWrite);
|
||||||
if (result == null && allowAlternative)
|
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;
|
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)
|
if (!allowAlternative)
|
||||||
{
|
{
|
||||||
@@ -153,32 +140,32 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
|
|
||||||
TraceVmem($"Could not allocate at 0x{desiredAddress:X16}, trying any address...");
|
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)
|
if (!executable)
|
||||||
{
|
{
|
||||||
result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, MEM_RESERVE, PAGE_READWRITE);
|
result = _hostMemory.Reserve(desiredAddress, alignedSize, HostPageProtection.ReadWrite);
|
||||||
if (result == null && allowAlternative)
|
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;
|
reservedOnly = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result == null)
|
if (result == 0)
|
||||||
{
|
{
|
||||||
throw new OutOfMemoryException($"Failed to allocate {alignedSize} bytes of virtual memory");
|
throw new OutOfMemoryException($"Failed to allocate {alignedSize} bytes of virtual memory");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var actualAddress = (ulong)result;
|
var actualAddress = result;
|
||||||
|
|
||||||
var lazyPrimeState = "n/a";
|
var lazyPrimeState = "n/a";
|
||||||
if (reservedOnly)
|
if (reservedOnly)
|
||||||
@@ -191,9 +178,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
{
|
{
|
||||||
var remaining = primeBytes - committedBytes;
|
var remaining = primeBytes - committedBytes;
|
||||||
var chunkBytes = Math.Min(remaining, LazyReservePrimeChunkBytes);
|
var chunkBytes = Math.Min(remaining, LazyReservePrimeChunkBytes);
|
||||||
var commitAddress = (void*)(actualAddress + committedBytes);
|
var commitAddress = actualAddress + committedBytes;
|
||||||
var committed = VirtualAlloc(commitAddress, (nuint)chunkBytes, MEM_COMMIT, PAGE_READWRITE);
|
if (!_hostMemory.Commit(commitAddress, chunkBytes, HostPageProtection.ReadWrite))
|
||||||
if (committed == null)
|
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -316,7 +302,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
GuestAllocationArenaSize,
|
GuestAllocationArenaSize,
|
||||||
executable: false,
|
executable: false,
|
||||||
allowAlternative: true);
|
allowAlternative: true);
|
||||||
_guestAllocationOffset = GuestAllocationArenaStartOffset;
|
_guestAllocationFreeRanges.Add(
|
||||||
|
GuestAllocationArenaStartOffset,
|
||||||
|
GuestAllocationArenaSize - GuestAllocationArenaStartOffset);
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
@@ -324,18 +312,128 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var alignedOffset = AlignUp(_guestAllocationOffset, alignment);
|
ulong rangeOffset = 0;
|
||||||
if (alignedOffset > GuestAllocationArenaSize || size > GuestAllocationArenaSize - alignedOffset)
|
ulong rangeSize = 0;
|
||||||
|
ulong alignedOffset = 0;
|
||||||
|
var found = false;
|
||||||
|
foreach (var range in _guestAllocationFreeRanges)
|
||||||
|
{
|
||||||
|
alignedOffset = AlignUp(range.Key, alignment);
|
||||||
|
if (alignedOffset >= range.Key &&
|
||||||
|
alignedOffset - range.Key <= range.Value &&
|
||||||
|
size <= range.Value - (alignedOffset - range.Key))
|
||||||
|
{
|
||||||
|
rangeOffset = range.Key;
|
||||||
|
rangeSize = range.Value;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_guestAllocationFreeRanges.Remove(rangeOffset);
|
||||||
|
if (alignedOffset > rangeOffset)
|
||||||
|
{
|
||||||
|
_guestAllocationFreeRanges.Add(rangeOffset, alignedOffset - rangeOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
var allocationEnd = alignedOffset + size;
|
||||||
|
var rangeEnd = rangeOffset + rangeSize;
|
||||||
|
if (allocationEnd < rangeEnd)
|
||||||
|
{
|
||||||
|
_guestAllocationFreeRanges.Add(allocationEnd, rangeEnd - allocationEnd);
|
||||||
|
}
|
||||||
|
|
||||||
address = _guestAllocationArenaBase + alignedOffset;
|
address = _guestAllocationArenaBase + alignedOffset;
|
||||||
_guestAllocationOffset = alignedOffset + size;
|
_guestAllocations.Add(address, (alignedOffset, size));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool TryFreeGuestMemory(ulong address)
|
||||||
|
{
|
||||||
|
lock (_guestAllocationGate)
|
||||||
|
{
|
||||||
|
if (!_guestAllocations.Remove(address, out var allocation))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var freeOffset = allocation.Offset;
|
||||||
|
var freeSize = allocation.Size;
|
||||||
|
ulong? previousOffset = null;
|
||||||
|
ulong? nextOffset = null;
|
||||||
|
|
||||||
|
foreach (var range in _guestAllocationFreeRanges)
|
||||||
|
{
|
||||||
|
if (range.Key < freeOffset)
|
||||||
|
{
|
||||||
|
previousOffset = range.Key;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextOffset = range.Key;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previousOffset is { } previous &&
|
||||||
|
previous + _guestAllocationFreeRanges[previous] == freeOffset)
|
||||||
|
{
|
||||||
|
freeOffset = previous;
|
||||||
|
freeSize += _guestAllocationFreeRanges[previous];
|
||||||
|
_guestAllocationFreeRanges.Remove(previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextOffset is { } next && freeOffset + freeSize == next)
|
||||||
|
{
|
||||||
|
freeSize += _guestAllocationFreeRanges[next];
|
||||||
|
_guestAllocationFreeRanges.Remove(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
_guestAllocationFreeRanges.Add(freeOffset, freeSize);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
public void Clear()
|
||||||
{
|
{
|
||||||
lock (_guestAllocationGate)
|
lock (_guestAllocationGate)
|
||||||
@@ -345,7 +443,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
{
|
{
|
||||||
foreach (var region in _regions)
|
foreach (var region in _regions)
|
||||||
{
|
{
|
||||||
VirtualFree((void*)region.VirtualAddress, 0, MEM_RELEASE);
|
_hostMemory.Free(region.VirtualAddress);
|
||||||
}
|
}
|
||||||
_regions.Clear();
|
_regions.Clear();
|
||||||
_pageProtections.Clear();
|
_pageProtections.Clear();
|
||||||
@@ -360,7 +458,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
|
|
||||||
_guestAllocationArenaBase = 0;
|
_guestAllocationArenaBase = 0;
|
||||||
_guestAllocationOffset = 0;
|
_guestAllocationFreeRanges.Clear();
|
||||||
|
_guestAllocations.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,46 +518,67 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
private void ApplySegmentProtection(ulong mapStart, ulong mapEnd, ProgramHeaderFlags flags)
|
private void ApplySegmentProtection(ulong mapStart, ulong mapEnd, ProgramHeaderFlags flags)
|
||||||
{
|
{
|
||||||
|
var runStart = mapStart;
|
||||||
|
var runFlags = ProgramHeaderFlags.None;
|
||||||
|
var hasRun = false;
|
||||||
|
|
||||||
for (var pageAddress = mapStart; pageAddress < mapEnd; pageAddress += PageSize)
|
for (var pageAddress = mapStart; pageAddress < mapEnd; pageAddress += PageSize)
|
||||||
{
|
{
|
||||||
_pageProtections.TryGetValue(pageAddress, out var existingFlags);
|
_pageProtections.TryGetValue(pageAddress, out var existingFlags);
|
||||||
var mergedFlags = existingFlags | flags;
|
var mergedFlags = existingFlags | flags;
|
||||||
_pageProtections[pageAddress] = mergedFlags;
|
_pageProtections[pageAddress] = mergedFlags;
|
||||||
SetProtection(pageAddress, PageSize, mergedFlags);
|
|
||||||
|
if (!hasRun)
|
||||||
|
{
|
||||||
|
runStart = pageAddress;
|
||||||
|
runFlags = mergedFlags;
|
||||||
|
hasRun = true;
|
||||||
|
}
|
||||||
|
else if (mergedFlags != runFlags)
|
||||||
|
{
|
||||||
|
SetProtection(runStart, pageAddress - runStart, runFlags);
|
||||||
|
runStart = pageAddress;
|
||||||
|
runFlags = mergedFlags;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasRun)
|
||||||
|
{
|
||||||
|
SetProtection(runStart, mapEnd - runStart, runFlags);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetProtection(ulong address, ulong size, ProgramHeaderFlags flags)
|
private void SetProtection(ulong address, ulong size, ProgramHeaderFlags flags)
|
||||||
{
|
{
|
||||||
uint protection;
|
HostPageProtection protection;
|
||||||
|
|
||||||
if (flags == ProgramHeaderFlags.None)
|
if (flags == ProgramHeaderFlags.None)
|
||||||
{
|
{
|
||||||
protection = PAGE_NOACCESS;
|
protection = HostPageProtection.NoAccess;
|
||||||
}
|
}
|
||||||
else if ((flags & ProgramHeaderFlags.Execute) != 0)
|
else if ((flags & ProgramHeaderFlags.Execute) != 0)
|
||||||
{
|
{
|
||||||
protection = (flags & ProgramHeaderFlags.Write) != 0
|
protection = (flags & ProgramHeaderFlags.Write) != 0
|
||||||
? PAGE_EXECUTE_READWRITE
|
? HostPageProtection.ReadWriteExecute
|
||||||
: PAGE_EXECUTE_READ;
|
: HostPageProtection.ReadExecute;
|
||||||
}
|
}
|
||||||
else if ((flags & ProgramHeaderFlags.Write) != 0)
|
else if ((flags & ProgramHeaderFlags.Write) != 0)
|
||||||
{
|
{
|
||||||
protection = PAGE_READWRITE;
|
protection = HostPageProtection.ReadWrite;
|
||||||
}
|
}
|
||||||
else
|
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}");
|
throw new InvalidOperationException($"Failed to set memory protection at 0x{address:X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((flags & ProgramHeaderFlags.Execute) != 0)
|
if ((flags & ProgramHeaderFlags.Execute) != 0)
|
||||||
{
|
{
|
||||||
FlushInstructionCache(GetCurrentProcess(), (void*)address, (nuint)size);
|
_hostMemory.FlushInstructionCache(address, size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,6 +670,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)
|
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
|
||||||
{
|
{
|
||||||
var requiresExclusiveAccess = false;
|
var requiresExclusiveAccess = false;
|
||||||
@@ -689,7 +850,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return true;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -703,10 +864,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _);
|
_hostMemory.ProtectRaw((ulong)destPtr, (ulong)source.Length, oldProtect, out _);
|
||||||
if (IsExecutableProtection(oldProtect))
|
if (IsExecutableProtection(oldProtect))
|
||||||
{
|
{
|
||||||
FlushInstructionCache(GetCurrentProcess(), destPtr, (nuint)source.Length);
|
_hostMemory.FlushInstructionCache((ulong)destPtr, (ulong)source.Length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -728,9 +889,14 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
_gate.EnterReadLock();
|
_gate.EnterReadLock();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return FindRegion(virtualAddress, 1) is not null
|
var region = FindRegion(virtualAddress, 1);
|
||||||
? (void*)virtualAddress
|
if (region is null ||
|
||||||
: null;
|
(region.IsReservedOnly && !EnsureRangeCommitted(virtualAddress, 1, region)))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (void*)virtualAddress;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -932,12 +1098,12 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return protection is PAGE_READWRITE or PAGE_EXECUTE_READWRITE;
|
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)
|
if (size == 0 || !region.IsReservedOnly)
|
||||||
{
|
{
|
||||||
@@ -951,7 +1117,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
var pageAddress = startPage;
|
var pageAddress = startPage;
|
||||||
while (pageAddress < endPage)
|
while (pageAddress < endPage)
|
||||||
{
|
{
|
||||||
if (VirtualQuery((void*)pageAddress, out var info, (nuint)sizeof(MemoryBasicInformation64)) == 0)
|
if (!_hostMemory.Query(pageAddress, out var info))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -965,19 +1131,19 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info.State == MEM_COMMIT)
|
if (info.State == HostRegionState.Committed)
|
||||||
{
|
{
|
||||||
pageAddress = rangeEnd;
|
pageAddress = rangeEnd;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info.State != MEM_RESERVE)
|
if (info.State != HostRegionState.Reserved)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var commitSize = rangeEnd - pageAddress;
|
var commitSize = rangeEnd - pageAddress;
|
||||||
if (VirtualAlloc((void*)pageAddress, (nuint)commitSize, MEM_COMMIT, commitProtection) == null)
|
if (!_hostMemory.Commit(pageAddress, commitSize, commitProtection))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -998,11 +1164,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
var startPage = AlignDown(address, PageSize);
|
var startPage = AlignDown(address, PageSize);
|
||||||
var endPage = AlignUp(address + size, 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)
|
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);
|
RestorePageProtections(touchedPages);
|
||||||
touchedPages.Clear();
|
touchedPages.Clear();
|
||||||
@@ -1015,11 +1181,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return true;
|
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)
|
foreach (var (pageAddress, protection) in touchedPages)
|
||||||
{
|
{
|
||||||
VirtualProtect((void*)pageAddress, (nuint)PageSize, protection, out _);
|
_hostMemory.ProtectRaw(pageAddress, PageSize, protection, out _);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1076,16 +1242,4 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
public uint Protection { get; set; }
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,15 +41,14 @@ public sealed class VirtualMemory : IVirtualMemory
|
|||||||
|
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
foreach (var existing in _regions)
|
var insertionIndex = FindInsertionIndex(virtualAddress);
|
||||||
|
if ((insertionIndex > 0 && virtualAddress < _regions[insertionIndex - 1].EndAddress) ||
|
||||||
|
(insertionIndex < _regions.Count && endAddress > _regions[insertionIndex].Region.VirtualAddress))
|
||||||
{
|
{
|
||||||
if (virtualAddress < existing.EndAddress && endAddress > existing.Region.VirtualAddress)
|
throw new InvalidOperationException("Attempted to map an overlapping virtual memory region.");
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Attempted to map an overlapping virtual memory region.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_regions.Add(new MappedRegion(
|
_regions.Insert(insertionIndex, new MappedRegion(
|
||||||
new VirtualMemoryRegion(virtualAddress, memorySize, fileOffset, (ulong)fileData.Length, protection),
|
new VirtualMemoryRegion(virtualAddress, memorySize, fileOffset, (ulong)fileData.Length, protection),
|
||||||
endAddress,
|
endAddress,
|
||||||
backingMemory));
|
backingMemory));
|
||||||
@@ -74,12 +73,12 @@ public sealed class VirtualMemory : IVirtualMemory
|
|||||||
{
|
{
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
if (!TryResolveRegion(virtualAddress, destination.Length, out var region, out var offset))
|
if (!TryValidateRange(virtualAddress, destination.Length, ProgramHeaderFlags.Read, out var regionIndex))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
region.BackingMemory.AsSpan(offset, destination.Length).CopyTo(destination);
|
CopyFromRegions(virtualAddress, destination, regionIndex);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,39 +87,127 @@ public sealed class VirtualMemory : IVirtualMemory
|
|||||||
{
|
{
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
if (!TryResolveRegion(virtualAddress, source.Length, out var region, out var offset))
|
if (!TryValidateRange(virtualAddress, source.Length, ProgramHeaderFlags.Write, out var regionIndex))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
source.CopyTo(region.BackingMemory.AsSpan(offset, source.Length));
|
CopyToRegions(virtualAddress, source, regionIndex);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryResolveRegion(ulong virtualAddress, int length, out MappedRegion region, out int offset)
|
private bool TryValidateRange(
|
||||||
|
ulong virtualAddress,
|
||||||
|
int length,
|
||||||
|
ProgramHeaderFlags requiredProtection,
|
||||||
|
out int regionIndex)
|
||||||
{
|
{
|
||||||
foreach (var candidate in _regions)
|
regionIndex = FindContainingRegionIndex(virtualAddress);
|
||||||
|
if (regionIndex < 0)
|
||||||
{
|
{
|
||||||
if (virtualAddress < candidate.Region.VirtualAddress || virtualAddress >= candidate.EndAddress)
|
return false;
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var candidateOffset = checked((int)(virtualAddress - candidate.Region.VirtualAddress));
|
|
||||||
if (candidateOffset + length > candidate.BackingMemory.Length)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
region = candidate;
|
|
||||||
offset = candidateOffset;
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
region = default;
|
var currentAddress = virtualAddress;
|
||||||
offset = 0;
|
var remaining = length;
|
||||||
return false;
|
var currentIndex = regionIndex;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (currentIndex >= _regions.Count)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var region = _regions[currentIndex];
|
||||||
|
if (currentAddress < region.Region.VirtualAddress ||
|
||||||
|
currentAddress >= region.EndAddress ||
|
||||||
|
(region.Region.Protection & requiredProtection) == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remaining == 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var available = region.EndAddress - currentAddress;
|
||||||
|
var chunkLength = (int)Math.Min((ulong)remaining, available);
|
||||||
|
remaining -= chunkLength;
|
||||||
|
if (remaining == 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentAddress += (ulong)chunkLength;
|
||||||
|
currentIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int FindContainingRegionIndex(ulong virtualAddress)
|
||||||
|
{
|
||||||
|
var insertionIndex = FindInsertionIndex(virtualAddress);
|
||||||
|
if (insertionIndex < _regions.Count &&
|
||||||
|
_regions[insertionIndex].Region.VirtualAddress == virtualAddress)
|
||||||
|
{
|
||||||
|
return insertionIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidateIndex = insertionIndex - 1;
|
||||||
|
return candidateIndex >= 0 && virtualAddress < _regions[candidateIndex].EndAddress
|
||||||
|
? candidateIndex
|
||||||
|
: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CopyFromRegions(ulong virtualAddress, Span<byte> destination, int regionIndex)
|
||||||
|
{
|
||||||
|
var copied = 0;
|
||||||
|
var currentAddress = virtualAddress;
|
||||||
|
while (copied < destination.Length)
|
||||||
|
{
|
||||||
|
var region = _regions[regionIndex++];
|
||||||
|
var regionOffset = checked((int)(currentAddress - region.Region.VirtualAddress));
|
||||||
|
var chunkLength = Math.Min(destination.Length - copied, region.BackingMemory.Length - regionOffset);
|
||||||
|
region.BackingMemory.AsSpan(regionOffset, chunkLength).CopyTo(destination[copied..]);
|
||||||
|
copied += chunkLength;
|
||||||
|
currentAddress += (ulong)chunkLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CopyToRegions(ulong virtualAddress, ReadOnlySpan<byte> source, int regionIndex)
|
||||||
|
{
|
||||||
|
var copied = 0;
|
||||||
|
var currentAddress = virtualAddress;
|
||||||
|
while (copied < source.Length)
|
||||||
|
{
|
||||||
|
var region = _regions[regionIndex++];
|
||||||
|
var regionOffset = checked((int)(currentAddress - region.Region.VirtualAddress));
|
||||||
|
var chunkLength = Math.Min(source.Length - copied, region.BackingMemory.Length - regionOffset);
|
||||||
|
source.Slice(copied, chunkLength).CopyTo(region.BackingMemory.AsSpan(regionOffset, chunkLength));
|
||||||
|
copied += chunkLength;
|
||||||
|
currentAddress += (ulong)chunkLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int FindInsertionIndex(ulong virtualAddress)
|
||||||
|
{
|
||||||
|
var lower = 0;
|
||||||
|
var upper = _regions.Count;
|
||||||
|
while (lower < upper)
|
||||||
|
{
|
||||||
|
var middle = lower + ((upper - lower) / 2);
|
||||||
|
if (_regions[middle].Region.VirtualAddress < virtualAddress)
|
||||||
|
{
|
||||||
|
lower = middle + 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
upper = middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lower;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly record struct MappedRegion(VirtualMemoryRegion Region, ulong EndAddress, byte[] BackingMemory);
|
private readonly record struct MappedRegion(VirtualMemoryRegion Region, ulong EndAddress, byte[] BackingMemory);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using SharpEmu.Core.Cpu.Disasm;
|
|||||||
using SharpEmu.Core.Loader;
|
using SharpEmu.Core.Loader;
|
||||||
using SharpEmu.Core.Memory;
|
using SharpEmu.Core.Memory;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
using SharpEmu.Libs.VideoOut;
|
using SharpEmu.Libs.VideoOut;
|
||||||
using SharpEmu.Libs.Kernel;
|
using SharpEmu.Libs.Kernel;
|
||||||
using SharpEmu.Libs.AppContent;
|
using SharpEmu.Libs.AppContent;
|
||||||
@@ -86,14 +87,19 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
moduleManager.RegisterFromAssembly(typeof(KernelExports).Assembly, Generation.Gen4 | Generation.Gen5, Aerolib.Instance);
|
moduleManager.RegisterFromAssembly(typeof(KernelExports).Assembly, Generation.Gen4 | Generation.Gen5, Aerolib.Instance);
|
||||||
moduleManager.Freeze();
|
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();
|
var fileSystem = new PhysicalFileSystem();
|
||||||
|
|
||||||
return new SharpEmuRuntime(
|
return new SharpEmuRuntime(
|
||||||
new SelfLoader(),
|
new SelfLoader(),
|
||||||
virtualMemory,
|
virtualMemory,
|
||||||
new CpuDispatcher(virtualMemory, moduleManager),
|
new CpuDispatcher(virtualMemory, moduleManager, hostPlatform: hostPlatform),
|
||||||
moduleManager,
|
moduleManager,
|
||||||
Aerolib.Instance,
|
Aerolib.Instance,
|
||||||
cpuExecutionOptions,
|
cpuExecutionOptions,
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ public sealed class GuiSettings
|
|||||||
/// <summary>Publish launcher/game status to Discord Rich Presence.</summary>
|
/// <summary>Publish launcher/game status to Discord Rich Presence.</summary>
|
||||||
public bool DiscordRichPresence { get; set; } = true;
|
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>
|
/// <summary>
|
||||||
/// Discord application ID used for Rich Presence; the default is the
|
/// Discord application ID used for Rich Presence; the default is the
|
||||||
/// SharpEmu application. Override to rebrand what Discord shows as
|
/// SharpEmu application. Override to rebrand what Discord shows as
|
||||||
|
|||||||
@@ -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,138 @@
|
|||||||
|
{
|
||||||
|
"_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.Env.Tab": "Ambiente",
|
||||||
|
"Options.Section.Environment": "VARIÁVEIS DE AMBIENTE",
|
||||||
|
"Options.Env.Desc": "Switches passados para o emulador como variáveis de ambiente na inicialização.",
|
||||||
|
"Options.Env.Bthid.Desc": "Reporta o Bluetooth HID como indisponível para títulos cujo middleware de volante/FFB fica esperando indefinidamente.\nDeixe desativado normalmente. Alguns títulos travam quando a inicialização falha.",
|
||||||
|
"Options.Env.LoopGuard.Desc": "Não force o encerramento de títulos que repetem a mesma chamada por tempo demais.\nExperimente isso quando um jogo fecha sozinho durante o carregamento.",
|
||||||
|
"Options.Env.VkValidation.Desc": "Ativa as camadas de validação do Vulkan para depuração de GPU.\nLento. Requer que o Vulkan SDK esteja instalado.",
|
||||||
|
"Options.Env.DumpSpirv.Desc": "Extrai os shaders AGC e suas traduções SPIR-V para a pasta shader-dumps.\nUse ao reportar bugs de shader ou renderização.",
|
||||||
|
"Options.Env.LogDirectMemory.Desc": "Registra alocações de memória direta e falhas no console.\nUse quando um jogo aborta ou fecha durante a inicialização (boot).",
|
||||||
|
"Options.Env.LogNp.Desc": "Registra chamadas da biblioteca NP (PlayStation Network) no console.",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
@@ -26,6 +26,15 @@
|
|||||||
"Library.Loading": "Loading library…",
|
"Library.Loading": "Loading library…",
|
||||||
|
|
||||||
"Options.General": "General",
|
"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.Emulation": "EMULATION",
|
||||||
"Options.Section.Logging": "LOGGING",
|
"Options.Section.Logging": "LOGGING",
|
||||||
"Options.Section.Launcher": "LAUNCHER",
|
"Options.Section.Launcher": "LAUNCHER",
|
||||||
@@ -125,5 +134,13 @@
|
|||||||
"Dialog.PsExecutables": "PS executables",
|
"Dialog.PsExecutables": "PS executables",
|
||||||
"Dialog.SaveLogFile": "Select where to save the Log file",
|
"Dialog.SaveLogFile": "Select where to save the Log file",
|
||||||
"Dialog.PlainTextFiles": "Plain Text Files",
|
"Dialog.PlainTextFiles": "Plain Text Files",
|
||||||
"Dialog.LogFiles": "Log 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,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,146 @@
|
|||||||
|
{
|
||||||
|
"_languageName": "Português (Portugal)",
|
||||||
|
|
||||||
|
"Page.Library": "Biblioteca",
|
||||||
|
"Page.Options": "Opções",
|
||||||
|
"Page.GameCount.One": "1 jogo",
|
||||||
|
"Page.GameCount.Other": "{0} jogos",
|
||||||
|
|
||||||
|
"Library.SearchWatermark": "Pesquisar biblioteca…",
|
||||||
|
"Library.AddFolder": "+ Adicionar pasta",
|
||||||
|
"Library.Rescan": "⟳ Reanalisar",
|
||||||
|
"Library.OpenFile": "Abrir ficheiro…",
|
||||||
|
|
||||||
|
"Library.Context.Launch": "Iniciar",
|
||||||
|
"Library.Context.OpenFolder": "Abrir pasta do jogo",
|
||||||
|
"Library.Context.CopyPath": "Copiar caminho",
|
||||||
|
"Library.Context.CopyTitleId": "Copiar ID do título",
|
||||||
|
"Library.Context.Remove": "Remover da biblioteca",
|
||||||
|
|
||||||
|
"Library.Empty.Title": "A sua biblioteca está vazia",
|
||||||
|
"Library.Empty.Hint": "Adicione uma pasta com os seus jogos para começar.",
|
||||||
|
"Library.Empty.SearchTitle": "Nenhum jogo corresponde à sua pesquisa",
|
||||||
|
"Library.Empty.SearchHint": "Nada na biblioteca corresponde a “{0}”.",
|
||||||
|
"Library.Empty.AddFolder": "+ Adicionar pasta de jogos",
|
||||||
|
|
||||||
|
"Library.Loading": "A carregar biblioteca…",
|
||||||
|
|
||||||
|
"Options.General": "Geral",
|
||||||
|
"Options.Env.Tab": "Ambiente",
|
||||||
|
"Options.Section.Environment": "VARIÁVEIS DE AMBIENTE",
|
||||||
|
"Options.Env.Desc": "Switches passados ao emulador como variáveis de ambiente no arranque.",
|
||||||
|
"Options.Env.Bthid.Desc": "Reporta o Bluetooth HID como indisponível para títulos cujo middleware de volante/FFB fica à espera indefinidamente.\nDeixe desativado normalmente. Alguns títulos bloqueiam quando a inicialização falha.",
|
||||||
|
"Options.Env.LoopGuard.Desc": "Não force o encerramento de títulos que repetem a mesma chamada durante demasiado tempo.\nExperimente isto quando um jogo fecha sozinho durante o carregamento.",
|
||||||
|
"Options.Env.VkValidation.Desc": "Ativa as camadas de validação do Vulkan para depuração da GPU.\nLento. Requer que o Vulkan SDK esteja instalado.",
|
||||||
|
"Options.Env.DumpSpirv.Desc": "Extrai os shaders AGC e as respetivas traduções SPIR-V para a pasta shader-dumps.\nUtilize ao reportar problemas de shaders ou renderização.",
|
||||||
|
"Options.Env.LogDirectMemory.Desc": "Regista alocações de memória direta e falhas na consola.\nUtilize quando um jogo aborta ou fecha durante o arranque.",
|
||||||
|
"Options.Env.LogNp.Desc": "Regista chamadas da biblioteca NP (PlayStation Network) na consola.",
|
||||||
|
"Options.Section.Emulation": "EMULAÇÃO",
|
||||||
|
"Options.Section.Logging": "REGISTOS",
|
||||||
|
"Options.Section.Launcher": "LANÇADOR",
|
||||||
|
|
||||||
|
"Options.CpuEngine.Label": "Motor de CPU",
|
||||||
|
"Options.CpuEngine.Desc": "Motor de execução utilizado para correr o código do jogo.",
|
||||||
|
"Options.CpuEngine.Native": "Nativo",
|
||||||
|
|
||||||
|
"Options.Strict.Label": "Resolução estrita de dynlib",
|
||||||
|
"Options.Strict.Desc": "Falha o arranque quando um símbolo importado não pode ser resolvido.",
|
||||||
|
|
||||||
|
"Options.LogLevel.Label": "Nível de registo",
|
||||||
|
"Options.LogLevel.Desc": "Nível de detalhe da saída da consola do emulador.",
|
||||||
|
"Options.LogLevel.Trace": "Rastreio",
|
||||||
|
"Options.LogLevel.Debug": "Depuração",
|
||||||
|
"Options.LogLevel.Info": "Informação",
|
||||||
|
"Options.LogLevel.Warning": "Aviso",
|
||||||
|
"Options.LogLevel.Error": "Erro",
|
||||||
|
"Options.LogLevel.Critical": "Crítico",
|
||||||
|
|
||||||
|
"Options.TraceImports.Label": "Limite de rastreio de importações",
|
||||||
|
"Options.TraceImports.Desc": "Rastreia as primeiras N importações por módulo (0 = desativado).",
|
||||||
|
|
||||||
|
"Options.LogToFile.Label": "Registar para ficheiro",
|
||||||
|
"Options.LogToFile.Desc": "Duplicar a saída do emulador para um ficheiro de registo.",
|
||||||
|
|
||||||
|
"Options.LogFilePath.Label": "Caminho do ficheiro de registo",
|
||||||
|
"Options.LogFilePath.Default": "Sem caminho personalizado — os registos vão para user/logs junto ao emulador.",
|
||||||
|
"Options.LogFilePath.Select": "Selecionar…",
|
||||||
|
|
||||||
|
"Options.OverrideLogFile.Label": "Substituir ficheiro de registo",
|
||||||
|
"Options.OverrideLogFile.Desc": "Utilizar o caminho de ficheiro exato em vez de acrescentar o ID do título e a hora.",
|
||||||
|
|
||||||
|
"Options.TitleMusic.Label": "Música do título",
|
||||||
|
"Options.TitleMusic.Desc": "Repetir em loop a música de pré-visualização do jogo selecionado na biblioteca.",
|
||||||
|
|
||||||
|
"Options.Discord.Label": "Presença no Discord",
|
||||||
|
"Options.Discord.Desc": "Mostrar o jogo em execução no seu perfil do Discord.",
|
||||||
|
|
||||||
|
"Options.Language.Label": "Idioma do emulador",
|
||||||
|
"Options.Language.Desc": "Idioma utilizado em todo o lançador. Aplica-se de imediato.",
|
||||||
|
|
||||||
|
"Common.On": "Ativado",
|
||||||
|
"Common.Off": "Desativado",
|
||||||
|
|
||||||
|
"Console.Title": "CONSOLA",
|
||||||
|
"Console.SearchWatermark": "Pesquisar...",
|
||||||
|
"Console.AutoScroll": "Deslocamento automático",
|
||||||
|
"Console.Split": "Dividir",
|
||||||
|
"Console.Copy": "Copiar",
|
||||||
|
"Console.Clear": "Limpar",
|
||||||
|
"Console.WindowTitle": "Consola do SharpEmu",
|
||||||
|
|
||||||
|
"Launch.NoGameSelected": "Nenhum jogo selecionado",
|
||||||
|
"Launch.NoGameHint": "Escolha um jogo da biblioteca ou abra um eboot.bin diretamente.",
|
||||||
|
"Launch.Idle": "Inativo",
|
||||||
|
"Launch.Console": "≡ Consola",
|
||||||
|
"Launch.Launch": "▶ Iniciar",
|
||||||
|
"Launch.Stop": "■ Parar",
|
||||||
|
"Launch.Running": "Em execução — {0}",
|
||||||
|
"Launch.Stopping": "A parar…",
|
||||||
|
"Launch.Exited": "Terminou com o código {0} ({1})",
|
||||||
|
"Launch.ExeNotFound": "Executável do SharpEmu não encontrado. Compile primeiro o projeto SharpEmu.CLI (dotnet build).",
|
||||||
|
"Launch.LogFile": "Ficheiro de registo: {0}",
|
||||||
|
"Launch.Command": "$ SharpEmu {0}",
|
||||||
|
"Launch.StartFailed": "Falha ao iniciar o emulador: {0}",
|
||||||
|
"Launch.ProcessExited": "O processo terminou com o código {0} ({1}).",
|
||||||
|
|
||||||
|
"Exit.Ok": "OK",
|
||||||
|
"Exit.InvalidArguments": "argumentos inválidos",
|
||||||
|
"Exit.EbootNotFound": "eboot não encontrado",
|
||||||
|
"Exit.RuntimeException": "exceção em tempo de execução",
|
||||||
|
"Exit.EmulationError": "erro de emulação",
|
||||||
|
"Exit.Unknown": "desconhecido",
|
||||||
|
|
||||||
|
"Status.EmulatorLocating": "Emulador: a localizar…",
|
||||||
|
"Status.EmulatorPath": "Emulador: {0}",
|
||||||
|
"Status.EmulatorNotFound": "Emulador: executável do SharpEmu não encontrado — compile primeiro o SharpEmu.CLI.",
|
||||||
|
"Status.ScanningLibrary": "A analisar biblioteca…",
|
||||||
|
"Status.AddFolderPrompt": "Adicione uma pasta de jogos para preencher a biblioteca.",
|
||||||
|
"Status.LibraryScanned": "Biblioteca analisada: {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 a pasta para o restaurar.",
|
||||||
|
"Status.Running": "A executar {0}",
|
||||||
|
"Status.Stopping": "A parar…",
|
||||||
|
"Status.Idle": "Inativo",
|
||||||
|
|
||||||
|
"Clipboard.Path": "Caminho",
|
||||||
|
"Clipboard.TitleId": "ID do título",
|
||||||
|
|
||||||
|
"Discord.Playing": "A jogar {0}",
|
||||||
|
"Discord.Browsing": "A navegar na biblioteca",
|
||||||
|
|
||||||
|
"Dialog.ChooseGameFolder": "Escolha uma pasta com jogos",
|
||||||
|
"Dialog.OpenExecutable": "Abrir um executável para iniciar",
|
||||||
|
"Dialog.PsExecutables": "Executáveis PS",
|
||||||
|
"Dialog.SaveLogFile": "Selecione onde guardar o ficheiro de registo",
|
||||||
|
"Dialog.PlainTextFiles": "Ficheiros de Texto Simples",
|
||||||
|
"Dialog.LogFiles": "Ficheiros de Registo",
|
||||||
|
|
||||||
|
"Options.About" : "Sobre",
|
||||||
|
"About.Github.Label": "GitHub",
|
||||||
|
"About.Github.Desc": "Código-fonte, problemas e desenvolvimento do projeto.",
|
||||||
|
"About.Discord.Label": "Discord",
|
||||||
|
"About.Discord.Desc": "Junte-se à comunidade, obtenha suporte e acompanhe o desenvolvimento.",
|
||||||
|
"About.GithubButton": "Contribua no GitHub!",
|
||||||
|
"About.DiscordButton": "Junte-se ao nosso Discord!"
|
||||||
|
}
|
||||||
@@ -22,6 +22,8 @@ public sealed class Localization
|
|||||||
private const string EmbeddedResourceSuffix = ".json";
|
private const string EmbeddedResourceSuffix = ".json";
|
||||||
|
|
||||||
private Dictionary<string, string> _strings = new();
|
private Dictionary<string, string> _strings = new();
|
||||||
|
private Dictionary<string, string> _fallbackStrings = new();
|
||||||
|
|
||||||
|
|
||||||
private Localization()
|
private Localization()
|
||||||
{
|
{
|
||||||
@@ -32,7 +34,16 @@ public sealed class Localization
|
|||||||
|
|
||||||
public string CurrentCode { get; private set; } = "en";
|
public string CurrentCode { get; private set; } = "en";
|
||||||
|
|
||||||
public string Get(string key) => _strings.TryGetValue(key, out var value) ? value : key;
|
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);
|
public string Format(string key, params object?[] args) => string.Format(Get(key), args);
|
||||||
|
|
||||||
@@ -74,13 +85,46 @@ public sealed class Localization
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Loads a language by code (e.g. "en"): a loose override file first, then the embedded copy.</summary>
|
/// <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)
|
public void Load(string code)
|
||||||
{
|
{
|
||||||
if (!TryLoadLooseFile(code) && !TryLoadEmbedded(code) &&
|
if (_fallbackStrings.Count == 0 && !string.Equals(code, "en", StringComparison.OrdinalIgnoreCase))
|
||||||
!string.Equals(code, "en", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
_ = TryLoadLooseFile("en") || TryLoadEmbedded("en");
|
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()
|
private static IEnumerable<string> EmbeddedLanguageCodes()
|
||||||
@@ -149,16 +193,64 @@ public sealed class Localization
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryLoad(string code, string json)
|
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);
|
var loaded = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
|
||||||
if (loaded is null)
|
if (loaded is null)
|
||||||
{
|
{
|
||||||
|
result = new Dictionary<string, string>();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_strings = loaded;
|
result = loaded;
|
||||||
CurrentCode = code;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private bool TryLoad(string code, string json)
|
||||||
|
{
|
||||||
|
if (TryLoad(json, out var dict))
|
||||||
|
{
|
||||||
|
_strings = dict;
|
||||||
|
CurrentCode = code;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -317,6 +317,154 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</Grid>
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</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>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|||||||
@@ -125,6 +125,18 @@ public partial class MainWindow : Window
|
|||||||
UpdateDiscordPresence();
|
UpdateDiscordPresence();
|
||||||
};
|
};
|
||||||
SelectLogFilePathButton.Click += async (_, _) => await SelectLogFilePathAsync();
|
SelectLogFilePathButton.Click += async (_, _) => await SelectLogFilePathAsync();
|
||||||
|
EnvBthidToggle.IsCheckedChanged += (_, _) =>
|
||||||
|
SetEnvironmentToggle("SHARPEMU_BTHID_UNAVAILABLE", EnvBthidToggle.IsChecked == true);
|
||||||
|
EnvLoopGuardToggle.IsCheckedChanged += (_, _) =>
|
||||||
|
SetEnvironmentToggle("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD", EnvLoopGuardToggle.IsChecked == true);
|
||||||
|
EnvVkValidationToggle.IsCheckedChanged += (_, _) =>
|
||||||
|
SetEnvironmentToggle("SHARPEMU_VK_VALIDATION", EnvVkValidationToggle.IsChecked == true);
|
||||||
|
EnvDumpSpirvToggle.IsCheckedChanged += (_, _) =>
|
||||||
|
SetEnvironmentToggle("SHARPEMU_DUMP_SPIRV", EnvDumpSpirvToggle.IsChecked == true);
|
||||||
|
EnvLogDirectMemoryToggle.IsCheckedChanged += (_, _) =>
|
||||||
|
SetEnvironmentToggle("SHARPEMU_LOG_DIRECT_MEMORY", EnvLogDirectMemoryToggle.IsChecked == true);
|
||||||
|
EnvLogNpToggle.IsCheckedChanged += (_, _) =>
|
||||||
|
SetEnvironmentToggle("SHARPEMU_LOG_NP", EnvLogNpToggle.IsChecked == true);
|
||||||
LanguageBox.SelectionChanged += (_, _) => OnLanguageChanged();
|
LanguageBox.SelectionChanged += (_, _) => OnLanguageChanged();
|
||||||
|
|
||||||
GameList.AddHandler(ContextRequestedEvent, OnGameContextRequested, RoutingStrategies.Tunnel);
|
GameList.AddHandler(ContextRequestedEvent, OnGameContextRequested, RoutingStrategies.Tunnel);
|
||||||
@@ -147,6 +159,25 @@ public partial class MainWindow : Window
|
|||||||
};
|
};
|
||||||
_gamepadTimer.Tick += (_, _) => PollGamepad();
|
_gamepadTimer.Tick += (_, _) => PollGamepad();
|
||||||
_gamepadTimer.Start();
|
_gamepadTimer.Start();
|
||||||
|
|
||||||
|
|
||||||
|
GithubButton.Click += (_, _) =>
|
||||||
|
{
|
||||||
|
Process.Start(new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "https://github.com/par274/sharpemu",
|
||||||
|
UseShellExecute = true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
DiscordButton.Click += (_, _) =>
|
||||||
|
{
|
||||||
|
Process.Start(new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "https://discord.com/invite/6GejPEDqpc",
|
||||||
|
UseShellExecute = true
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -384,6 +415,15 @@ public partial class MainWindow : Window
|
|||||||
LoadingStateText.Text = loc.Get("Library.Loading");
|
LoadingStateText.Text = loc.Get("Library.Loading");
|
||||||
|
|
||||||
GeneralTabItem.Header = loc.Get("Options.General");
|
GeneralTabItem.Header = loc.Get("Options.General");
|
||||||
|
EnvTabItem.Header = loc.Get("Options.Env.Tab");
|
||||||
|
EnvSectionTitle.Text = loc.Get("Options.Section.Environment");
|
||||||
|
EnvDesc.Text = loc.Get("Options.Env.Desc");
|
||||||
|
EnvBthidDesc.Text = loc.Get("Options.Env.Bthid.Desc");
|
||||||
|
EnvLoopGuardDesc.Text = loc.Get("Options.Env.LoopGuard.Desc");
|
||||||
|
EnvVkValidationDesc.Text = loc.Get("Options.Env.VkValidation.Desc");
|
||||||
|
EnvDumpSpirvDesc.Text = loc.Get("Options.Env.DumpSpirv.Desc");
|
||||||
|
EnvLogDirectMemoryDesc.Text = loc.Get("Options.Env.LogDirectMemory.Desc");
|
||||||
|
EnvLogNpDesc.Text = loc.Get("Options.Env.LogNp.Desc");
|
||||||
EmulationSectionTitle.Text = loc.Get("Options.Section.Emulation");
|
EmulationSectionTitle.Text = loc.Get("Options.Section.Emulation");
|
||||||
LoggingSectionTitle.Text = loc.Get("Options.Section.Logging");
|
LoggingSectionTitle.Text = loc.Get("Options.Section.Logging");
|
||||||
LauncherSectionTitle.Text = loc.Get("Options.Section.Launcher");
|
LauncherSectionTitle.Text = loc.Get("Options.Section.Launcher");
|
||||||
@@ -443,6 +483,14 @@ public partial class MainWindow : Window
|
|||||||
LaunchButton.Content = loc.Get("Launch.Launch");
|
LaunchButton.Content = loc.Get("Launch.Launch");
|
||||||
StopButton.Content = loc.Get("Launch.Stop");
|
StopButton.Content = loc.Get("Launch.Stop");
|
||||||
|
|
||||||
|
AboutSectionTitle.Text = loc.Get("Options.About");
|
||||||
|
GithubLabel.Text = loc.Get("About.Github.Label");
|
||||||
|
GithubDesc.Text = loc.Get("About.Github.Desc");
|
||||||
|
DiscordServerLabel.Text = loc.Get("About.Discord.Label");
|
||||||
|
DiscordServerDesc.Text = loc.Get("About.Discord.Desc");
|
||||||
|
GithubButton.Content = loc.Get("About.GithubButton");
|
||||||
|
DiscordButton.Content = loc.Get("About.DiscordButton");
|
||||||
|
|
||||||
UpdateEmptyStateTexts();
|
UpdateEmptyStateTexts();
|
||||||
UpdateSelectedGameTexts();
|
UpdateSelectedGameTexts();
|
||||||
}
|
}
|
||||||
@@ -557,9 +605,34 @@ public partial class MainWindow : Window
|
|||||||
OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile;
|
OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile;
|
||||||
TitleMusicToggle.IsChecked = _settings.PlayTitleMusic;
|
TitleMusicToggle.IsChecked = _settings.PlayTitleMusic;
|
||||||
DiscordToggle.IsChecked = _settings.DiscordRichPresence;
|
DiscordToggle.IsChecked = _settings.DiscordRichPresence;
|
||||||
|
EnvBthidToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_BTHID_UNAVAILABLE");
|
||||||
|
EnvLoopGuardToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD");
|
||||||
|
EnvVkValidationToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_VK_VALIDATION");
|
||||||
|
EnvDumpSpirvToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_DUMP_SPIRV");
|
||||||
|
EnvLogDirectMemoryToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_DIRECT_MEMORY");
|
||||||
|
EnvLogNpToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_NP");
|
||||||
UpdateLogFilePathText();
|
UpdateLogFilePathText();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Environment variables set on this process at the previous launch; children
|
||||||
|
// inherit the process environment, so stale names must be cleared explicitly.
|
||||||
|
private readonly HashSet<string> _appliedEnvironmentVariables = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private void SetEnvironmentToggle(string name, bool enabled)
|
||||||
|
{
|
||||||
|
if (enabled)
|
||||||
|
{
|
||||||
|
if (!_settings.EnvironmentToggles.Contains(name))
|
||||||
|
{
|
||||||
|
_settings.EnvironmentToggles.Add(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_settings.EnvironmentToggles.Remove(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private string SelectedLogLevel()
|
private string SelectedLogLevel()
|
||||||
{
|
{
|
||||||
return LogLevelBox.SelectedIndex switch
|
return LogLevelBox.SelectedIndex switch
|
||||||
@@ -1375,6 +1448,24 @@ public partial class MainWindow : Window
|
|||||||
Localization.Instance.Format("Launch.Command", string.Join(' ', arguments)),
|
Localization.Instance.Format("Launch.Command", string.Join(' ', arguments)),
|
||||||
DimLineBrush);
|
DimLineBrush);
|
||||||
|
|
||||||
|
// Apply the enabled switches to this process; both emulator launch paths
|
||||||
|
// (CreateProcessW and Process.Start) inherit it. Clear switches turned
|
||||||
|
// off since the previous launch.
|
||||||
|
foreach (var staleName in _appliedEnvironmentVariables)
|
||||||
|
{
|
||||||
|
if (!_settings.EnvironmentToggles.Contains(staleName))
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable(staleName, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_appliedEnvironmentVariables.Clear();
|
||||||
|
foreach (var name in _settings.EnvironmentToggles)
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable(name, "1");
|
||||||
|
_appliedEnvironmentVariables.Add(name);
|
||||||
|
}
|
||||||
|
|
||||||
var emulator = new EmulatorProcess();
|
var emulator = new EmulatorProcess();
|
||||||
emulator.OutputReceived += (line, isError) => _pendingLines.Enqueue((line, isError));
|
emulator.OutputReceived += (line, isError) => _pendingLines.Enqueue((line, isError));
|
||||||
emulator.Exited += code => Dispatcher.UIThread.Post(() => OnEmulatorExited(code));
|
emulator.Exited += code => Dispatcher.UIThread.Post(() => OnEmulatorExited(code));
|
||||||
@@ -1472,6 +1563,7 @@ public partial class MainWindow : Window
|
|||||||
2 => "Exit.EbootNotFound",
|
2 => "Exit.EbootNotFound",
|
||||||
3 => "Exit.RuntimeException",
|
3 => "Exit.RuntimeException",
|
||||||
4 => "Exit.EmulationError",
|
4 => "Exit.EmulationError",
|
||||||
|
-1073741819 => "Exit.EmulationError",
|
||||||
_ => "Exit.Unknown",
|
_ => "Exit.Unknown",
|
||||||
};
|
};
|
||||||
var meaning = Localization.Instance.Get(meaningKey);
|
var meaning = Localization.Instance.Get(meaningKey);
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
|
<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>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
Binary file not shown.
@@ -1,6 +1,7 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
@@ -238,23 +239,63 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var bytes = new byte[capacity];
|
const int StackBufferLength = 512;
|
||||||
for (var index = 0; index < bytes.Length; index++)
|
const int ReadChunkLength = 128;
|
||||||
|
var rented = capacity > StackBufferLength ? ArrayPool<byte>.Shared.Rent(capacity) : null;
|
||||||
|
Span<byte> bytes = rented is null ? stackalloc byte[StackBufferLength] : rented;
|
||||||
|
try
|
||||||
{
|
{
|
||||||
if (!Memory.TryRead(address + (ulong)index, bytes.AsSpan(index, 1)))
|
var length = 0;
|
||||||
|
while (length < capacity)
|
||||||
{
|
{
|
||||||
return false;
|
// Bulk-read in bounded chunks rather than the full capacity: the string
|
||||||
|
// may end just before unmapped memory, and overreading past the
|
||||||
|
// terminator by more than a chunk could fault where the old
|
||||||
|
// byte-by-byte loop succeeded.
|
||||||
|
var chunk = Math.Min(ReadChunkLength, capacity - length);
|
||||||
|
var span = bytes.Slice(length, chunk);
|
||||||
|
if (Memory.TryRead(address + (ulong)length, span))
|
||||||
|
{
|
||||||
|
var terminator = span.IndexOf((byte)0);
|
||||||
|
if (terminator >= 0)
|
||||||
|
{
|
||||||
|
value = Encoding.UTF8.GetString(bytes[..(length + terminator)]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
length += chunk;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The chunk touches an unreadable range; fall back to per-byte reads so a
|
||||||
|
// terminator sitting before the bad byte still yields the string.
|
||||||
|
for (var i = 0; i < chunk; i++)
|
||||||
|
{
|
||||||
|
if (!Memory.TryRead(address + (ulong)(length + i), bytes.Slice(length + i, 1)))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes[length + i] == 0)
|
||||||
|
{
|
||||||
|
value = Encoding.UTF8.GetString(bytes[..(length + i)]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
length += chunk;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bytes[index] == 0)
|
value = Encoding.UTF8.GetString(bytes[..capacity]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (rented is not null)
|
||||||
{
|
{
|
||||||
value = Encoding.UTF8.GetString(bytes, 0, index);
|
ArrayPool<byte>.Shared.Return(rented);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
value = Encoding.UTF8.GetString(bytes);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool PushUInt64(ulong value)
|
public bool PushUInt64(ulong value)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -23,6 +23,20 @@ public readonly record struct GuestThreadSnapshot(
|
|||||||
ulong LastReturnRip,
|
ulong LastReturnRip,
|
||||||
string? BlockReason);
|
string? BlockReason);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Continuation state for a blocked guest thread, replacing the closure pair a blocking
|
||||||
|
/// wait used to allocate. TryWake runs under the scheduler's guest-thread gate and
|
||||||
|
/// returns true when the waiter has a final result and the thread should be re-readied;
|
||||||
|
/// false leaves it parked. Resume runs later on the woken thread outside that gate, and
|
||||||
|
/// its return value becomes the guest's RAX for the resumed call.
|
||||||
|
/// </summary>
|
||||||
|
public interface IGuestThreadBlockWaiter
|
||||||
|
{
|
||||||
|
int Resume();
|
||||||
|
|
||||||
|
bool TryWake();
|
||||||
|
}
|
||||||
|
|
||||||
public interface IGuestThreadScheduler
|
public interface IGuestThreadScheduler
|
||||||
{
|
{
|
||||||
bool SupportsGuestContextTransfer { get; }
|
bool SupportsGuestContextTransfer { get; }
|
||||||
@@ -106,10 +120,7 @@ public static class GuestThreadExecution
|
|||||||
private static string? _pendingBlockWakeKey;
|
private static string? _pendingBlockWakeKey;
|
||||||
|
|
||||||
[ThreadStatic]
|
[ThreadStatic]
|
||||||
private static Func<int>? _pendingBlockResumeHandler;
|
private static IGuestThreadBlockWaiter? _pendingBlockWaiter;
|
||||||
|
|
||||||
[ThreadStatic]
|
|
||||||
private static Func<bool>? _pendingBlockWakeHandler;
|
|
||||||
|
|
||||||
[ThreadStatic]
|
[ThreadStatic]
|
||||||
private static long _pendingBlockDeadlineTimestamp;
|
private static long _pendingBlockDeadlineTimestamp;
|
||||||
@@ -157,8 +168,7 @@ public static class GuestThreadExecution
|
|||||||
_pendingBlockContinuationValid = false;
|
_pendingBlockContinuationValid = false;
|
||||||
_pendingBlockContinuation = default;
|
_pendingBlockContinuation = default;
|
||||||
_pendingBlockWakeKey = null;
|
_pendingBlockWakeKey = null;
|
||||||
_pendingBlockResumeHandler = null;
|
_pendingBlockWaiter = null;
|
||||||
_pendingBlockWakeHandler = null;
|
|
||||||
_pendingBlockDeadlineTimestamp = 0;
|
_pendingBlockDeadlineTimestamp = 0;
|
||||||
_pendingEntryExit = false;
|
_pendingEntryExit = false;
|
||||||
_pendingEntryExitValue = 0;
|
_pendingEntryExitValue = 0;
|
||||||
@@ -179,8 +189,7 @@ public static class GuestThreadExecution
|
|||||||
_pendingBlockContinuationValid = false;
|
_pendingBlockContinuationValid = false;
|
||||||
_pendingBlockContinuation = default;
|
_pendingBlockContinuation = default;
|
||||||
_pendingBlockWakeKey = null;
|
_pendingBlockWakeKey = null;
|
||||||
_pendingBlockResumeHandler = null;
|
_pendingBlockWaiter = null;
|
||||||
_pendingBlockWakeHandler = null;
|
|
||||||
_pendingBlockDeadlineTimestamp = 0;
|
_pendingBlockDeadlineTimestamp = 0;
|
||||||
_pendingEntryExit = false;
|
_pendingEntryExit = false;
|
||||||
_pendingEntryExitValue = 0;
|
_pendingEntryExitValue = 0;
|
||||||
@@ -211,8 +220,7 @@ public static class GuestThreadExecution
|
|||||||
CpuContext? context,
|
CpuContext? context,
|
||||||
string reason,
|
string reason,
|
||||||
string? wakeKey = null,
|
string? wakeKey = null,
|
||||||
Func<int>? resumeHandler = null,
|
IGuestThreadBlockWaiter? waiter = null,
|
||||||
Func<bool>? wakeHandler = null,
|
|
||||||
long blockDeadlineTimestamp = 0)
|
long blockDeadlineTimestamp = 0)
|
||||||
{
|
{
|
||||||
if (!IsGuestThread)
|
if (!IsGuestThread)
|
||||||
@@ -222,8 +230,7 @@ public static class GuestThreadExecution
|
|||||||
|
|
||||||
_pendingBlockReason = string.IsNullOrWhiteSpace(reason) ? "guest_thread_blocked" : reason;
|
_pendingBlockReason = string.IsNullOrWhiteSpace(reason) ? "guest_thread_blocked" : reason;
|
||||||
_pendingBlockWakeKey = string.IsNullOrWhiteSpace(wakeKey) ? _pendingBlockReason : wakeKey;
|
_pendingBlockWakeKey = string.IsNullOrWhiteSpace(wakeKey) ? _pendingBlockReason : wakeKey;
|
||||||
_pendingBlockResumeHandler = resumeHandler;
|
_pendingBlockWaiter = waiter;
|
||||||
_pendingBlockWakeHandler = wakeHandler;
|
|
||||||
_pendingBlockDeadlineTimestamp = blockDeadlineTimestamp;
|
_pendingBlockDeadlineTimestamp = blockDeadlineTimestamp;
|
||||||
if (context is not null && TryCaptureCurrentBlockContinuation(context, out var continuation))
|
if (context is not null && TryCaptureCurrentBlockContinuation(context, out var continuation))
|
||||||
{
|
{
|
||||||
@@ -255,7 +262,6 @@ public static class GuestThreadExecution
|
|||||||
out hasContinuation,
|
out hasContinuation,
|
||||||
out _,
|
out _,
|
||||||
out _,
|
out _,
|
||||||
out _,
|
|
||||||
out _);
|
out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,16 +270,14 @@ public static class GuestThreadExecution
|
|||||||
out GuestCpuContinuation continuation,
|
out GuestCpuContinuation continuation,
|
||||||
out bool hasContinuation,
|
out bool hasContinuation,
|
||||||
out string wakeKey,
|
out string wakeKey,
|
||||||
out Func<int>? resumeHandler,
|
out IGuestThreadBlockWaiter? waiter)
|
||||||
out Func<bool>? wakeHandler)
|
|
||||||
{
|
{
|
||||||
return TryConsumeCurrentThreadBlock(
|
return TryConsumeCurrentThreadBlock(
|
||||||
out reason,
|
out reason,
|
||||||
out continuation,
|
out continuation,
|
||||||
out hasContinuation,
|
out hasContinuation,
|
||||||
out wakeKey,
|
out wakeKey,
|
||||||
out resumeHandler,
|
out waiter,
|
||||||
out wakeHandler,
|
|
||||||
out _);
|
out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,8 +286,7 @@ public static class GuestThreadExecution
|
|||||||
out GuestCpuContinuation continuation,
|
out GuestCpuContinuation continuation,
|
||||||
out bool hasContinuation,
|
out bool hasContinuation,
|
||||||
out string wakeKey,
|
out string wakeKey,
|
||||||
out Func<int>? resumeHandler,
|
out IGuestThreadBlockWaiter? waiter,
|
||||||
out Func<bool>? wakeHandler,
|
|
||||||
out long blockDeadlineTimestamp)
|
out long blockDeadlineTimestamp)
|
||||||
{
|
{
|
||||||
reason = _pendingBlockReason ?? string.Empty;
|
reason = _pendingBlockReason ?? string.Empty;
|
||||||
@@ -292,8 +295,7 @@ public static class GuestThreadExecution
|
|||||||
continuation = default;
|
continuation = default;
|
||||||
hasContinuation = false;
|
hasContinuation = false;
|
||||||
wakeKey = string.Empty;
|
wakeKey = string.Empty;
|
||||||
resumeHandler = null;
|
waiter = null;
|
||||||
wakeHandler = null;
|
|
||||||
blockDeadlineTimestamp = 0;
|
blockDeadlineTimestamp = 0;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -301,15 +303,13 @@ public static class GuestThreadExecution
|
|||||||
continuation = _pendingBlockContinuation;
|
continuation = _pendingBlockContinuation;
|
||||||
hasContinuation = _pendingBlockContinuationValid;
|
hasContinuation = _pendingBlockContinuationValid;
|
||||||
wakeKey = _pendingBlockWakeKey ?? reason;
|
wakeKey = _pendingBlockWakeKey ?? reason;
|
||||||
resumeHandler = _pendingBlockResumeHandler;
|
waiter = _pendingBlockWaiter;
|
||||||
wakeHandler = _pendingBlockWakeHandler;
|
|
||||||
blockDeadlineTimestamp = _pendingBlockDeadlineTimestamp;
|
blockDeadlineTimestamp = _pendingBlockDeadlineTimestamp;
|
||||||
_pendingBlockReason = null;
|
_pendingBlockReason = null;
|
||||||
_pendingBlockContinuation = default;
|
_pendingBlockContinuation = default;
|
||||||
_pendingBlockContinuationValid = false;
|
_pendingBlockContinuationValid = false;
|
||||||
_pendingBlockWakeKey = null;
|
_pendingBlockWakeKey = null;
|
||||||
_pendingBlockResumeHandler = null;
|
_pendingBlockWaiter = null;
|
||||||
_pendingBlockWakeHandler = null;
|
|
||||||
_pendingBlockDeadlineTimestamp = 0;
|
_pendingBlockDeadlineTimestamp = 0;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 TryRead(ulong virtualAddress, Span<byte> destination);
|
||||||
|
|
||||||
bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source);
|
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);
|
||||||
|
}
|
||||||
@@ -6,4 +6,6 @@ namespace SharpEmu.HLE;
|
|||||||
public interface IGuestMemoryAllocator
|
public interface IGuestMemoryAllocator
|
||||||
{
|
{
|
||||||
bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address);
|
bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address);
|
||||||
|
|
||||||
|
bool TryFreeGuestMemory(ulong address);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ public static class AgcExports
|
|||||||
private const uint ItDispatchDirect = 0x15;
|
private const uint ItDispatchDirect = 0x15;
|
||||||
private const uint ItDispatchIndirect = 0x16;
|
private const uint ItDispatchIndirect = 0x16;
|
||||||
private const uint ItWaitRegMem = 0x3C;
|
private const uint ItWaitRegMem = 0x3C;
|
||||||
|
private const uint ItIndirectBuffer = 0x3F;
|
||||||
private const uint ItEventWrite = 0x46;
|
private const uint ItEventWrite = 0x46;
|
||||||
private const uint ItDmaData = 0x50;
|
private const uint ItDmaData = 0x50;
|
||||||
private const uint ItSetContextReg = 0x69;
|
private const uint ItSetContextReg = 0x69;
|
||||||
@@ -120,6 +121,9 @@ public static class AgcExports
|
|||||||
private const uint RegisterDefaultsVersion13 = 13;
|
private const uint RegisterDefaultsVersion13 = 13;
|
||||||
private const int RegisterDefaultsSize = 0x40;
|
private const int RegisterDefaultsSize = 0x40;
|
||||||
private const int RegisterDefaultBlockSize = 16 * 8;
|
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 ShaderUserDataOffset = 0x08;
|
||||||
private const ulong ShaderCodeOffset = 0x10;
|
private const ulong ShaderCodeOffset = 0x10;
|
||||||
@@ -154,7 +158,7 @@ public static class AgcExports
|
|||||||
private static readonly HashSet<uint> _tracedSubmittedDrawOpcodes = new();
|
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 Ps, ulong State, Gen5PixelOutputKind Output), byte[]> _pixelSpirvCache = new();
|
||||||
private static readonly Dictionary<
|
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();
|
(byte[] Vertex, byte[] Pixel)> _graphicsSpirvCache = new();
|
||||||
private static readonly Dictionary<
|
private static readonly Dictionary<
|
||||||
(ulong Cs, ulong State, uint LocalX, uint LocalY, uint LocalZ),
|
(ulong Cs, ulong State, uint LocalX, uint LocalY, uint LocalZ),
|
||||||
@@ -388,9 +392,26 @@ public static class AgcExports
|
|||||||
public SubmittedDcbState Graphics { get; } = new();
|
public SubmittedDcbState Graphics { get; } = new();
|
||||||
public Dictionary<uint, SubmittedDcbState> ComputeQueues { get; } = new();
|
public Dictionary<uint, SubmittedDcbState> ComputeQueues { get; } = new();
|
||||||
public Dictionary<ulong, ComputeImageWriter> ComputeImageWriters { 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; }
|
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 RegisterDefaultValue(uint Offset, uint Value);
|
||||||
|
|
||||||
private readonly record struct RegisterDefaultGroup(
|
private readonly record struct RegisterDefaultGroup(
|
||||||
@@ -1923,6 +1944,96 @@ public static class AgcExports
|
|||||||
return ReturnPointer(ctx, commandAddress);
|
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(
|
[SysAbiExport(
|
||||||
Nid = "w2rJhmD+dsE",
|
Nid = "w2rJhmD+dsE",
|
||||||
ExportName = "sceAgcDriverAddEqEvent",
|
ExportName = "sceAgcDriverAddEqEvent",
|
||||||
@@ -1999,8 +2110,16 @@ public static class AgcExports
|
|||||||
var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
||||||
lock (gpuState.Gate)
|
lock (gpuState.Gate)
|
||||||
{
|
{
|
||||||
ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets);
|
Gen5ShaderScalarEvaluator.BeginGlobalMemoryReadScope();
|
||||||
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
try
|
||||||
|
{
|
||||||
|
ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets);
|
||||||
|
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Gen5ShaderScalarEvaluator.EndGlobalMemoryReadScope();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
@@ -2030,27 +2149,35 @@ public static class AgcExports
|
|||||||
var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
||||||
lock (gpuState.Gate)
|
lock (gpuState.Gate)
|
||||||
{
|
{
|
||||||
for (uint i = 0; i < bufferCount; i++)
|
Gen5ShaderScalarEvaluator.BeginGlobalMemoryReadScope();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
if (!ctx.TryReadUInt64(addressArray + i * 8, out var commandAddress) ||
|
for (uint i = 0; i < bufferCount; i++)
|
||||||
commandAddress == 0 ||
|
|
||||||
!ctx.TryReadUInt32(sizeArray + i * 4, out var dwordCount) ||
|
|
||||||
dwordCount == 0)
|
|
||||||
{
|
{
|
||||||
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)
|
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
||||||
{
|
}
|
||||||
TraceAgc(
|
finally
|
||||||
$"agc.driver_submit_multi_dcbs index={i}/{bufferCount} " +
|
{
|
||||||
$"addr=0x{commandAddress:X16} dwords={dwordCount}");
|
Gen5ShaderScalarEvaluator.EndGlobalMemoryReadScope();
|
||||||
}
|
|
||||||
|
|
||||||
ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
@@ -2098,8 +2225,16 @@ public static class AgcExports
|
|||||||
gpuState.ComputeQueues.Add(ownerHandle, queueState);
|
gpuState.ComputeQueues.Add(ownerHandle, queueState);
|
||||||
}
|
}
|
||||||
|
|
||||||
ParseSubmittedDcb(ctx, gpuState, queueState, commandAddress, dwordCount, tracePackets);
|
Gen5ShaderScalarEvaluator.BeginGlobalMemoryReadScope();
|
||||||
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
try
|
||||||
|
{
|
||||||
|
ParseSubmittedDcb(ctx, gpuState, queueState, commandAddress, dwordCount, tracePackets);
|
||||||
|
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Gen5ShaderScalarEvaluator.EndGlobalMemoryReadScope();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
@@ -2120,12 +2255,87 @@ public static class AgcExports
|
|||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
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);
|
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);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2144,12 +2354,97 @@ public static class AgcExports
|
|||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
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);
|
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);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2160,19 +2455,90 @@ public static class AgcExports
|
|||||||
LibraryName = "libSceAgc")]
|
LibraryName = "libSceAgc")]
|
||||||
public static int DriverRegisterResource(CpuContext ctx)
|
public static int DriverRegisterResource(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var resourceAddress = ctx[CpuRegister.Rdi];
|
var resourceHandleAddress = ctx[CpuRegister.Rdi];
|
||||||
var owner = (uint)ctx[CpuRegister.Rsi];
|
var owner = (uint)ctx[CpuRegister.Rsi];
|
||||||
var nameAddress = ctx[CpuRegister.Rdx];
|
var resourceAddress = ctx[CpuRegister.Rdx];
|
||||||
var type = (uint)ctx[CpuRegister.R8];
|
var resourceSize = ctx[CpuRegister.Rcx];
|
||||||
var flags = (uint)ctx[CpuRegister.R9];
|
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(
|
TraceAgc(
|
||||||
$"agc.driver_register_resource resource=0x{resourceAddress:X16} owner={owner} " +
|
$"agc.driver_register_resource handle={resourceHandle} owner={owner} " +
|
||||||
$"name=0x{nameAddress:X16} type={type} flags={flags}");
|
$"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);
|
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(
|
[SysAbiExport(
|
||||||
Nid = "-KRzWekV120",
|
Nid = "-KRzWekV120",
|
||||||
ExportName = "sceAgcDriverUnknown_KRzWekV120",
|
ExportName = "sceAgcDriverUnknown_KRzWekV120",
|
||||||
@@ -3054,12 +3420,13 @@ public static class AgcExports
|
|||||||
textures,
|
textures,
|
||||||
globalMemoryBuffers,
|
globalMemoryBuffers,
|
||||||
translatedDraw.AttributeCount,
|
translatedDraw.AttributeCount,
|
||||||
new VulkanGuestRenderTarget(
|
translatedDraw.RenderTargets.Select(target =>
|
||||||
firstTarget.Address,
|
new VulkanGuestRenderTarget(
|
||||||
firstTarget.Width,
|
target.Address,
|
||||||
firstTarget.Height,
|
target.Width,
|
||||||
firstTarget.Format,
|
target.Height,
|
||||||
firstTarget.NumberType),
|
target.Format,
|
||||||
|
target.NumberType)).ToArray(),
|
||||||
translatedDraw.VertexSpirv,
|
translatedDraw.VertexSpirv,
|
||||||
translatedDraw.VertexCount,
|
translatedDraw.VertexCount,
|
||||||
translatedDraw.InstanceCount,
|
translatedDraw.InstanceCount,
|
||||||
@@ -3167,7 +3534,8 @@ public static class AgcExports
|
|||||||
exportState,
|
exportState,
|
||||||
out var exportEvaluation,
|
out var exportEvaluation,
|
||||||
out error,
|
out error,
|
||||||
resolveVertexInputs: true) ||
|
resolveVertexInputs: true,
|
||||||
|
vertexRecordLimit: indexed ? null : vertexCount) ||
|
||||||
!Gen5ShaderTranslator.TryCreateState(
|
!Gen5ShaderTranslator.TryCreateState(
|
||||||
ctx,
|
ctx,
|
||||||
pixelShaderAddress,
|
pixelShaderAddress,
|
||||||
@@ -3186,19 +3554,47 @@ public static class AgcExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var renderTargets = GetRenderTargets(state.CxRegisters)
|
var renderTargets = GetRenderTargets(state.CxRegisters)
|
||||||
.Where(target =>
|
.Where(target => HasPixelColorExport(pixelState, target.Slot))
|
||||||
target.Slot == 0 &&
|
.OrderBy(target => target.Slot)
|
||||||
HasPixelColorExport(pixelState, target.Slot))
|
|
||||||
.ToArray();
|
.ToArray();
|
||||||
var outputKind = GetPixelOutputKind(renderTargets.FirstOrDefault().NumberType);
|
var renderTargetFormats = new VulkanRenderTargetFormat[renderTargets.Length];
|
||||||
var exportStateFingerprint = ComputeShaderStateFingerprint(exportEvaluation);
|
for (var index = 0; index < renderTargets.Length; index++)
|
||||||
var pixelStateFingerprint = ComputeShaderStateFingerprint(pixelEvaluation);
|
{
|
||||||
|
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 = (
|
var shaderKey = (
|
||||||
exportShaderAddress,
|
exportShaderAddress,
|
||||||
exportStateFingerprint,
|
exportStateFingerprint,
|
||||||
pixelShaderAddress,
|
pixelShaderAddress,
|
||||||
pixelStateFingerprint,
|
pixelStateFingerprint,
|
||||||
outputKind);
|
outputLayout,
|
||||||
|
attributeCount);
|
||||||
|
var totalGlobalBuffers =
|
||||||
|
pixelEvaluation.GlobalMemoryBindings.Count +
|
||||||
|
exportEvaluation.GlobalMemoryBindings.Count;
|
||||||
(byte[] Vertex, byte[] Pixel) compiled;
|
(byte[] Vertex, byte[] Pixel) compiled;
|
||||||
lock (_submitTraceGate)
|
lock (_submitTraceGate)
|
||||||
{
|
{
|
||||||
@@ -3207,26 +3603,25 @@ public static class AgcExports
|
|||||||
|
|
||||||
if (compiled.Vertex is null || compiled.Pixel is null)
|
if (compiled.Vertex is null || compiled.Pixel is null)
|
||||||
{
|
{
|
||||||
var totalGlobalBuffers =
|
|
||||||
pixelEvaluation.GlobalMemoryBindings.Count +
|
|
||||||
exportEvaluation.GlobalMemoryBindings.Count;
|
|
||||||
if (!Gen5SpirvTranslator.TryCompilePixelShader(
|
if (!Gen5SpirvTranslator.TryCompilePixelShader(
|
||||||
pixelState,
|
pixelState,
|
||||||
pixelEvaluation,
|
pixelEvaluation,
|
||||||
outputKind,
|
pixelOutputs,
|
||||||
out var pixelShader,
|
out var pixelShader,
|
||||||
out error,
|
out error,
|
||||||
globalBufferBase: 0,
|
globalBufferBase: 0,
|
||||||
totalGlobalBufferCount: totalGlobalBuffers,
|
totalGlobalBufferCount: totalGlobalBuffers + 2,
|
||||||
imageBindingBase: 0) ||
|
imageBindingBase: 0,
|
||||||
|
scalarRegisterBufferIndex: totalGlobalBuffers) ||
|
||||||
!Gen5SpirvTranslator.TryCompileVertexShader(
|
!Gen5SpirvTranslator.TryCompileVertexShader(
|
||||||
exportState,
|
exportState,
|
||||||
exportEvaluation,
|
exportEvaluation,
|
||||||
out var vertexShader,
|
out var vertexShader,
|
||||||
out error,
|
out error,
|
||||||
globalBufferBase: pixelEvaluation.GlobalMemoryBindings.Count,
|
globalBufferBase: pixelEvaluation.GlobalMemoryBindings.Count,
|
||||||
totalGlobalBufferCount: totalGlobalBuffers,
|
totalGlobalBufferCount: totalGlobalBuffers + 2,
|
||||||
imageBindingBase: pixelEvaluation.ImageBindings.Count))
|
imageBindingBase: pixelEvaluation.ImageBindings.Count,
|
||||||
|
scalarRegisterBufferIndex: totalGlobalBuffers + 1))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -3278,6 +3673,8 @@ public static class AgcExports
|
|||||||
|
|
||||||
var globalMemoryBindings = pixelEvaluation.GlobalMemoryBindings
|
var globalMemoryBindings = pixelEvaluation.GlobalMemoryBindings
|
||||||
.Concat(exportEvaluation.GlobalMemoryBindings)
|
.Concat(exportEvaluation.GlobalMemoryBindings)
|
||||||
|
.Append(CreateScalarRegisterBinding(pixelEvaluation))
|
||||||
|
.Append(CreateScalarRegisterBinding(exportEvaluation))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
IReadOnlyList<Gen5VertexInputBinding> vertexInputs =
|
IReadOnlyList<Gen5VertexInputBinding> vertexInputs =
|
||||||
exportEvaluation.VertexInputs ?? [];
|
exportEvaluation.VertexInputs ?? [];
|
||||||
@@ -3288,7 +3685,7 @@ public static class AgcExports
|
|||||||
primitiveType,
|
primitiveType,
|
||||||
compiled.Vertex,
|
compiled.Vertex,
|
||||||
compiled.Pixel,
|
compiled.Pixel,
|
||||||
GetInterpolatedAttributeCount(pixelState),
|
attributeCount,
|
||||||
vertexCount,
|
vertexCount,
|
||||||
state.InstanceCount,
|
state.InstanceCount,
|
||||||
indexed ? CreateVulkanIndexBuffer(ctx, state, vertexCount) : null,
|
indexed ? CreateVulkanIndexBuffer(ctx, state, vertexCount) : null,
|
||||||
@@ -3297,7 +3694,7 @@ public static class AgcExports
|
|||||||
vertexInputs,
|
vertexInputs,
|
||||||
renderTargets,
|
renderTargets,
|
||||||
ApplyTransparentPremultipliedFillClear(
|
ApplyTransparentPremultipliedFillClear(
|
||||||
CreateRenderState(state.CxRegisters, renderTargets.FirstOrDefault()),
|
CreateRenderState(state.CxRegisters, renderTargets, pixelState),
|
||||||
textures,
|
textures,
|
||||||
vertexInputs,
|
vertexInputs,
|
||||||
pixelEvaluation.InitialScalarRegisters));
|
pixelEvaluation.InitialScalarRegisters));
|
||||||
@@ -3313,7 +3710,8 @@ public static class AgcExports
|
|||||||
/// Chowdren resets its effect layers with an untextured transparent-black
|
/// Chowdren resets its effect layers with an untextured transparent-black
|
||||||
/// fill using premultiplied blending. With One/OneMinusSrcAlpha that draw
|
/// fill using premultiplied blending. With One/OneMinusSrcAlpha that draw
|
||||||
/// is otherwise a no-op, causing fog and vignette layers to accumulate.
|
/// is otherwise a no-op, causing fog and vignette layers to accumulate.
|
||||||
/// Treat precisely that draw shape as an overwrite.
|
/// Treat precisely that draw shape as an overwrite only when every MRT
|
||||||
|
/// attachment uses the same premultiplied blend pattern.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static VulkanGuestRenderState ApplyTransparentPremultipliedFillClear(
|
private static VulkanGuestRenderState ApplyTransparentPremultipliedFillClear(
|
||||||
VulkanGuestRenderState renderState,
|
VulkanGuestRenderState renderState,
|
||||||
@@ -3325,13 +3723,7 @@ public static class AgcExports
|
|||||||
textures.Count != 0 ||
|
textures.Count != 0 ||
|
||||||
vertexInputs.Count != 0 ||
|
vertexInputs.Count != 0 ||
|
||||||
pixelUserData.Count < 4 ||
|
pixelUserData.Count < 4 ||
|
||||||
renderState.Blend is not
|
!renderState.Blends.All(IsTransparentPremultipliedFillBlend))
|
||||||
{
|
|
||||||
Enable: true,
|
|
||||||
ColorSrcFactor: 1,
|
|
||||||
ColorDstFactor: 5,
|
|
||||||
ColorFunc: 0,
|
|
||||||
})
|
|
||||||
{
|
{
|
||||||
return renderState;
|
return renderState;
|
||||||
}
|
}
|
||||||
@@ -3346,10 +3738,21 @@ public static class AgcExports
|
|||||||
|
|
||||||
return renderState with
|
return renderState with
|
||||||
{
|
{
|
||||||
Blend = renderState.Blend with { Enable = false },
|
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(
|
private static VulkanGuestIndexBuffer? CreateVulkanIndexBuffer(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
SubmittedDcbState state,
|
SubmittedDcbState state,
|
||||||
@@ -3372,19 +3775,15 @@ public static class AgcExports
|
|||||||
: null;
|
: 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) =>
|
private static bool HasPixelColorExport(Gen5ShaderState state, uint target) =>
|
||||||
state.Program.Instructions.Any(instruction =>
|
GetPixelColorExportMask(state, target) != 0;
|
||||||
instruction.Control is Gen5ExportControl export &&
|
|
||||||
export.Target == target &&
|
private static uint GetPixelColorExportMask(Gen5ShaderState state, uint target) =>
|
||||||
export.EnableMask != 0);
|
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)
|
private static uint GetInterpolatedAttributeCount(Gen5ShaderState state)
|
||||||
{
|
{
|
||||||
@@ -3400,6 +3799,88 @@ public static class AgcExports
|
|||||||
return (uint)(maxAttribute + 1);
|
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)
|
private static ulong ComputeShaderStateFingerprint(Gen5ShaderEvaluation evaluation)
|
||||||
{
|
{
|
||||||
const ulong offsetBasis = 14695981039346656037UL;
|
const ulong offsetBasis = 14695981039346656037UL;
|
||||||
@@ -3460,11 +3941,25 @@ public static class AgcExports
|
|||||||
|
|
||||||
private static VulkanGuestRenderState CreateRenderState(
|
private static VulkanGuestRenderState CreateRenderState(
|
||||||
IReadOnlyDictionary<uint, uint> registers,
|
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);
|
var scissor = DecodeScissor(registers, target.Width, target.Height);
|
||||||
return new VulkanGuestRenderState(
|
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,
|
scissor,
|
||||||
DecodeViewport(registers, target.Width, target.Height, scissor));
|
DecodeViewport(registers, target.Width, target.Height, scissor));
|
||||||
}
|
}
|
||||||
@@ -3489,7 +3984,7 @@ public static class AgcExports
|
|||||||
(control >> 24) & 0x1Fu,
|
(control >> 24) & 0x1Fu,
|
||||||
(control >> 21) & 0x7u,
|
(control >> 21) & 0x7u,
|
||||||
((control >> 29) & 1u) != 0,
|
((control >> 29) & 1u) != 0,
|
||||||
writeMask == 0 ? 0xFu : writeMask);
|
writeMask);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static VulkanGuestRect? DecodeScissor(
|
private static VulkanGuestRect? DecodeScissor(
|
||||||
@@ -4250,7 +4745,7 @@ public static class AgcExports
|
|||||||
var localSizeZ = GetComputeLocalSize(state.ShRegisters, ComputeNumThreadZ);
|
var localSizeZ = GetComputeLocalSize(state.ShRegisters, ComputeNumThreadZ);
|
||||||
var gpuDispatch = false;
|
var gpuDispatch = false;
|
||||||
var computeError = string.Empty;
|
var computeError = string.Empty;
|
||||||
if (hasStorageBinding &&
|
if ((hasStorageBinding || evaluation.GlobalMemoryBindings.Count != 0) &&
|
||||||
(ulong)localSizeX * localSizeY * localSizeZ <= 1024)
|
(ulong)localSizeX * localSizeY * localSizeZ <= 1024)
|
||||||
{
|
{
|
||||||
var shaderKey = (
|
var shaderKey = (
|
||||||
@@ -4320,7 +4815,8 @@ public static class AgcExports
|
|||||||
$"groups={dispatch.GroupCountX}x{dispatch.GroupCountY}x{dispatch.GroupCountZ} " +
|
$"groups={dispatch.GroupCountX}x{dispatch.GroupCountY}x{dispatch.GroupCountZ} " +
|
||||||
$"local={localSizeX}x{localSizeY}x{localSizeZ} " +
|
$"local={localSizeX}x{localSizeY}x{localSizeZ} " +
|
||||||
$"sys={DescribeComputeSystemRegisters(computeSystemRegisters)} " +
|
$"sys={DescribeComputeSystemRegisters(computeSystemRegisters)} " +
|
||||||
$"gpu={gpuDispatch} blits={blitCount}" +
|
$"gpu={gpuDispatch} blits={blitCount} " +
|
||||||
|
$"globals={evaluation.GlobalMemoryBindings.Count}" +
|
||||||
(computeError.Length == 0 ? string.Empty : $" error={computeError}") +
|
(computeError.Length == 0 ? string.Empty : $" error={computeError}") +
|
||||||
$" bindings=[{string.Join(',', descriptions)}]");
|
$" bindings=[{string.Join(',', descriptions)}]");
|
||||||
}
|
}
|
||||||
@@ -4826,7 +5322,7 @@ public static class AgcExports
|
|||||||
if (Gen5SpirvTranslator.TryCompilePixelShader(
|
if (Gen5SpirvTranslator.TryCompilePixelShader(
|
||||||
pixelState,
|
pixelState,
|
||||||
evaluation,
|
evaluation,
|
||||||
Gen5PixelOutputKind.Float,
|
[new(0, 0, Gen5PixelOutputKind.Float)],
|
||||||
out var compiledPixel,
|
out var compiledPixel,
|
||||||
out var compileError))
|
out var compileError))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ internal enum Gen5PixelOutputKind
|
|||||||
Sint,
|
Sint,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal readonly record struct Gen5PixelOutputBinding(
|
||||||
|
uint GuestSlot,
|
||||||
|
uint HostLocation,
|
||||||
|
Gen5PixelOutputKind Kind);
|
||||||
|
|
||||||
internal enum Gen5SpirvStage
|
internal enum Gen5SpirvStage
|
||||||
{
|
{
|
||||||
Vertex,
|
Vertex,
|
||||||
|
|||||||
@@ -13,6 +13,24 @@ internal static class Gen5ShaderScalarEvaluator
|
|||||||
private const int ImageDescriptorDwords = 8;
|
private const int ImageDescriptorDwords = 8;
|
||||||
private const int SamplerDescriptorDwords = 4;
|
private const int SamplerDescriptorDwords = 4;
|
||||||
private const int MaxGlobalMemoryBindingBytes = 16 * 1024 * 1024;
|
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 const ulong RdnaWaveMask = 0xFFFF_FFFFUL;
|
||||||
|
|
||||||
private readonly record struct BufferDescriptor(
|
private readonly record struct BufferDescriptor(
|
||||||
@@ -44,7 +62,8 @@ internal static class Gen5ShaderScalarEvaluator
|
|||||||
Gen5ShaderState state,
|
Gen5ShaderState state,
|
||||||
out Gen5ShaderEvaluation evaluation,
|
out Gen5ShaderEvaluation evaluation,
|
||||||
out string error,
|
out string error,
|
||||||
bool resolveVertexInputs = false)
|
bool resolveVertexInputs = false,
|
||||||
|
uint? vertexRecordLimit = null)
|
||||||
{
|
{
|
||||||
evaluation = default!;
|
evaluation = default!;
|
||||||
error = string.Empty;
|
error = string.Empty;
|
||||||
@@ -255,10 +274,28 @@ internal static class Gen5ShaderScalarEvaluator
|
|||||||
if (resolveVertexInputs &&
|
if (resolveVertexInputs &&
|
||||||
IsVertexFetchCandidate(instruction, bufferMemory, bufferDescriptor))
|
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(
|
if (!TryReadGlobalMemory(
|
||||||
ctx,
|
ctx,
|
||||||
bufferDescriptor.BaseAddress,
|
bufferDescriptor.BaseAddress,
|
||||||
bufferDescriptor.SizeBytes,
|
vertexReadSize,
|
||||||
out var vertexData))
|
out var vertexData))
|
||||||
{
|
{
|
||||||
error =
|
error =
|
||||||
@@ -533,22 +570,29 @@ internal static class Gen5ShaderScalarEvaluator
|
|||||||
return false;
|
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(
|
private static bool TryReadGlobalMemory(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
ulong baseAddress,
|
ulong baseAddress,
|
||||||
out byte[] data)
|
out byte[] data)
|
||||||
{
|
{
|
||||||
for (var size = MaxGlobalMemoryBindingBytes; size >= 4096; size >>= 1)
|
return TryReadGlobalMemory(
|
||||||
{
|
ctx,
|
||||||
data = GC.AllocateUninitializedArray<byte>(size);
|
baseAddress,
|
||||||
if (ctx.Memory.TryRead(baseAddress, data))
|
(ulong)DefaultGlobalMemoryBindingBytes,
|
||||||
{
|
out data);
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data = [];
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryReadGlobalMemory(
|
private static bool TryReadGlobalMemory(
|
||||||
@@ -570,13 +614,74 @@ internal static class Gen5ShaderScalarEvaluator
|
|||||||
return false;
|
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;
|
var candidateSize = (int)cappedSize;
|
||||||
while (candidateSize >= sizeof(uint))
|
while (candidateSize >= sizeof(uint))
|
||||||
{
|
{
|
||||||
data = GC.AllocateUninitializedArray<byte>(candidateSize);
|
data = GC.AllocateUninitializedArray<byte>(candidateSize);
|
||||||
if (ctx.Memory.TryRead(baseAddress, data) ||
|
var readFromPvm = ctx.Memory.TryRead(baseAddress, data);
|
||||||
|
if (readFromPvm ||
|
||||||
KernelMemoryCompatExports.TryReadTrackedLibcHeap(baseAddress, data))
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -802,8 +802,15 @@ internal static class Gen5ShaderTranslator
|
|||||||
0x14 => "VCmpxGtF32",
|
0x14 => "VCmpxGtF32",
|
||||||
0x15 => "VCmpxLgF32",
|
0x15 => "VCmpxLgF32",
|
||||||
0x16 => "VCmpxGeF32",
|
0x16 => "VCmpxGeF32",
|
||||||
|
0x17 => "VCmpxOF32",
|
||||||
|
0x18 => "VCmpxUF32",
|
||||||
|
0x19 => "VCmpxNgeF32",
|
||||||
|
0x1A => "VCmpxNlgF32",
|
||||||
|
0x1B => "VCmpxNgtF32",
|
||||||
|
0x1C => "VCmpxNleF32",
|
||||||
0x1D => "VCmpxNeqF32",
|
0x1D => "VCmpxNeqF32",
|
||||||
0x1E => "VCmpxNltF32",
|
0x1E => "VCmpxNltF32",
|
||||||
|
0x1F => "VCmpxTruF32",
|
||||||
0x80 => "VCmpFI32",
|
0x80 => "VCmpFI32",
|
||||||
0x81 => "VCmpLtI32",
|
0x81 => "VCmpLtI32",
|
||||||
0x82 => "VCmpEqI32",
|
0x82 => "VCmpEqI32",
|
||||||
|
|||||||
@@ -396,6 +396,14 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
_module.Constant64(_ulongType, 32)));
|
_module.Constant64(_ulongType, 32)));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "VBcntU32B32":
|
||||||
|
result = IAdd(
|
||||||
|
_module.AddInstruction(
|
||||||
|
SpirvOp.BitCount,
|
||||||
|
_uintType,
|
||||||
|
GetRawSource(instruction, 0)),
|
||||||
|
GetRawSource(instruction, 1));
|
||||||
|
break;
|
||||||
case "VMadU32U24":
|
case "VMadU32U24":
|
||||||
{
|
{
|
||||||
var left = BitwiseAnd(
|
var left = BitwiseAnd(
|
||||||
@@ -593,6 +601,18 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
Ext(38, _uintType, high, right));
|
Ext(38, _uintType, high, right));
|
||||||
break;
|
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":
|
case "VMed3I32":
|
||||||
{
|
{
|
||||||
var left = Bitcast(_intType, GetRawSource(instruction, 0));
|
var left = Bitcast(_intType, GetRawSource(instruction, 0));
|
||||||
@@ -702,6 +722,14 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
result = Ext(58, _uintType, vector);
|
result = Ext(58, _uintType, vector);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "VCvtPkU16U32":
|
||||||
|
case "VCvtPkI16I32":
|
||||||
|
result = BitwiseOr(
|
||||||
|
BitwiseAnd(GetRawSource(instruction, 0), UInt(0xFFFF)),
|
||||||
|
ShiftLeftLogical(
|
||||||
|
BitwiseAnd(GetRawSource(instruction, 1), UInt(0xFFFF)),
|
||||||
|
UInt(16)));
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
error = $"unsupported vector opcode {instruction.Opcode}";
|
error = $"unsupported vector opcode {instruction.Opcode}";
|
||||||
return false;
|
return false;
|
||||||
@@ -833,6 +861,25 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
{
|
{
|
||||||
condition = _module.ConstantBool(true);
|
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") &&
|
else if (opcode is not ("VCmpClassF32" or "VCmpxClassF32") &&
|
||||||
opcode.EndsWith("F32", StringComparison.Ordinal))
|
opcode.EndsWith("F32", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
@@ -847,6 +894,7 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
"VCmpLgF32" or "VCmpxLgF32" => SpirvOp.FOrdNotEqual,
|
"VCmpLgF32" or "VCmpxLgF32" => SpirvOp.FOrdNotEqual,
|
||||||
"VCmpGeF32" or "VCmpxGeF32" => SpirvOp.FOrdGreaterThanEqual,
|
"VCmpGeF32" or "VCmpxGeF32" => SpirvOp.FOrdGreaterThanEqual,
|
||||||
"VCmpNeqF32" or "VCmpxNeqF32" => SpirvOp.FUnordNotEqual,
|
"VCmpNeqF32" or "VCmpxNeqF32" => SpirvOp.FUnordNotEqual,
|
||||||
|
"VCmpNlgF32" or "VCmpxNlgF32" => SpirvOp.FUnordEqual,
|
||||||
"VCmpNltF32" or "VCmpxNltF32" => SpirvOp.FUnordGreaterThanEqual,
|
"VCmpNltF32" or "VCmpxNltF32" => SpirvOp.FUnordGreaterThanEqual,
|
||||||
"VCmpNleF32" or "VCmpxNleF32" => SpirvOp.FUnordGreaterThan,
|
"VCmpNleF32" or "VCmpxNleF32" => SpirvOp.FUnordGreaterThan,
|
||||||
"VCmpNgtF32" or "VCmpxNgtF32" => SpirvOp.FUnordLessThanEqual,
|
"VCmpNgtF32" or "VCmpxNgtF32" => SpirvOp.FUnordLessThanEqual,
|
||||||
@@ -897,7 +945,8 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
condition = _module.AddInstruction(operation, _boolType, left, right);
|
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))
|
if (opcode.StartsWith("VCmpx", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
var active = _module.AddInstruction(
|
var active = _module.AddInstruction(
|
||||||
@@ -907,6 +956,10 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
condition);
|
condition);
|
||||||
StoreWaveMask(126, active);
|
StoreWaveMask(126, active);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StoreWaveMask(106, condition);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1303,6 +1356,43 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
Store(_scc, IsNotZero(result));
|
Store(_scc, IsNotZero(result));
|
||||||
break;
|
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":
|
case "SCselectB32":
|
||||||
result = _module.AddInstruction(
|
result = _module.AddInstruction(
|
||||||
SpirvOp.Select,
|
SpirvOp.Select,
|
||||||
|
|||||||
@@ -18,19 +18,67 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
out string error,
|
out string error,
|
||||||
int globalBufferBase = 0,
|
int globalBufferBase = 0,
|
||||||
int totalGlobalBufferCount = -1,
|
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(
|
var context = new CompilationContext(
|
||||||
Gen5SpirvStage.Pixel,
|
Gen5SpirvStage.Pixel,
|
||||||
state,
|
state,
|
||||||
evaluation,
|
evaluation,
|
||||||
outputKind,
|
outputs,
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
globalBufferBase,
|
globalBufferBase,
|
||||||
totalGlobalBufferCount,
|
totalGlobalBufferCount,
|
||||||
imageBindingBase);
|
imageBindingBase,
|
||||||
|
scalarRegisterBufferIndex);
|
||||||
return context.TryCompile(out shader, out error);
|
return context.TryCompile(out shader, out error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,19 +89,21 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
out string error,
|
out string error,
|
||||||
int globalBufferBase = 0,
|
int globalBufferBase = 0,
|
||||||
int totalGlobalBufferCount = -1,
|
int totalGlobalBufferCount = -1,
|
||||||
int imageBindingBase = 0)
|
int imageBindingBase = 0,
|
||||||
|
int scalarRegisterBufferIndex = -1)
|
||||||
{
|
{
|
||||||
var context = new CompilationContext(
|
var context = new CompilationContext(
|
||||||
Gen5SpirvStage.Vertex,
|
Gen5SpirvStage.Vertex,
|
||||||
state,
|
state,
|
||||||
evaluation,
|
evaluation,
|
||||||
Gen5PixelOutputKind.Float,
|
[],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
globalBufferBase,
|
globalBufferBase,
|
||||||
totalGlobalBufferCount,
|
totalGlobalBufferCount,
|
||||||
imageBindingBase);
|
imageBindingBase,
|
||||||
|
scalarRegisterBufferIndex);
|
||||||
return context.TryCompile(out shader, out error);
|
return context.TryCompile(out shader, out error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,13 +120,14 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
Gen5SpirvStage.Compute,
|
Gen5SpirvStage.Compute,
|
||||||
state,
|
state,
|
||||||
evaluation,
|
evaluation,
|
||||||
Gen5PixelOutputKind.Float,
|
[],
|
||||||
Math.Max(localSizeX, 1),
|
Math.Max(localSizeX, 1),
|
||||||
Math.Max(localSizeY, 1),
|
Math.Max(localSizeY, 1),
|
||||||
Math.Max(localSizeZ, 1),
|
Math.Max(localSizeZ, 1),
|
||||||
0,
|
0,
|
||||||
-1,
|
-1,
|
||||||
0);
|
0,
|
||||||
|
-1);
|
||||||
return context.TryCompile(out shader, out error);
|
return context.TryCompile(out shader, out error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,15 +137,17 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
private readonly Gen5SpirvStage _stage;
|
private readonly Gen5SpirvStage _stage;
|
||||||
private readonly Gen5ShaderState _state;
|
private readonly Gen5ShaderState _state;
|
||||||
private readonly Gen5ShaderEvaluation _evaluation;
|
private readonly Gen5ShaderEvaluation _evaluation;
|
||||||
private readonly Gen5PixelOutputKind _outputKind;
|
private readonly IReadOnlyList<Gen5PixelOutputBinding> _pixelOutputBindings;
|
||||||
private readonly uint _localSizeX;
|
private readonly uint _localSizeX;
|
||||||
private readonly uint _localSizeY;
|
private readonly uint _localSizeY;
|
||||||
private readonly uint _localSizeZ;
|
private readonly uint _localSizeZ;
|
||||||
private readonly int _globalBufferBase;
|
private readonly int _globalBufferBase;
|
||||||
private readonly int _totalGlobalBufferCount;
|
private readonly int _totalGlobalBufferCount;
|
||||||
private readonly int _imageBindingBase;
|
private readonly int _imageBindingBase;
|
||||||
|
private readonly int _scalarRegisterBufferIndex;
|
||||||
private readonly List<uint> _interfaces = [];
|
private readonly List<uint> _interfaces = [];
|
||||||
private readonly Dictionary<uint, uint> _pixelInputs = [];
|
private readonly Dictionary<uint, uint> _pixelInputs = [];
|
||||||
|
private readonly Dictionary<uint, SpirvPixelOutput> _pixelOutputs = [];
|
||||||
private readonly Dictionary<uint, uint> _vertexOutputs = [];
|
private readonly Dictionary<uint, uint> _vertexOutputs = [];
|
||||||
private readonly Dictionary<uint, SpirvVertexInput> _vertexInputsByPc = [];
|
private readonly Dictionary<uint, SpirvVertexInput> _vertexInputsByPc = [];
|
||||||
private readonly List<SpirvImageResource> _imageResources = [];
|
private readonly List<SpirvImageResource> _imageResources = [];
|
||||||
@@ -127,7 +180,6 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
private uint _lds;
|
private uint _lds;
|
||||||
private uint _workgroupUintPointer;
|
private uint _workgroupUintPointer;
|
||||||
private uint _positionOutput;
|
private uint _positionOutput;
|
||||||
private uint _pixelOutput;
|
|
||||||
private uint _vertexIndexInput;
|
private uint _vertexIndexInput;
|
||||||
private uint _instanceIndexInput;
|
private uint _instanceIndexInput;
|
||||||
private uint _fragCoordInput;
|
private uint _fragCoordInput;
|
||||||
@@ -157,22 +209,28 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
uint Type,
|
uint Type,
|
||||||
uint ComponentCount);
|
uint ComponentCount);
|
||||||
|
|
||||||
|
private readonly record struct SpirvPixelOutput(
|
||||||
|
uint Variable,
|
||||||
|
uint Type,
|
||||||
|
Gen5PixelOutputKind Kind);
|
||||||
|
|
||||||
public CompilationContext(
|
public CompilationContext(
|
||||||
Gen5SpirvStage stage,
|
Gen5SpirvStage stage,
|
||||||
Gen5ShaderState state,
|
Gen5ShaderState state,
|
||||||
Gen5ShaderEvaluation evaluation,
|
Gen5ShaderEvaluation evaluation,
|
||||||
Gen5PixelOutputKind outputKind,
|
IReadOnlyList<Gen5PixelOutputBinding> pixelOutputBindings,
|
||||||
uint localSizeX,
|
uint localSizeX,
|
||||||
uint localSizeY,
|
uint localSizeY,
|
||||||
uint localSizeZ,
|
uint localSizeZ,
|
||||||
int globalBufferBase,
|
int globalBufferBase,
|
||||||
int totalGlobalBufferCount,
|
int totalGlobalBufferCount,
|
||||||
int imageBindingBase)
|
int imageBindingBase,
|
||||||
|
int scalarRegisterBufferIndex)
|
||||||
{
|
{
|
||||||
_stage = stage;
|
_stage = stage;
|
||||||
_state = state;
|
_state = state;
|
||||||
_evaluation = evaluation;
|
_evaluation = evaluation;
|
||||||
_outputKind = outputKind;
|
_pixelOutputBindings = pixelOutputBindings;
|
||||||
_localSizeX = localSizeX;
|
_localSizeX = localSizeX;
|
||||||
_localSizeY = localSizeY;
|
_localSizeY = localSizeY;
|
||||||
_localSizeZ = localSizeZ;
|
_localSizeZ = localSizeZ;
|
||||||
@@ -181,6 +239,7 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
? evaluation.GlobalMemoryBindings.Count
|
? evaluation.GlobalMemoryBindings.Count
|
||||||
: totalGlobalBufferCount;
|
: totalGlobalBufferCount;
|
||||||
_imageBindingBase = imageBindingBase;
|
_imageBindingBase = imageBindingBase;
|
||||||
|
_scalarRegisterBufferIndex = scalarRegisterBufferIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryCompile(out Gen5SpirvShader shader, out string error)
|
public bool TryCompile(out Gen5SpirvShader shader, out string error)
|
||||||
@@ -692,19 +751,24 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
(uint)SpirvBuiltIn.FragCoord);
|
(uint)SpirvBuiltIn.FragCoord);
|
||||||
_interfaces.Add(_fragCoordInput);
|
_interfaces.Add(_fragCoordInput);
|
||||||
|
|
||||||
var outputType = _outputKind switch
|
foreach (var binding in _pixelOutputBindings)
|
||||||
{
|
{
|
||||||
Gen5PixelOutputKind.Uint => _uvec4Type,
|
var outputType = GetPixelOutputType(binding.Kind);
|
||||||
Gen5PixelOutputKind.Sint => _module.TypeVector(_intType, 4),
|
var outputPointer =
|
||||||
_ => _vec4Type,
|
_module.TypePointer(SpirvStorageClass.Output, outputType);
|
||||||
};
|
var variable = _module.AddGlobalVariable(
|
||||||
var outputPointer =
|
outputPointer,
|
||||||
_module.TypePointer(SpirvStorageClass.Output, outputType);
|
SpirvStorageClass.Output);
|
||||||
_pixelOutput = _module.AddGlobalVariable(
|
_module.AddName(variable, $"mrt{binding.GuestSlot}");
|
||||||
outputPointer,
|
_module.AddDecoration(
|
||||||
SpirvStorageClass.Output);
|
variable,
|
||||||
_module.AddDecoration(_pixelOutput, SpirvDecoration.Location, 0);
|
SpirvDecoration.Location,
|
||||||
_interfaces.Add(_pixelOutput);
|
binding.HostLocation);
|
||||||
|
_pixelOutputs.Add(
|
||||||
|
binding.GuestSlot,
|
||||||
|
new SpirvPixelOutput(variable, outputType, binding.Kind));
|
||||||
|
_interfaces.Add(variable);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -767,15 +831,25 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
|
|
||||||
private void EmitInitialState()
|
private void EmitInitialState()
|
||||||
{
|
{
|
||||||
for (uint index = 0;
|
if (_scalarRegisterBufferIndex >= 0)
|
||||||
index < _evaluation.InitialScalarRegisters.Count &&
|
|
||||||
index < ScalarRegisterCount;
|
|
||||||
index++)
|
|
||||||
{
|
{
|
||||||
var value = _evaluation.InitialScalarRegisters[(int)index];
|
for (uint index = 0; index < ScalarRegisterCount; index++)
|
||||||
if (value != 0)
|
|
||||||
{
|
{
|
||||||
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);
|
1);
|
||||||
StoreV(2, Bitcast(_uintType, x), guardWithExec: false);
|
StoreV(2, Bitcast(_uintType, x), guardWithExec: false);
|
||||||
StoreV(3, Bitcast(_uintType, y), 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
|
else
|
||||||
{
|
{
|
||||||
@@ -2066,21 +2143,27 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
|
|
||||||
if (_stage == Gen5SpirvStage.Pixel)
|
if (_stage == Gen5SpirvStage.Pixel)
|
||||||
{
|
{
|
||||||
if (export.Target != 0)
|
if (!_pixelOutputs.TryGetValue(export.Target, out var output))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var outputType = GetPixelOutputType();
|
|
||||||
var values = new uint[4];
|
var values = new uint[4];
|
||||||
for (var component = 0; component < 4; component++)
|
for (var component = 0; component < 4; component++)
|
||||||
{
|
{
|
||||||
var enabled = (export.EnableMask & (1u << component)) != 0;
|
var enabled = (export.EnableMask & (1u << component)) != 0;
|
||||||
if (!enabled)
|
if (!enabled)
|
||||||
{
|
{
|
||||||
values[component] = _outputKind == Gen5PixelOutputKind.Sint
|
values[component] = _module.AddInstruction(
|
||||||
? Bitcast(_intType, UInt(0))
|
SpirvOp.CompositeExtract,
|
||||||
: UInt(0);
|
output.Kind switch
|
||||||
|
{
|
||||||
|
Gen5PixelOutputKind.Uint => _uintType,
|
||||||
|
Gen5PixelOutputKind.Sint => _intType,
|
||||||
|
_ => _floatType,
|
||||||
|
},
|
||||||
|
Load(output.Type, output.Variable),
|
||||||
|
(uint)component);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2089,7 +2172,7 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
var value = LoadCompressedExportComponent(
|
var value = LoadCompressedExportComponent(
|
||||||
instruction,
|
instruction,
|
||||||
component);
|
component);
|
||||||
values[component] = _outputKind switch
|
values[component] = output.Kind switch
|
||||||
{
|
{
|
||||||
Gen5PixelOutputKind.Uint => _module.AddInstruction(
|
Gen5PixelOutputKind.Uint => _module.AddInstruction(
|
||||||
SpirvOp.ConvertFToU,
|
SpirvOp.ConvertFToU,
|
||||||
@@ -2105,7 +2188,7 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
}
|
}
|
||||||
|
|
||||||
var raw = LoadV(instruction.Sources[component].Value);
|
var raw = LoadV(instruction.Sources[component].Value);
|
||||||
values[component] = _outputKind switch
|
values[component] = output.Kind switch
|
||||||
{
|
{
|
||||||
Gen5PixelOutputKind.Uint => raw,
|
Gen5PixelOutputKind.Uint => raw,
|
||||||
Gen5PixelOutputKind.Sint => Bitcast(_intType, raw),
|
Gen5PixelOutputKind.Sint => Bitcast(_intType, raw),
|
||||||
@@ -2115,15 +2198,15 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
|
|
||||||
var vector = _module.AddInstruction(
|
var vector = _module.AddInstruction(
|
||||||
SpirvOp.CompositeConstruct,
|
SpirvOp.CompositeConstruct,
|
||||||
outputType,
|
output.Type,
|
||||||
values);
|
values);
|
||||||
vector = _module.AddInstruction(
|
vector = _module.AddInstruction(
|
||||||
SpirvOp.Select,
|
SpirvOp.Select,
|
||||||
outputType,
|
output.Type,
|
||||||
Load(_boolType, _exec),
|
Load(_boolType, _exec),
|
||||||
vector,
|
vector,
|
||||||
Load(outputType, _pixelOutput));
|
Load(output.Type, output.Variable));
|
||||||
Store(_pixelOutput, vector);
|
Store(output.Variable, vector);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2191,8 +2274,8 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
(uint)(component & 1));
|
(uint)(component & 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
private uint GetPixelOutputType() =>
|
private uint GetPixelOutputType(Gen5PixelOutputKind kind) =>
|
||||||
_outputKind switch
|
kind switch
|
||||||
{
|
{
|
||||||
Gen5PixelOutputKind.Uint => _uvec4Type,
|
Gen5PixelOutputKind.Uint => _uvec4Type,
|
||||||
Gen5PixelOutputKind.Sint => _module.TypeVector(_intType, 4),
|
Gen5PixelOutputKind.Sint => _module.TypeVector(_intType, 4),
|
||||||
|
|||||||
@@ -348,6 +348,18 @@ public static class AmprExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
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(
|
[SysAbiExport(
|
||||||
Nid = "C+IEj+BsAFM",
|
Nid = "C+IEj+BsAFM",
|
||||||
ExportName = "sceAmprMeasureCommandSizeWriteAddressOnCompletion",
|
ExportName = "sceAmprMeasureCommandSizeWriteAddressOnCompletion",
|
||||||
@@ -440,6 +452,40 @@ public static class AmprExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
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(
|
[SysAbiExport(
|
||||||
Nid = "sJXyWHjP-F8",
|
Nid = "sJXyWHjP-F8",
|
||||||
ExportName = "sceAmprCommandBufferWriteAddressOnCompletion",
|
ExportName = "sceAmprCommandBufferWriteAddressOnCompletion",
|
||||||
|
|||||||
@@ -54,20 +54,41 @@ internal static class PakDirectoryTracker
|
|||||||
|
|
||||||
if (state.Directory is { } entries)
|
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)
|
foreach (var entry in entries)
|
||||||
{
|
{
|
||||||
if (!entry.Consumed && entry.FileLen == requestedSize)
|
if (entry.Consumed || entry.FileLen != requestedSize)
|
||||||
{
|
{
|
||||||
entry.Consumed = true;
|
continue;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -213,6 +213,17 @@ public static class AudioOutExports
|
|||||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
: (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(
|
private static bool TryGetFormat(
|
||||||
int rawFormat,
|
int rawFormat,
|
||||||
out int channels,
|
out int channels,
|
||||||
|
|||||||
@@ -35,6 +35,17 @@ public static class AvPlayerExports
|
|||||||
return unchecked((int)handle);
|
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(
|
[SysAbiExport(
|
||||||
Nid = "JdksQu8pNdQ",
|
Nid = "JdksQu8pNdQ",
|
||||||
ExportName = "sceAvPlayerGetVideoDataEx",
|
ExportName = "sceAvPlayerGetVideoDataEx",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using System.Buffers.Binary;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.CommonDialog;
|
namespace SharpEmu.Libs.CommonDialog;
|
||||||
@@ -10,10 +11,21 @@ public static class MsgDialogExports
|
|||||||
{
|
{
|
||||||
private const int StatusNone = 0;
|
private const int StatusNone = 0;
|
||||||
private const int StatusInitialized = 1;
|
private const int StatusInitialized = 1;
|
||||||
|
private const int StatusRunning = 2;
|
||||||
private const int StatusFinished = 3;
|
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;
|
private static int _status;
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -24,11 +36,10 @@ public static class MsgDialogExports
|
|||||||
public static int MsgDialogInitialize(CpuContext ctx)
|
public static int MsgDialogInitialize(CpuContext ctx)
|
||||||
{
|
{
|
||||||
// Treat repeated initialization as success. The dialog service is process-global in
|
// Treat repeated initialization as success. The dialog service is process-global in
|
||||||
// this HLE implementation and has no per-call resources to recreate.
|
// this HLE implementation and has no per-call resources to recreate. Only promote
|
||||||
Interlocked.Exchange(ref _initialized, 1);
|
// from NONE so re-initializing mid-flow cannot clobber a running/finished dialog.
|
||||||
Interlocked.Exchange(ref _status, StatusInitialized);
|
Interlocked.CompareExchange(ref _status, StatusInitialized, StatusNone);
|
||||||
ctx[CpuRegister.Rax] = 0;
|
return ctx.SetReturn(ErrorOk);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -38,9 +49,12 @@ public static class MsgDialogExports
|
|||||||
LibraryName = "libSceMsgDialog")]
|
LibraryName = "libSceMsgDialog")]
|
||||||
public static int MsgDialogTerminate(CpuContext ctx)
|
public static int MsgDialogTerminate(CpuContext ctx)
|
||||||
{
|
{
|
||||||
Interlocked.Exchange(ref _initialized, 0);
|
if (Interlocked.Exchange(ref _status, StatusNone) == StatusNone)
|
||||||
Interlocked.Exchange(ref _status, StatusNone);
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(ErrorNotInitialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.SetReturn(ErrorOk);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -50,13 +64,29 @@ public static class MsgDialogExports
|
|||||||
LibraryName = "libSceMsgDialog")]
|
LibraryName = "libSceMsgDialog")]
|
||||||
public static int MsgDialogOpen(CpuContext ctx)
|
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
|
var status = Volatile.Read(ref _status);
|
||||||
// guest polling loop (GetStatus/UpdateStatus -> GetResult -> Close) sees a dismissed
|
if (status == StatusNone)
|
||||||
// dialog on its very first poll instead of spinning forever waiting for user input.
|
{
|
||||||
Interlocked.Exchange(ref _status, StatusFinished);
|
return ctx.SetReturn(ErrorNotInitialized);
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
}
|
||||||
|
|
||||||
|
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
|
// 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.
|
// level deep, then a second level for nested sub-param structs.
|
||||||
private static void LogDialogMessage(CpuContext ctx, ulong paramAddress)
|
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}");
|
Console.Error.WriteLine($"[LOADER][INFO] sceMsgDialogOpen: param=0x{paramAddress:X12}");
|
||||||
|
|
||||||
Span<byte> head = stackalloc byte[0xA0];
|
Span<byte> head = stackalloc byte[0xA0];
|
||||||
@@ -134,14 +158,24 @@ public static class MsgDialogExports
|
|||||||
ExportName = "sceMsgDialogGetStatus",
|
ExportName = "sceMsgDialogGetStatus",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libSceMsgDialog")]
|
LibraryName = "libSceMsgDialog")]
|
||||||
public static int MsgDialogGetStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
|
public static int MsgDialogGetStatus(CpuContext ctx) => ctx.SetReturn(PollStatus());
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "6fIC3XKt2k0",
|
Nid = "6fIC3XKt2k0",
|
||||||
ExportName = "sceMsgDialogUpdateStatus",
|
ExportName = "sceMsgDialogUpdateStatus",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libSceMsgDialog")]
|
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(
|
[SysAbiExport(
|
||||||
Nid = "Lr8ovHH9l6A",
|
Nid = "Lr8ovHH9l6A",
|
||||||
@@ -153,17 +187,27 @@ public static class MsgDialogExports
|
|||||||
var resultAddress = ctx[CpuRegister.Rdi];
|
var resultAddress = ctx[CpuRegister.Rdi];
|
||||||
if (resultAddress == 0)
|
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];
|
Span<byte> result = stackalloc byte[ResultSize];
|
||||||
result.Clear();
|
result.Clear();
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(result[0x04..], 0);
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(result[0x08..], ButtonIdAffirmative);
|
||||||
|
|
||||||
if (!ctx.Memory.TryWrite(resultAddress, result))
|
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(
|
[SysAbiExport(
|
||||||
@@ -173,13 +217,44 @@ public static class MsgDialogExports
|
|||||||
LibraryName = "libSceMsgDialog")]
|
LibraryName = "libSceMsgDialog")]
|
||||||
public static int MsgDialogClose(CpuContext ctx)
|
public static int MsgDialogClose(CpuContext ctx)
|
||||||
{
|
{
|
||||||
Interlocked.Exchange(ref _status, StatusFinished);
|
if (Interlocked.CompareExchange(ref _status, StatusFinished, StatusRunning) != StatusRunning)
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
{
|
||||||
|
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);
|
if (Volatile.Read(ref _status) == StatusNone)
|
||||||
return result;
|
{
|
||||||
|
return ctx.SetReturn(ErrorNotInitialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.SetReturn(ErrorOk);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,9 +78,33 @@ public static class JsonExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
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(
|
[SysAbiExport(
|
||||||
Nid = "WSOuge5IsCg",
|
Nid = "WSOuge5IsCg",
|
||||||
ExportName = "_ZN3sce4Json15InitParameter2C2Ev",
|
ExportName = "_ZN3sce4Json14InitParameter2C1Ev",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libSceJson2")]
|
LibraryName = "libSceJson2")]
|
||||||
public static int InitParameter2Constructor(CpuContext ctx)
|
public static int InitParameter2Constructor(CpuContext ctx)
|
||||||
@@ -93,7 +117,7 @@ public static class JsonExports
|
|||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "I2QC8PYhJWY",
|
Nid = "I2QC8PYhJWY",
|
||||||
ExportName = "_ZN3sce4Json15InitParameter212setAllocatorERNS0_12MemAllocatorE",
|
ExportName = "_ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libSceJson2")]
|
LibraryName = "libSceJson2")]
|
||||||
public static int InitParameter2SetAllocator(CpuContext ctx)
|
public static int InitParameter2SetAllocator(CpuContext ctx)
|
||||||
@@ -106,7 +130,7 @@ public static class JsonExports
|
|||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "Eu95jmqn5Rw",
|
Nid = "Eu95jmqn5Rw",
|
||||||
ExportName = "_ZN3sce4Json15InitParameter217setFileBufferSizeEm",
|
ExportName = "_ZN3sce4Json14InitParameter217setFileBufferSizeEm",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libSceJson2")]
|
LibraryName = "libSceJson2")]
|
||||||
public static int InitParameter2SetFileBufferSize(CpuContext ctx)
|
public static int InitParameter2SetFileBufferSize(CpuContext ctx)
|
||||||
@@ -119,7 +143,7 @@ public static class JsonExports
|
|||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "IXW-z8pggfg",
|
Nid = "IXW-z8pggfg",
|
||||||
ExportName = "_ZN3sce4Json12Initializer2C1Ev",
|
ExportName = "_ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libSceJson2")]
|
LibraryName = "libSceJson2")]
|
||||||
public static int Initializer2Constructor(CpuContext ctx)
|
public static int Initializer2Constructor(CpuContext ctx)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Json;
|
||||||
|
|
||||||
|
// sce::Json::Value is an opaque variant type (null / bool / signed / unsigned / real / string /
|
||||||
|
// array / object). Games build one, populate it through set()/ctors and later serialize it. We
|
||||||
|
// model the payload host-side keyed by the guest `this` pointer instead of writing into the guest
|
||||||
|
// object: the object is often stack-allocated and its real byte layout is unknown, so writing a
|
||||||
|
// guessed layout risks smashing an adjacent stack canary (the same failure the AudioOut2 context
|
||||||
|
// param sizing note in this project already ran into). The guest reaches the payload only through
|
||||||
|
// libSceJson methods, so shadowing by address is enough for the build path.
|
||||||
|
internal enum JsonValueKind : byte
|
||||||
|
{
|
||||||
|
Null = 0,
|
||||||
|
Boolean = 1,
|
||||||
|
Integer = 2,
|
||||||
|
UInteger = 3,
|
||||||
|
Real = 4,
|
||||||
|
String = 5,
|
||||||
|
|
||||||
|
// set(ValueType) / Value(ValueType): the guest chose the type itself. We keep its raw enum
|
||||||
|
// value verbatim rather than mapping it, because the canonical ValueType constants are not
|
||||||
|
// known from clean-room evidence and round-tripping the guest's own value is what matters.
|
||||||
|
ExplicitType = 6,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal readonly struct JsonValueState
|
||||||
|
{
|
||||||
|
private JsonValueState(
|
||||||
|
JsonValueKind kind,
|
||||||
|
bool boolean = false,
|
||||||
|
long integer = 0,
|
||||||
|
ulong unsignedInteger = 0,
|
||||||
|
double real = 0,
|
||||||
|
string? text = null,
|
||||||
|
uint explicitType = 0)
|
||||||
|
{
|
||||||
|
Kind = kind;
|
||||||
|
Boolean = boolean;
|
||||||
|
Integer = integer;
|
||||||
|
UnsignedInteger = unsignedInteger;
|
||||||
|
Real = real;
|
||||||
|
Text = text;
|
||||||
|
ExplicitType = explicitType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonValueKind Kind { get; }
|
||||||
|
public bool Boolean { get; }
|
||||||
|
public long Integer { get; }
|
||||||
|
public ulong UnsignedInteger { get; }
|
||||||
|
public double Real { get; }
|
||||||
|
public string? Text { get; }
|
||||||
|
public uint ExplicitType { get; }
|
||||||
|
|
||||||
|
public static JsonValueState Null { get; } = new(JsonValueKind.Null);
|
||||||
|
|
||||||
|
public static JsonValueState FromBoolean(bool value) => new(JsonValueKind.Boolean, boolean: value);
|
||||||
|
|
||||||
|
public static JsonValueState FromInteger(long value) => new(JsonValueKind.Integer, integer: value);
|
||||||
|
|
||||||
|
public static JsonValueState FromUnsignedInteger(ulong value) =>
|
||||||
|
new(JsonValueKind.UInteger, unsignedInteger: value);
|
||||||
|
|
||||||
|
public static JsonValueState FromReal(double value) => new(JsonValueKind.Real, real: value);
|
||||||
|
|
||||||
|
public static JsonValueState FromString(string value) => new(JsonValueKind.String, text: value);
|
||||||
|
|
||||||
|
public static JsonValueState FromExplicitType(uint value) =>
|
||||||
|
new(JsonValueKind.ExplicitType, explicitType: value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shared host-side heap for the libSceJson object shadows. Keyed by the guest object address;
|
||||||
|
// constructors overwrite and destructors remove, so guest stack-address reuse stays correct.
|
||||||
|
internal static class JsonObjectHeap
|
||||||
|
{
|
||||||
|
public static ConcurrentDictionary<ulong, JsonValueState> Values { get; } = new();
|
||||||
|
|
||||||
|
public static ConcurrentDictionary<ulong, string> Strings { get; } = new();
|
||||||
|
|
||||||
|
// Guest function the library should call when a Value is read as the wrong type. This HLE
|
||||||
|
// never dereferences missing members (shadows degrade to defaults), so the hook is stored for
|
||||||
|
// fidelity but not invoked.
|
||||||
|
public static ulong GlobalNullAccessCallback;
|
||||||
|
|
||||||
|
public static ulong GlobalNullAccessCallbackContext;
|
||||||
|
|
||||||
|
public static void SetValue(ulong address, JsonValueState state) => Values[address] = state;
|
||||||
|
|
||||||
|
public static void RemoveValue(ulong address) => Values.TryRemove(address, out _);
|
||||||
|
|
||||||
|
public static void SetString(ulong address, string text) => Strings[address] = text;
|
||||||
|
|
||||||
|
public static void RemoveString(ulong address) => Strings.TryRemove(address, out _);
|
||||||
|
|
||||||
|
// A missing shadow (temporary the compiler built without an out-of-line ctor, or a copy we did
|
||||||
|
// not track) degrades to the empty string rather than faulting.
|
||||||
|
public static string GetStringOrEmpty(ulong address) =>
|
||||||
|
Strings.TryGetValue(address, out var text) ? text : string.Empty;
|
||||||
|
|
||||||
|
internal static void ResetForTests()
|
||||||
|
{
|
||||||
|
Values.Clear();
|
||||||
|
Strings.Clear();
|
||||||
|
GlobalNullAccessCallback = 0;
|
||||||
|
GlobalNullAccessCallbackContext = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.Libs.Fiber;
|
using SharpEmu.Libs.Fiber;
|
||||||
@@ -33,6 +34,45 @@ public static class KernelEventFlagCompatExports
|
|||||||
public object Gate { get; } = new();
|
public object Gate { get; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class EventFlagWaiter : IGuestThreadBlockWaiter
|
||||||
|
{
|
||||||
|
public required CpuContext Ctx { get; init; }
|
||||||
|
public required EventFlagState State { get; init; }
|
||||||
|
public required ulong Pattern { get; init; }
|
||||||
|
public required uint WaitMode { get; init; }
|
||||||
|
public required ulong ResultAddress { get; init; }
|
||||||
|
public bool Timed { get; init; }
|
||||||
|
|
||||||
|
// Timed-wait completion state; unused when Timed is false.
|
||||||
|
public ulong TimeoutAddress { get; init; }
|
||||||
|
public long DeadlineTimestamp { get; init; }
|
||||||
|
|
||||||
|
public OrbisGen2Result? Result { get; set; }
|
||||||
|
|
||||||
|
// Untimed waits stash the prepared result here at wake and return it at resume.
|
||||||
|
private OrbisGen2Result _blockedResult = OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
|
||||||
|
public int Resume() => Timed
|
||||||
|
? CompleteBlockedTimedWait(Ctx, State, this, Pattern, WaitMode, ResultAddress, TimeoutAddress, DeadlineTimestamp)
|
||||||
|
: (int)_blockedResult;
|
||||||
|
|
||||||
|
public bool TryWake()
|
||||||
|
{
|
||||||
|
if (Timed)
|
||||||
|
{
|
||||||
|
return TryCompleteBlockedTimedWait(Ctx, State, this, Pattern, WaitMode, ResultAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryPrepareBlockedWait(Ctx, State, Pattern, WaitMode, ResultAddress, out var preparedResult))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_blockedResult = preparedResult;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "BpFoboUJoZU",
|
Nid = "BpFoboUJoZU",
|
||||||
ExportName = "sceKernelCreateEventFlag",
|
ExportName = "sceKernelCreateEventFlag",
|
||||||
@@ -233,36 +273,59 @@ public static class KernelEventFlagCompatExports
|
|||||||
|
|
||||||
if (timeoutAddress != 0)
|
if (timeoutAddress != 0)
|
||||||
{
|
{
|
||||||
|
if (timeoutUsec == 0)
|
||||||
|
{
|
||||||
|
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||||
|
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
||||||
|
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} timeout=0 ret=0x{returnRip:X16}");
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(
|
||||||
|
TimeSpan.FromTicks((long)timeoutUsec * 10L));
|
||||||
|
var timedWaiter = new EventFlagWaiter
|
||||||
|
{
|
||||||
|
Ctx = ctx,
|
||||||
|
State = state,
|
||||||
|
Pattern = pattern,
|
||||||
|
WaitMode = waitMode,
|
||||||
|
ResultAddress = resultAddress,
|
||||||
|
Timed = true,
|
||||||
|
TimeoutAddress = timeoutAddress,
|
||||||
|
DeadlineTimestamp = deadline,
|
||||||
|
};
|
||||||
|
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
|
ctx,
|
||||||
|
"sceKernelWaitEventFlag",
|
||||||
|
GetEventFlagWakeKey(handle),
|
||||||
|
timedWaiter,
|
||||||
|
blockDeadlineTimestamp: deadline))
|
||||||
|
{
|
||||||
|
state.WaitingThreads++;
|
||||||
|
TraceEventFlag($"wait-block-timed handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} waiters={state.WaitingThreads} ret=0x{returnRip:X16}");
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||||
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
||||||
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} ret=0x{returnRip:X16}");
|
TraceEventFlag($"wait-timeout-host handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} ret=0x{returnRip:X16}");
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
|
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||||
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
|
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
|
||||||
var managedThread = Environment.CurrentManagedThreadId;
|
var managedThread = Environment.CurrentManagedThreadId;
|
||||||
var blockedWaitResult = OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock(
|
var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
ctx,
|
ctx,
|
||||||
"sceKernelWaitEventFlag",
|
"sceKernelWaitEventFlag",
|
||||||
GetEventFlagWakeKey(handle),
|
GetEventFlagWakeKey(handle),
|
||||||
() => (int)blockedWaitResult,
|
new EventFlagWaiter
|
||||||
() =>
|
|
||||||
{
|
{
|
||||||
if (!TryPrepareBlockedWait(
|
Ctx = ctx,
|
||||||
ctx,
|
State = state,
|
||||||
state,
|
Pattern = pattern,
|
||||||
pattern,
|
WaitMode = waitMode,
|
||||||
waitMode,
|
ResultAddress = resultAddress,
|
||||||
resultAddress,
|
|
||||||
out var preparedResult))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockedWaitResult = preparedResult;
|
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
TraceEventFlag($"wait-unsatisfied handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} block={requestedBlock} ret=0x{returnRip:X16} frames={FormatFrameChain(ctx)}");
|
TraceEventFlag($"wait-unsatisfied handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} block={requestedBlock} ret=0x{returnRip:X16} frames={FormatFrameChain(ctx)}");
|
||||||
TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}");
|
TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}");
|
||||||
@@ -446,6 +509,92 @@ public static class KernelEventFlagCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryCompleteBlockedTimedWait(
|
||||||
|
CpuContext ctx,
|
||||||
|
EventFlagState state,
|
||||||
|
EventFlagWaiter waiter,
|
||||||
|
ulong pattern,
|
||||||
|
uint waitMode,
|
||||||
|
ulong resultAddress)
|
||||||
|
{
|
||||||
|
lock (state.Gate)
|
||||||
|
{
|
||||||
|
if (waiter.Result is not null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsSatisfied(state.Bits, pattern, waitMode))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
waiter.Result = TryWriteResultPattern(ctx, resultAddress, state.Bits)
|
||||||
|
? OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
if (waiter.Result == OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
{
|
||||||
|
ApplyClearMode(state, pattern, waitMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CompleteBlockedTimedWait(
|
||||||
|
CpuContext ctx,
|
||||||
|
EventFlagState state,
|
||||||
|
EventFlagWaiter waiter,
|
||||||
|
ulong pattern,
|
||||||
|
uint waitMode,
|
||||||
|
ulong resultAddress,
|
||||||
|
ulong timeoutAddress,
|
||||||
|
long deadlineTimestamp)
|
||||||
|
{
|
||||||
|
lock (state.Gate)
|
||||||
|
{
|
||||||
|
if (waiter.Result is null)
|
||||||
|
{
|
||||||
|
if (IsSatisfied(state.Bits, pattern, waitMode))
|
||||||
|
{
|
||||||
|
waiter.Result = TryWriteResultPattern(ctx, resultAddress, state.Bits)
|
||||||
|
? OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
if (waiter.Result == OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
{
|
||||||
|
ApplyClearMode(state, pattern, waitMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
waiter.Result = TryWriteResultPattern(ctx, resultAddress, state.Bits)
|
||||||
|
? OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT
|
||||||
|
: OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (waiter.Result == OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
{
|
||||||
|
var remainingTicks = deadlineTimestamp - Stopwatch.GetTimestamp();
|
||||||
|
var remainingMicros = remainingTicks <= 0
|
||||||
|
? 0u
|
||||||
|
: (uint)Math.Min(
|
||||||
|
uint.MaxValue,
|
||||||
|
remainingTicks / (double)Stopwatch.Frequency * 1_000_000d);
|
||||||
|
_ = ctx.TryWriteUInt32(timeoutAddress, remainingMicros);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)waiter.Result.Value;
|
||||||
|
}
|
||||||
|
|
||||||
private static string GetEventFlagWakeKey(ulong handle) =>
|
private static string GetEventFlagWakeKey(ulong handle) =>
|
||||||
$"event_flag:0x{handle:X16}";
|
$"event_flag:0x{handle:X16}";
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using System.Buffers;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Kernel;
|
namespace SharpEmu.Libs.Kernel;
|
||||||
@@ -17,7 +19,7 @@ public static class KernelEventQueueCompatExports
|
|||||||
|
|
||||||
private static readonly object _eventQueueGate = new();
|
private static readonly object _eventQueueGate = new();
|
||||||
private static readonly HashSet<ulong> _eventQueues = new();
|
private static readonly HashSet<ulong> _eventQueues = new();
|
||||||
private static readonly Dictionary<ulong, LinkedList<KernelQueuedEvent>> _pendingEvents = new();
|
private static readonly Dictionary<ulong, KernelEventDeque> _pendingEvents = new();
|
||||||
private static readonly Dictionary<ulong, Dictionary<(ulong Ident, short Filter), KernelEventRegistration>> _registeredEvents = new();
|
private static readonly Dictionary<ulong, Dictionary<(ulong Ident, short Filter), KernelEventRegistration>> _registeredEvents = new();
|
||||||
private static long _nextEventQueueHandle = 1;
|
private static long _nextEventQueueHandle = 1;
|
||||||
|
|
||||||
@@ -34,6 +36,76 @@ public static class KernelEventQueueCompatExports
|
|||||||
short Filter,
|
short Filter,
|
||||||
ulong UserData);
|
ulong UserData);
|
||||||
|
|
||||||
|
// Grow-only ring buffer standing in for LinkedList<KernelQueuedEvent>, which
|
||||||
|
// allocated a node per enqueue — steady churn at one enqueue per vblank/flip edge
|
||||||
|
// per registered queue. Mutated only under _eventQueueGate.
|
||||||
|
private sealed class KernelEventDeque
|
||||||
|
{
|
||||||
|
private KernelQueuedEvent[] _items = new KernelQueuedEvent[4];
|
||||||
|
private int _head;
|
||||||
|
|
||||||
|
public int Count { get; private set; }
|
||||||
|
|
||||||
|
public KernelQueuedEvent this[int index]
|
||||||
|
{
|
||||||
|
get => _items[(_head + index) % _items.Length];
|
||||||
|
set => _items[(_head + index) % _items.Length] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddLast(in KernelQueuedEvent item)
|
||||||
|
{
|
||||||
|
if (Count == _items.Length)
|
||||||
|
{
|
||||||
|
var grown = new KernelQueuedEvent[_items.Length * 2];
|
||||||
|
for (var i = 0; i < Count; i++)
|
||||||
|
{
|
||||||
|
grown[i] = this[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
_items = grown;
|
||||||
|
_head = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_items[(_head + Count) % _items.Length] = item;
|
||||||
|
Count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KernelQueuedEvent RemoveFirst()
|
||||||
|
{
|
||||||
|
var value = _items[_head];
|
||||||
|
_head = (_head + 1) % _items.Length;
|
||||||
|
Count--;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int FindIndex(ulong ident, short filter)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < Count; i++)
|
||||||
|
{
|
||||||
|
var candidate = this[i];
|
||||||
|
if (candidate.Ident == ident && candidate.Filter == filter)
|
||||||
|
{
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class EqueueWaiter : IGuestThreadBlockWaiter
|
||||||
|
{
|
||||||
|
public required CpuContext Ctx { get; init; }
|
||||||
|
public required ulong Handle { get; init; }
|
||||||
|
public required ulong EventsAddress { get; init; }
|
||||||
|
public required int EventCapacity { get; init; }
|
||||||
|
public required ulong OutCountAddress { get; init; }
|
||||||
|
|
||||||
|
public int Resume() => ResumeWaitEqueue(Ctx, Handle, EventsAddress, EventCapacity, OutCountAddress);
|
||||||
|
|
||||||
|
public bool TryWake() => HasPendingEvents(Handle);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "D0OdFMjp46I",
|
Nid = "D0OdFMjp46I",
|
||||||
ExportName = "sceKernelCreateEqueue",
|
ExportName = "sceKernelCreateEqueue",
|
||||||
@@ -51,7 +123,7 @@ public static class KernelEventQueueCompatExports
|
|||||||
lock (_eventQueueGate)
|
lock (_eventQueueGate)
|
||||||
{
|
{
|
||||||
_eventQueues.Add(handle);
|
_eventQueues.Add(handle);
|
||||||
_pendingEvents[handle] = new LinkedList<KernelQueuedEvent>();
|
_pendingEvents[handle] = new KernelEventDeque();
|
||||||
_registeredEvents[handle] = new Dictionary<(ulong Ident, short Filter), KernelEventRegistration>();
|
_registeredEvents[handle] = new Dictionary<(ulong Ident, short Filter), KernelEventRegistration>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +151,8 @@ public static class KernelEventQueueCompatExports
|
|||||||
_registeredEvents.Remove(handle);
|
_registeredEvents.Remove(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_wakeKeys.TryRemove(handle, out _);
|
||||||
|
|
||||||
TraceEventQueue(ctx, "delete", handle);
|
TraceEventQueue(ctx, "delete", handle);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -343,8 +417,14 @@ public static class KernelEventQueueCompatExports
|
|||||||
ctx,
|
ctx,
|
||||||
"sceKernelWaitEqueue",
|
"sceKernelWaitEqueue",
|
||||||
GetEventQueueWakeKey(handle),
|
GetEventQueueWakeKey(handle),
|
||||||
() => ResumeWaitEqueue(ctx, handle, eventsAddress, eventCapacity, outCountAddress),
|
new EqueueWaiter
|
||||||
() => HasPendingEvents(handle)))
|
{
|
||||||
|
Ctx = ctx,
|
||||||
|
Handle = handle,
|
||||||
|
EventsAddress = eventsAddress,
|
||||||
|
EventCapacity = eventCapacity,
|
||||||
|
OutCountAddress = outCountAddress,
|
||||||
|
}))
|
||||||
{
|
{
|
||||||
TraceEventQueue(ctx, "wait-block", handle);
|
TraceEventQueue(ctx, "wait-block", handle);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
@@ -409,7 +489,7 @@ public static class KernelEventQueueCompatExports
|
|||||||
|
|
||||||
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
||||||
{
|
{
|
||||||
queue = new LinkedList<KernelQueuedEvent>();
|
queue = new KernelEventDeque();
|
||||||
_pendingEvents[handle] = queue;
|
_pendingEvents[handle] = queue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,7 +560,7 @@ public static class KernelEventQueueCompatExports
|
|||||||
|
|
||||||
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
||||||
{
|
{
|
||||||
queue = new LinkedList<KernelQueuedEvent>();
|
queue = new KernelEventDeque();
|
||||||
_pendingEvents[handle] = queue;
|
_pendingEvents[handle] = queue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,7 +607,7 @@ public static class KernelEventQueueCompatExports
|
|||||||
|
|
||||||
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
||||||
{
|
{
|
||||||
queue = new LinkedList<KernelQueuedEvent>();
|
queue = new KernelEventDeque();
|
||||||
_pendingEvents[handle] = queue;
|
_pendingEvents[handle] = queue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,15 +643,15 @@ public static class KernelEventQueueCompatExports
|
|||||||
|
|
||||||
if (!_pendingEvents.TryGetValue(handle, out var events))
|
if (!_pendingEvents.TryGetValue(handle, out var events))
|
||||||
{
|
{
|
||||||
events = new LinkedList<KernelQueuedEvent>();
|
events = new KernelEventDeque();
|
||||||
_pendingEvents[handle] = events;
|
_pendingEvents[handle] = events;
|
||||||
}
|
}
|
||||||
|
|
||||||
var count = 1UL;
|
var count = 1UL;
|
||||||
var pendingNode = FindPendingEvent(events, ident, filter);
|
var pendingIndex = events.FindIndex(ident, filter);
|
||||||
if (pendingNode is not null)
|
if (pendingIndex >= 0)
|
||||||
{
|
{
|
||||||
count = Math.Min(((pendingNode.Value.Data >> 12) & 0xFUL) + 1, 0xFUL);
|
count = Math.Min(((events[pendingIndex].Data >> 12) & 0xFUL) + 1, 0xFUL);
|
||||||
}
|
}
|
||||||
|
|
||||||
var timeBits = unchecked((ulong)Environment.TickCount64) & 0xFFFUL;
|
var timeBits = unchecked((ulong)Environment.TickCount64) & 0xFFFUL;
|
||||||
@@ -584,9 +664,9 @@ public static class KernelEventQueueCompatExports
|
|||||||
eventData,
|
eventData,
|
||||||
userData);
|
userData);
|
||||||
|
|
||||||
if (pendingNode is not null)
|
if (pendingIndex >= 0)
|
||||||
{
|
{
|
||||||
pendingNode.Value = triggeredEvent;
|
events[pendingIndex] = triggeredEvent;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -631,40 +711,28 @@ public static class KernelEventQueueCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void QueueOrUpdateEvent(
|
private static void QueueOrUpdateEvent(
|
||||||
LinkedList<KernelQueuedEvent> queue,
|
KernelEventDeque queue,
|
||||||
KernelQueuedEvent queuedEvent)
|
KernelQueuedEvent queuedEvent)
|
||||||
{
|
{
|
||||||
var pendingNode = FindPendingEvent(queue, queuedEvent.Ident, queuedEvent.Filter);
|
var pendingIndex = queue.FindIndex(queuedEvent.Ident, queuedEvent.Filter);
|
||||||
if (pendingNode is null)
|
if (pendingIndex < 0)
|
||||||
{
|
{
|
||||||
queue.AddLast(queuedEvent);
|
queue.AddLast(queuedEvent);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pendingNode.Value = queuedEvent with
|
queue[pendingIndex] = queuedEvent with
|
||||||
{
|
{
|
||||||
Fflags = Math.Max(pendingNode.Value.Fflags + 1, queuedEvent.Fflags),
|
Fflags = Math.Max(queue[pendingIndex].Fflags + 1, queuedEvent.Fflags),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static LinkedListNode<KernelQueuedEvent>? FindPendingEvent(
|
// Wake keys are formatted once per handle: WakeEventQueue runs on every event
|
||||||
LinkedList<KernelQueuedEvent> queue,
|
// enqueue (vblank/flip edges included), so formatting there is steady string churn.
|
||||||
ulong ident,
|
private static readonly ConcurrentDictionary<ulong, string> _wakeKeys = new();
|
||||||
short filter)
|
|
||||||
{
|
|
||||||
for (var node = queue.First; node is not null; node = node.Next)
|
|
||||||
{
|
|
||||||
if (node.Value.Ident == ident && node.Value.Filter == filter)
|
|
||||||
{
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetEventQueueWakeKey(ulong handle) =>
|
private static string GetEventQueueWakeKey(ulong handle) =>
|
||||||
$"sceKernelWaitEqueue:{handle:X16}";
|
_wakeKeys.GetOrAdd(handle, static h => $"sceKernelWaitEqueue:{h:X16}");
|
||||||
|
|
||||||
private static void WakeEventQueue(ulong handle)
|
private static void WakeEventQueue(ulong handle)
|
||||||
{
|
{
|
||||||
@@ -678,7 +746,10 @@ public static class KernelEventQueueCompatExports
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Engines wait on the vblank/flip equeue every frame, so the delivery buffer
|
||||||
|
// (usually a single event) comes from the pool instead of a per-call array.
|
||||||
KernelQueuedEvent[] events;
|
KernelQueuedEvent[] events;
|
||||||
|
int count;
|
||||||
lock (_eventQueueGate)
|
lock (_eventQueueGate)
|
||||||
{
|
{
|
||||||
if (!_pendingEvents.TryGetValue(handle, out var queue) || queue.Count == 0)
|
if (!_pendingEvents.TryGetValue(handle, out var queue) || queue.Count == 0)
|
||||||
@@ -686,24 +757,30 @@ public static class KernelEventQueueCompatExports
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
var count = Math.Min(eventCapacity, queue.Count);
|
count = Math.Min(eventCapacity, queue.Count);
|
||||||
events = new KernelQueuedEvent[count];
|
events = ArrayPool<KernelQueuedEvent>.Shared.Rent(count);
|
||||||
for (var i = 0; i < count; i++)
|
for (var i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
events[i] = queue.First!.Value;
|
events[i] = queue.RemoveFirst();
|
||||||
queue.RemoveFirst();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i = 0; i < events.Length; i++)
|
try
|
||||||
{
|
{
|
||||||
if (!WriteKernelEvent(ctx, eventsAddress + ((ulong)i * KernelEventSize), events[i]))
|
for (var i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
return i;
|
if (!WriteKernelEvent(ctx, eventsAddress + ((ulong)i * KernelEventSize), events[i]))
|
||||||
|
{
|
||||||
|
return i;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<KernelQueuedEvent>.Shared.Return(events);
|
||||||
|
}
|
||||||
|
|
||||||
return events.Length;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool WriteKernelEvent(CpuContext ctx, ulong address, KernelQueuedEvent queuedEvent)
|
private static bool WriteKernelEvent(CpuContext ctx, ulong address, KernelQueuedEvent queuedEvent)
|
||||||
@@ -718,9 +795,12 @@ public static class KernelEventQueueCompatExports
|
|||||||
return ctx.Memory.TryWrite(address, eventBytes);
|
return ctx.Memory.TryWrite(address, eventBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly bool _logEqueue =
|
||||||
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EQUEUE"), "1", StringComparison.Ordinal);
|
||||||
|
|
||||||
private static void TraceEventQueue(CpuContext ctx, string operation, ulong handle)
|
private static void TraceEventQueue(CpuContext ctx, string operation, ulong handle)
|
||||||
{
|
{
|
||||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EQUEUE"), "1", StringComparison.Ordinal))
|
if (!_logEqueue)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
using SharpEmu.Libs.Ampr;
|
using SharpEmu.Libs.Ampr;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
@@ -17,6 +18,10 @@ public static class KernelMemoryCompatExports
|
|||||||
private const int MaxGuestStringLength = 4096;
|
private const int MaxGuestStringLength = 4096;
|
||||||
private const int WideCharSize = sizeof(ushort);
|
private const int WideCharSize = sizeof(ushort);
|
||||||
private const int MemsetChunkSize = 16 * 1024;
|
private const int MemsetChunkSize = 16 * 1024;
|
||||||
|
private const int MemcpyChunkSize = 256 * 1024;
|
||||||
|
|
||||||
|
// Shared all-zero scratch for chunked zero-fill loops; never written to.
|
||||||
|
private static readonly byte[] _zeroChunk = new byte[MemsetChunkSize];
|
||||||
private const int TlsModuleBlockSize = 0x10000;
|
private const int TlsModuleBlockSize = 0x10000;
|
||||||
private const int O_WRONLY = 0x1;
|
private const int O_WRONLY = 0x1;
|
||||||
private const int O_RDWR = 0x2;
|
private const int O_RDWR = 0x2;
|
||||||
@@ -52,14 +57,13 @@ public static class KernelMemoryCompatExports
|
|||||||
private const ulong FlexibleMemorySizeBytes = 448UL * 1024 * 1024;
|
private const ulong FlexibleMemorySizeBytes = 448UL * 1024 * 1024;
|
||||||
private const int OrbisVirtualQueryInfoSize = 72;
|
private const int OrbisVirtualQueryInfoSize = 72;
|
||||||
private const int OrbisKernelMaximumNameLength = 32;
|
private const int OrbisKernelMaximumNameLength = 32;
|
||||||
private const uint MemCommit = 0x1000;
|
// Raw Windows PAGE_* values used only against HostRegionInfo.RawProtection,
|
||||||
private const uint MemReserve = 0x2000;
|
// which by contract carries the untranslated protection word of the host
|
||||||
private const uint MemRelease = 0x8000;
|
// platform in use (see IHostMemory).
|
||||||
private const uint HostPageNoAccess = 0x01;
|
private const uint HostPageNoAccess = 0x01;
|
||||||
private const uint HostPageReadOnly = 0x02;
|
private const uint HostPageReadOnly = 0x02;
|
||||||
private const uint HostPageReadWrite = 0x04;
|
private const uint HostPageReadWrite = 0x04;
|
||||||
private const uint HostPageWriteCopy = 0x08;
|
private const uint HostPageWriteCopy = 0x08;
|
||||||
private const uint HostPageExecute = 0x10;
|
|
||||||
private const uint HostPageExecuteRead = 0x20;
|
private const uint HostPageExecuteRead = 0x20;
|
||||||
private const uint HostPageExecuteReadWrite = 0x40;
|
private const uint HostPageExecuteReadWrite = 0x40;
|
||||||
private const uint HostPageExecuteWriteCopy = 0x80;
|
private const uint HostPageExecuteWriteCopy = 0x80;
|
||||||
@@ -136,31 +140,9 @@ public static class KernelMemoryCompatExports
|
|||||||
private static string? _cachedApp0Root;
|
private static string? _cachedApp0Root;
|
||||||
private static string? _cachedDownload0Root;
|
private static string? _cachedDownload0Root;
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
// Property (not a cached field) so merely touching this type never resolves
|
||||||
private struct MemoryBasicInformation
|
// the platform backend; non-Windows hosts only throw if a call is reached.
|
||||||
{
|
private static IHostMemory HostMemory => HostPlatform.Current.Memory;
|
||||||
public nint BaseAddress;
|
|
||||||
public nint AllocationBase;
|
|
||||||
public uint AllocationProtect;
|
|
||||||
public nuint RegionSize;
|
|
||||||
public uint State;
|
|
||||||
public uint Protect;
|
|
||||||
public uint Type;
|
|
||||||
}
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
private static extern nuint VirtualQuery(nint lpAddress, out MemoryBasicInformation lpBuffer, nuint dwLength);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
|
||||||
private static extern bool VirtualProtect(nint lpAddress, nuint dwSize, uint flNewProtect, out uint lpflOldProtect);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
private static extern nint VirtualAlloc(nint lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
|
||||||
private static extern bool VirtualFree(nint lpAddress, nuint dwSize, uint dwFreeType);
|
|
||||||
|
|
||||||
private sealed class OpenDirectory
|
private sealed class OpenDirectory
|
||||||
{
|
{
|
||||||
@@ -200,6 +182,40 @@ public static class KernelMemoryCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool TryUnregisterGuestPathMount(string guestMountPoint)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(guestMountPoint))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedMountPoint = NormalizeGuestStatCachePath(guestMountPoint);
|
||||||
|
if (normalizedMountPoint is null || normalizedMountPoint == "/")
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var removed = false;
|
||||||
|
lock (_guestMountGate)
|
||||||
|
{
|
||||||
|
removed = _guestMounts.Remove(normalizedMountPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!removed)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_statCacheGate)
|
||||||
|
{
|
||||||
|
_negativeStatCache.RemoveWhere(path =>
|
||||||
|
string.Equals(path, normalizedMountPoint, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
path.StartsWith(normalizedMountPoint + "/", StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
internal static bool TryAllocateHleData(
|
internal static bool TryAllocateHleData(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
ulong length,
|
ulong length,
|
||||||
@@ -243,11 +259,10 @@ public static class KernelMemoryCompatExports
|
|||||||
DirectStart: 0);
|
DirectStart: 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
var zeroes = new byte[(int)Math.Min(mappedLength, (ulong)MemsetChunkSize)];
|
|
||||||
for (ulong offset = 0; offset < mappedLength;)
|
for (ulong offset = 0; offset < mappedLength;)
|
||||||
{
|
{
|
||||||
var chunkLength = (int)Math.Min((ulong)zeroes.Length, mappedLength - offset);
|
var chunkLength = (int)Math.Min((ulong)_zeroChunk.Length, mappedLength - offset);
|
||||||
if (!ctx.Memory.TryWrite(address + offset, zeroes.AsSpan(0, chunkLength)))
|
if (!ctx.Memory.TryWrite(address + offset, _zeroChunk.AsSpan(0, chunkLength)))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -410,33 +425,50 @@ public static class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
var chunk = new byte[MemsetChunkSize];
|
// Rent may hand back a larger array than requested; only the first chunkLength
|
||||||
Array.Fill(chunk, value);
|
// bytes are filled, so the loop must cap at chunkLength rather than chunk.Length.
|
||||||
var remaining = length;
|
var chunkLength = (int)Math.Min(length, (ulong)MemsetChunkSize);
|
||||||
var cursor = destination;
|
var chunk = value == 0 ? _zeroChunk : ArrayPool<byte>.Shared.Rent(chunkLength);
|
||||||
while (remaining > 0)
|
if (value != 0)
|
||||||
{
|
{
|
||||||
var take = (int)Math.Min((ulong)chunk.Length, remaining);
|
chunk.AsSpan(0, chunkLength).Fill(value);
|
||||||
if (!TryWriteCompat(ctx, cursor, chunk.AsSpan(0, take)))
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var remaining = length;
|
||||||
|
var cursor = destination;
|
||||||
|
while (remaining > 0)
|
||||||
{
|
{
|
||||||
if (length <= 0x40)
|
var take = (int)Math.Min((ulong)chunkLength, remaining);
|
||||||
|
if (!TryWriteCompat(ctx, cursor, chunk.AsSpan(0, take)))
|
||||||
{
|
{
|
||||||
var recoveryIndex = Interlocked.Increment(ref _inaccessibleMemsetRecoveryCount);
|
if (length <= 0x40)
|
||||||
if (recoveryIndex <= 8)
|
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
var recoveryIndex = Interlocked.Increment(ref _inaccessibleMemsetRecoveryCount);
|
||||||
$"[LOADER][WARNING] memset inaccessible-dst recovery#{recoveryIndex}: rip=0x{ctx.Rip:X16} dst=0x{destination:X16} len=0x{length:X} val=0x{value:X2}");
|
if (recoveryIndex <= 8)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARNING] memset inaccessible-dst recovery#{recoveryIndex}: rip=0x{ctx.Rip:X16} dst=0x{destination:X16} len=0x{length:X} val=0x{value:X2}");
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = destination;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = destination;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
cursor += (ulong)take;
|
||||||
|
remaining -= (ulong)take;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (value != 0)
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(chunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor += (ulong)take;
|
|
||||||
remaining -= (ulong)take;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = destination;
|
ctx[CpuRegister.Rax] = destination;
|
||||||
@@ -478,7 +510,7 @@ public static class KernelMemoryCompatExports
|
|||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "LHMrG7e8G78",
|
Nid = "LHMrG7e8G78",
|
||||||
ExportName = "wcslen",
|
ExportName = "wcsmisc",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libc")]
|
LibraryName = "libc")]
|
||||||
public static int Wcslen(CpuContext ctx)
|
public static int Wcslen(CpuContext ctx)
|
||||||
@@ -1173,11 +1205,10 @@ public static class KernelMemoryCompatExports
|
|||||||
var rawCount = ctx[CpuRegister.Rdx];
|
var rawCount = ctx[CpuRegister.Rdx];
|
||||||
|
|
||||||
// A garbage/absurd count (observed as e.g. 0xA7560035 from the same still-unidentified
|
// A garbage/absurd count (observed as e.g. 0xA7560035 from the same still-unidentified
|
||||||
// upstream bug that also feeds bad lengths to memset) must not reach
|
// upstream bug that also feeds bad lengths to memset) must not turn into a multi-GB
|
||||||
// GC.AllocateUninitializedArray: attempting a multi-GB allocation from a guest-thread
|
// copy attempt from a guest-thread call context, which corrupted the CLR outright
|
||||||
// call context corrupted the CLR outright ("Invalid Program: attempted to call a
|
// ("Invalid Program: attempted to call a UnmanagedCallersOnly method from managed
|
||||||
// UnmanagedCallersOnly method from managed code") instead of throwing a normal
|
// code") instead of throwing a normal exception. Reject anything above a sane bound.
|
||||||
// exception. Reject anything above a sane bound before allocating.
|
|
||||||
const ulong maxSaneCount = 512UL * 1024 * 1024;
|
const ulong maxSaneCount = 512UL * 1024 * 1024;
|
||||||
if (rawCount > maxSaneCount)
|
if (rawCount > maxSaneCount)
|
||||||
{
|
{
|
||||||
@@ -1186,15 +1217,52 @@ public static class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
var count = (int)rawCount;
|
ctx[CpuRegister.Rax] = destination;
|
||||||
var payload = GC.AllocateUninitializedArray<byte>(count);
|
if (rawCount == 0)
|
||||||
if (count > 0 && (!TryReadCompat(ctx, source, payload) || !TryWriteCompat(ctx, destination, payload)))
|
|
||||||
{
|
{
|
||||||
ctx[CpuRegister.Rax] = destination;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
}
|
||||||
|
|
||||||
|
// Cap iterations at the requested chunk size, not chunk.Length: Rent may hand
|
||||||
|
// back a larger array, and the copy granularity should not depend on pool
|
||||||
|
// bucketing internals.
|
||||||
|
var chunkLength = (int)Math.Min(rawCount, (ulong)MemcpyChunkSize);
|
||||||
|
var chunk = ArrayPool<byte>.Shared.Rent(chunkLength);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// memmove aliases this export, so overlapping ranges must survive the chunked
|
||||||
|
// copy: when the destination starts inside the source range, copy high-to-low
|
||||||
|
// so no source byte is overwritten before it has been read.
|
||||||
|
var copyBackward = destination > source && destination - source < rawCount;
|
||||||
|
var remaining = rawCount;
|
||||||
|
ulong offset = copyBackward ? rawCount : 0;
|
||||||
|
while (remaining > 0)
|
||||||
|
{
|
||||||
|
var take = (int)Math.Min((ulong)chunkLength, remaining);
|
||||||
|
if (copyBackward)
|
||||||
|
{
|
||||||
|
offset -= (ulong)take;
|
||||||
|
}
|
||||||
|
|
||||||
|
var span = chunk.AsSpan(0, take);
|
||||||
|
if (!TryReadCompat(ctx, source + offset, span) || !TryWriteCompat(ctx, destination + offset, span))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!copyBackward)
|
||||||
|
{
|
||||||
|
offset += (ulong)take;
|
||||||
|
}
|
||||||
|
|
||||||
|
remaining -= (ulong)take;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(chunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = destination;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1488,6 +1556,214 @@ public static class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "ob5xAW4ln-0",
|
||||||
|
ExportName = "strchr",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Strchr(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var address = ctx[CpuRegister.Rdi];
|
||||||
|
var needle = unchecked((byte)ctx[CpuRegister.Rsi]);
|
||||||
|
if (address == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The terminator counts as part of the scanned range, so strchr(s, '\0')
|
||||||
|
// returns a pointer to the string's null byte just like a native libc.
|
||||||
|
Span<byte> current = stackalloc byte[1];
|
||||||
|
for (ulong index = 0; index < 1_048_576; index++)
|
||||||
|
{
|
||||||
|
if (!TryReadCompat(ctx, address + index, current))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current[0] == needle)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = address + index;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current[0] == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "9yDWMxEFdJU",
|
||||||
|
ExportName = "strrchr",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Strrchr(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var address = ctx[CpuRegister.Rdi];
|
||||||
|
var needle = unchecked((byte)ctx[CpuRegister.Rsi]);
|
||||||
|
if (address == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong match = 0;
|
||||||
|
var found = false;
|
||||||
|
Span<byte> current = stackalloc byte[1];
|
||||||
|
for (ulong index = 0; index < 1_048_576; index++)
|
||||||
|
{
|
||||||
|
if (!TryReadCompat(ctx, address + index, current))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current[0] == needle)
|
||||||
|
{
|
||||||
|
match = address + index;
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current[0] == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = found ? match : 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "8u8lPzUEq+U",
|
||||||
|
ExportName = "memchr",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Memchr(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var address = ctx[CpuRegister.Rdi];
|
||||||
|
var needle = unchecked((byte)ctx[CpuRegister.Rsi]);
|
||||||
|
var count = ctx[CpuRegister.Rdx];
|
||||||
|
|
||||||
|
Span<byte> current = stackalloc byte[1];
|
||||||
|
for (ulong index = 0; index < count; index++)
|
||||||
|
{
|
||||||
|
if (!TryReadCompat(ctx, address + index, current))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current[0] == needle)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = address + index;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Ls4tzzhimqQ",
|
||||||
|
ExportName = "strcat",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Strcat(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var destination = ctx[CpuRegister.Rdi];
|
||||||
|
var source = ctx[CpuRegister.Rsi];
|
||||||
|
if (!TryReadCString(ctx, source, 1_048_576, out var sourceBytes))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadCString(ctx, destination, 1_048_576, out var destinationBytes))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overwrite the destination terminator and re-terminate after the copied bytes.
|
||||||
|
var appendAddress = destination + (ulong)destinationBytes.Length;
|
||||||
|
var payload = new byte[sourceBytes.Length + 1];
|
||||||
|
sourceBytes.CopyTo(payload.AsSpan());
|
||||||
|
if (!TryWriteCompat(ctx, appendAddress, payload))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = destination;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "kHg45qPC6f0",
|
||||||
|
ExportName = "strncat",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Strncat(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var destination = ctx[CpuRegister.Rdi];
|
||||||
|
var source = ctx[CpuRegister.Rsi];
|
||||||
|
var limit = ctx[CpuRegister.Rdx];
|
||||||
|
|
||||||
|
// Bounding the source read by the count yields strncat's "at most n bytes"
|
||||||
|
// semantics while still stopping early at the source terminator.
|
||||||
|
if (!TryReadCString(ctx, source, limit, out var sourceBytes))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadCString(ctx, destination, 1_048_576, out var destinationBytes))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var appendAddress = destination + (ulong)destinationBytes.Length;
|
||||||
|
var payload = new byte[sourceBytes.Length + 1];
|
||||||
|
sourceBytes.CopyTo(payload.AsSpan());
|
||||||
|
if (!TryWriteCompat(ctx, appendAddress, payload))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = destination;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "viiwFMaNamA",
|
||||||
|
ExportName = "strstr",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Strstr(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var haystack = ctx[CpuRegister.Rdi];
|
||||||
|
var needle = ctx[CpuRegister.Rsi];
|
||||||
|
if (!TryReadCString(ctx, haystack, 1_048_576, out var haystackBytes))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadCString(ctx, needle, 1_048_576, out var needleBytes))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An empty needle matches at the start of the haystack.
|
||||||
|
if (needleBytes.Length == 0)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = haystack;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
var matchIndex = haystackBytes.AsSpan().IndexOf(needleBytes.AsSpan());
|
||||||
|
ctx[CpuRegister.Rax] = matchIndex >= 0 ? haystack + (ulong)matchIndex : 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "QrZZdJ8XsX0",
|
Nid = "QrZZdJ8XsX0",
|
||||||
ExportName = "fputs",
|
ExportName = "fputs",
|
||||||
@@ -1614,6 +1890,45 @@ public static class KernelMemoryCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "fgIsQ10xYVA",
|
||||||
|
ExportName = "sceKernelChmod",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelChmod(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var pathAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var mode = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||||
|
if (pathAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadNullTerminatedUtf8(ctx, pathAddress, MaxGuestStringLength, out var guestPath))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hostPath = ResolveGuestPath(guestPath);
|
||||||
|
if (IsReadOnlyGuestMutationPath(guestPath))
|
||||||
|
{
|
||||||
|
LogOpenTrace($"chmod readonly path='{guestPath}' host='{hostPath}' mode=0x{mode:X}");
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(hostPath) && !Directory.Exists(hostPath))
|
||||||
|
{
|
||||||
|
AddNegativeStatCacheForGuestPath(guestPath);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
// POSIX permission bits have no host equivalent on Windows; accept the call
|
||||||
|
// so guests that chmod their freshly created files/directories can proceed.
|
||||||
|
LogOpenTrace($"chmod path='{guestPath}' host='{hostPath}' mode=0x{mode:X}");
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "NNtFaKJbPt0",
|
Nid = "NNtFaKJbPt0",
|
||||||
ExportName = "_close",
|
ExportName = "_close",
|
||||||
@@ -3037,7 +3352,7 @@ public static class KernelMemoryCompatExports
|
|||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "4h6F1LLbTiw",
|
Nid = "4h6F1LLbTiw",
|
||||||
ExportName = "sceKernelMapFlexibleMemoryInternal",
|
ExportName = "sceKernelMapNamedFlexibleMemoryInternal",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int KernelMapFlexibleMemoryInternal(CpuContext ctx)
|
public static int KernelMapFlexibleMemoryInternal(CpuContext ctx)
|
||||||
@@ -3321,7 +3636,7 @@ public static class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryProtectHostRange(alignedAddress, alignedLength, protection))
|
if (!TryProtectHostRange(ctx, alignedAddress, alignedLength, protection))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
@@ -3355,7 +3670,7 @@ public static class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryProtectHostRange(alignedAddress, alignedLength, protection))
|
if (!TryProtectHostRange(ctx, alignedAddress, alignedLength, protection))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
@@ -3989,7 +4304,7 @@ public static class KernelMemoryCompatExports
|
|||||||
_ => unchecked((int)argumentSource.NextGpArg())
|
_ => unchecked((int)argumentSource.NextGpArg())
|
||||||
};
|
};
|
||||||
|
|
||||||
var formatted = value.ToString();
|
var formatted = value.ToString(CultureInfo.InvariantCulture);
|
||||||
if (showSign && value >= 0)
|
if (showSign && value >= 0)
|
||||||
formatted = "+" + formatted;
|
formatted = "+" + formatted;
|
||||||
else if (spaceForSign && value >= 0)
|
else if (spaceForSign && value >= 0)
|
||||||
@@ -4013,7 +4328,7 @@ public static class KernelMemoryCompatExports
|
|||||||
_ => (uint)argumentSource.NextGpArg()
|
_ => (uint)argumentSource.NextGpArg()
|
||||||
};
|
};
|
||||||
|
|
||||||
var formatted = value.ToString();
|
var formatted = value.ToString(CultureInfo.InvariantCulture);
|
||||||
sb.Append(PadString(formatted, width, leftAlign, padWithZero && !leftAlign));
|
sb.Append(PadString(formatted, width, leftAlign, padWithZero && !leftAlign));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -4034,8 +4349,8 @@ public static class KernelMemoryCompatExports
|
|||||||
};
|
};
|
||||||
|
|
||||||
var formatted = specifier == 'x'
|
var formatted = specifier == 'x'
|
||||||
? value.ToString("x")
|
? value.ToString("x", CultureInfo.InvariantCulture)
|
||||||
: value.ToString("X");
|
: value.ToString("X", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
if (alternateForm && value != 0)
|
if (alternateForm && value != 0)
|
||||||
formatted = specifier == 'x' ? "0x" + formatted : "0X" + formatted;
|
formatted = specifier == 'x' ? "0x" + formatted : "0X" + formatted;
|
||||||
@@ -4143,7 +4458,10 @@ public static class KernelMemoryCompatExports
|
|||||||
var formatStr = precision >= 0
|
var formatStr = precision >= 0
|
||||||
? $"{{0:{specifier}{precision}}}"
|
? $"{{0:{specifier}{precision}}}"
|
||||||
: $"{{0:{specifier}}}";
|
: $"{{0:{specifier}}}";
|
||||||
var formatted = string.Format(formatStr, value);
|
var formatted = string.Format(
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
formatStr,
|
||||||
|
value);
|
||||||
|
|
||||||
if (showSign && value >= 0)
|
if (showSign && value >= 0)
|
||||||
formatted = "+" + formatted;
|
formatted = "+" + formatted;
|
||||||
@@ -5552,42 +5870,40 @@ public static class KernelMemoryCompatExports
|
|||||||
return alignedLength != 0;
|
return alignedLength != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryProtectHostRange(ulong address, ulong length, int orbisProtection)
|
private static bool TryProtectHostRange(CpuContext ctx, ulong address, ulong length, int orbisProtection)
|
||||||
{
|
{
|
||||||
if (length == 0 || length > nuint.MaxValue)
|
if (length == 0 || length > nuint.MaxValue)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var hostProtection = ResolveHostProtection(orbisProtection);
|
if (!KernelVirtualRangeAllocator.TryResolveAddressSpace(ctx.Memory, out var addressSpace))
|
||||||
if (!VirtualProtect((nint)address, (nuint)length, hostProtection, out _))
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return addressSpace.TryProtect(address, length, ResolveGuestProtection(orbisProtection));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static uint ResolveHostProtection(int orbisProtection)
|
private static GuestPageProtection ResolveGuestProtection(int orbisProtection)
|
||||||
{
|
{
|
||||||
var read = (orbisProtection & (OrbisProtCpuRead | OrbisProtGpuRead)) != 0;
|
var protection = GuestPageProtection.None;
|
||||||
var write = (orbisProtection & (OrbisProtCpuWrite | OrbisProtGpuWrite)) != 0;
|
if ((orbisProtection & (OrbisProtCpuRead | OrbisProtGpuRead)) != 0)
|
||||||
var execute = (orbisProtection & OrbisProtCpuExec) != 0;
|
|
||||||
|
|
||||||
if (execute)
|
|
||||||
{
|
{
|
||||||
return write
|
protection |= GuestPageProtection.Read;
|
||||||
? HostPageExecuteReadWrite
|
|
||||||
: read
|
|
||||||
? HostPageExecuteRead
|
|
||||||
: HostPageExecute;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return write
|
if ((orbisProtection & (OrbisProtCpuWrite | OrbisProtGpuWrite)) != 0)
|
||||||
? HostPageReadWrite
|
{
|
||||||
: read
|
protection |= GuestPageProtection.Write;
|
||||||
? HostPageReadOnly
|
}
|
||||||
: HostPageNoAccess;
|
|
||||||
|
if ((orbisProtection & OrbisProtCpuExec) != 0)
|
||||||
|
{
|
||||||
|
protection |= GuestPageProtection.Execute;
|
||||||
|
}
|
||||||
|
|
||||||
|
return protection;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryFindVirtualQueryRegionLocked(ulong queryAddress, bool findNext, out MappedRegion region)
|
private static bool TryFindVirtualQueryRegionLocked(ulong queryAddress, bool findNext, out MappedRegion region)
|
||||||
@@ -5973,7 +6289,7 @@ public static class KernelMemoryCompatExports
|
|||||||
var effectiveAlignment = Math.Max(alignment, pageSize);
|
var effectiveAlignment = Math.Max(alignment, pageSize);
|
||||||
var usableSize = checked((nuint)AlignUp((ulong)actualSize, (ulong)pageSize));
|
var usableSize = checked((nuint)AlignUp((ulong)actualSize, (ulong)pageSize));
|
||||||
var reservationSize = checked(pageSize + effectiveAlignment - 1 + usableSize + pageSize);
|
var reservationSize = checked(pageSize + effectiveAlignment - 1 + usableSize + pageSize);
|
||||||
var baseAddress = VirtualAlloc(0, reservationSize, MemCommit | MemReserve, HostPageReadWrite);
|
var baseAddress = unchecked((nint)HostMemory.Allocate(0, reservationSize, HostPageProtection.ReadWrite));
|
||||||
if (baseAddress == 0)
|
if (baseAddress == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -5981,9 +6297,9 @@ public static class KernelMemoryCompatExports
|
|||||||
|
|
||||||
var alignedAddress = AlignUp(unchecked((ulong)baseAddress) + (ulong)pageSize, (ulong)effectiveAlignment);
|
var alignedAddress = AlignUp(unchecked((ulong)baseAddress) + (ulong)pageSize, (ulong)effectiveAlignment);
|
||||||
var guardAddress = alignedAddress + (ulong)usableSize;
|
var guardAddress = alignedAddress + (ulong)usableSize;
|
||||||
if (!VirtualProtect((nint)guardAddress, pageSize, HostPageNoAccess, out _))
|
if (!HostMemory.Protect(guardAddress, pageSize, HostPageProtection.NoAccess, out _))
|
||||||
{
|
{
|
||||||
_ = VirtualFree(baseAddress, 0, MemRelease);
|
_ = HostMemory.Free(unchecked((ulong)baseAddress));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6118,7 +6434,7 @@ public static class KernelMemoryCompatExports
|
|||||||
|
|
||||||
if (allocation.IsGuarded)
|
if (allocation.IsGuarded)
|
||||||
{
|
{
|
||||||
_ = VirtualFree(allocation.BaseAddress, 0, MemRelease);
|
_ = HostMemory.Free(unchecked((ulong)allocation.BaseAddress));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -6214,7 +6530,7 @@ public static class KernelMemoryCompatExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryQueryHostPage(address, out var startInfo) || !HasRequiredProtection(startInfo.Protect, writeAccess))
|
if (!TryQueryHostPage(address, out var startInfo) || !HasRequiredProtection(startInfo.RawProtection, writeAccess))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -6225,7 +6541,7 @@ public static class KernelMemoryCompatExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryQueryHostPage(endAddress, out var endInfo) || !HasRequiredProtection(endInfo.Protect, writeAccess))
|
if (!TryQueryHostPage(endAddress, out var endInfo) || !HasRequiredProtection(endInfo.RawProtection, writeAccess))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -6233,16 +6549,14 @@ public static class KernelMemoryCompatExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryQueryHostPage(ulong address, out MemoryBasicInformation info)
|
private static bool TryQueryHostPage(ulong address, out HostRegionInfo info)
|
||||||
{
|
{
|
||||||
info = default;
|
if (!HostMemory.Query(address, out info))
|
||||||
var size = (nuint)Marshal.SizeOf<MemoryBasicInformation>();
|
|
||||||
if (VirtualQuery((nint)address, out info, size) == 0)
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return info.State == MemCommit;
|
return info.State == HostRegionState.Committed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool HasRequiredProtection(uint protect, bool writeAccess)
|
private static bool HasRequiredProtection(uint protect, bool writeAccess)
|
||||||
@@ -6736,15 +7050,4 @@ public static class KernelMemoryCompatExports
|
|||||||
sum = left + right;
|
sum = left + right;
|
||||||
return sum >= left;
|
return sum >= left;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "KMcEa+rHsIo",
|
|
||||||
ExportName = "sceKernelMapMemory",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int KernelMapMemory(CpuContext ctx)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,10 +61,34 @@ public static class KernelPthreadCompatExports
|
|||||||
public string WakeKey { get; } = "pthread_mutex#" + Interlocked.Increment(ref _nextMutexWakeId).ToString("X");
|
public string WakeKey { get; } = "pthread_mutex#" + Interlocked.Increment(ref _nextMutexWakeId).ToString("X");
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class PthreadMutexWaiter
|
private sealed class PthreadMutexWaiter : IGuestThreadBlockWaiter
|
||||||
{
|
{
|
||||||
public required ulong ThreadId { get; init; }
|
public required ulong ThreadId { get; init; }
|
||||||
|
public required CpuContext Ctx { get; init; }
|
||||||
|
public required ulong MutexAddress { get; init; }
|
||||||
|
public required ulong ResolvedAddress { get; init; }
|
||||||
|
public required PthreadMutexState State { get; init; }
|
||||||
public int Reserved;
|
public int Reserved;
|
||||||
|
|
||||||
|
public int Resume() => CompleteBlockedMutexLock(Ctx, MutexAddress, ResolvedAddress, State, this);
|
||||||
|
|
||||||
|
public bool TryWake() => TryReserveBlockedMutexLock(Ctx, MutexAddress, ResolvedAddress, State, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class PthreadCondWaiter : IGuestThreadBlockWaiter
|
||||||
|
{
|
||||||
|
public required CpuContext Ctx { get; init; }
|
||||||
|
public required ulong CondAddress { get; init; }
|
||||||
|
public required ulong MutexAddress { get; init; }
|
||||||
|
public required PthreadCondState State { get; init; }
|
||||||
|
public required ulong ObservedEpoch { get; init; }
|
||||||
|
public required bool Timed { get; init; }
|
||||||
|
public required int ReleasedRecursion { get; init; }
|
||||||
|
public required bool PosixResult { get; init; }
|
||||||
|
|
||||||
|
public int Resume() => ResumePthreadCondWait(Ctx, CondAddress, MutexAddress, State, ObservedEpoch, Timed, ReleasedRecursion, PosixResult);
|
||||||
|
|
||||||
|
public bool TryWake() => State.SignalEpoch != ObservedEpoch;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class PthreadCondState
|
private sealed class PthreadCondState
|
||||||
@@ -629,6 +653,7 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
if (!InitializeMutexObject(ctx, handle, state))
|
if (!InitializeMutexObject(ctx, handle, state))
|
||||||
{
|
{
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
state.Semaphore.Dispose();
|
state.Semaphore.Dispose();
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -641,6 +666,7 @@ public static class KernelPthreadCompatExports
|
|||||||
_mutexStates.TryRemove(mutexAddress, out _);
|
_mutexStates.TryRemove(mutexAddress, out _);
|
||||||
_mutexStates.TryRemove(handle, out _);
|
_mutexStates.TryRemove(handle, out _);
|
||||||
|
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
state.Semaphore.Dispose();
|
state.Semaphore.Dispose();
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -668,6 +694,7 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, mutexAddress, 0);
|
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, mutexAddress, 0);
|
||||||
|
TryFreeOpaqueObject(ctx, resolvedAddress);
|
||||||
state.Semaphore.Dispose();
|
state.Semaphore.Dispose();
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -724,7 +751,6 @@ public static class KernelPthreadCompatExports
|
|||||||
if (!acquired)
|
if (!acquired)
|
||||||
{
|
{
|
||||||
TraceContendedMutex(ctx, mutexAddress, resolvedAddress, state, currentThreadId);
|
TraceContendedMutex(ctx, mutexAddress, resolvedAddress, state, currentThreadId);
|
||||||
var waiter = new PthreadMutexWaiter { ThreadId = currentThreadId };
|
|
||||||
// Fibers retain the synchronous fallback to preserve switch state.
|
// Fibers retain the synchronous fallback to preserve switch state.
|
||||||
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
|
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
|
||||||
var canCooperativelyBlock = _enableMutexLockBlocking || currentFiber == 0;
|
var canCooperativelyBlock = _enableMutexLockBlocking || currentFiber == 0;
|
||||||
@@ -736,8 +762,14 @@ public static class KernelPthreadCompatExports
|
|||||||
ctx,
|
ctx,
|
||||||
"pthread_mutex_lock",
|
"pthread_mutex_lock",
|
||||||
state.WakeKey,
|
state.WakeKey,
|
||||||
() => CompleteBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter),
|
new PthreadMutexWaiter
|
||||||
() => TryReserveBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter)))
|
{
|
||||||
|
ThreadId = currentThreadId,
|
||||||
|
Ctx = ctx,
|
||||||
|
MutexAddress = mutexAddress,
|
||||||
|
ResolvedAddress = resolvedAddress,
|
||||||
|
State = state,
|
||||||
|
}))
|
||||||
{
|
{
|
||||||
TracePthreadMutex(ctx, "lock-block", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
TracePthreadMutex(ctx, "lock-block", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
@@ -895,6 +927,7 @@ public static class KernelPthreadCompatExports
|
|||||||
var initialState = new PthreadMutexAttrState(MutexTypeErrorCheck, 0);
|
var initialState = new PthreadMutexAttrState(MutexTypeErrorCheck, 0);
|
||||||
if (!WriteMutexAttrObject(ctx, handle, initialState))
|
if (!WriteMutexAttrObject(ctx, handle, initialState))
|
||||||
{
|
{
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -912,6 +945,7 @@ public static class KernelPthreadCompatExports
|
|||||||
_mutexAttrStates.Remove(handle);
|
_mutexAttrStates.Remove(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -935,6 +969,7 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TryFreeOpaqueObject(ctx, resolvedAddress);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1195,6 +1230,7 @@ public static class KernelPthreadCompatExports
|
|||||||
{
|
{
|
||||||
if (_condStates.TryGetValue(condAddress, out var raced))
|
if (_condStates.TryGetValue(condAddress, out var raced))
|
||||||
{
|
{
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
resolvedAddress = condAddress;
|
resolvedAddress = condAddress;
|
||||||
state = raced;
|
state = raced;
|
||||||
return true;
|
return true;
|
||||||
@@ -1212,6 +1248,7 @@ public static class KernelPthreadCompatExports
|
|||||||
_condStates.Remove(handle);
|
_condStates.Remove(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1231,7 +1268,22 @@ public static class KernelPthreadCompatExports
|
|||||||
|
|
||||||
Span<byte> initialData = stackalloc byte[size];
|
Span<byte> initialData = stackalloc byte[size];
|
||||||
initialData.Clear();
|
initialData.Clear();
|
||||||
return ctx.Memory.TryWrite(address, initialData);
|
if (ctx.Memory.TryWrite(address, initialData))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
allocator.TryFreeGuestMemory(address);
|
||||||
|
address = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryFreeOpaqueObject(CpuContext ctx, ulong address)
|
||||||
|
{
|
||||||
|
if (ctx.Memory is IGuestMemoryAllocator allocator)
|
||||||
|
{
|
||||||
|
allocator.TryFreeGuestMemory(address);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool InitializeMutexObject(CpuContext ctx, ulong address, PthreadMutexState state) =>
|
private static bool InitializeMutexObject(CpuContext ctx, ulong address, PthreadMutexState state) =>
|
||||||
@@ -1269,6 +1321,7 @@ public static class KernelPthreadCompatExports
|
|||||||
_condStates.Remove(handle);
|
_condStates.Remove(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1293,6 +1346,7 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, condAddress, 0);
|
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, condAddress, 0);
|
||||||
|
TryFreeOpaqueObject(ctx, resolvedAddress);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1363,8 +1417,17 @@ public static class KernelPthreadCompatExports
|
|||||||
ctx,
|
ctx,
|
||||||
timed ? "pthread_cond_timedwait" : "pthread_cond_wait",
|
timed ? "pthread_cond_timedwait" : "pthread_cond_wait",
|
||||||
state.WakeKey,
|
state.WakeKey,
|
||||||
() => ResumePthreadCondWait(ctx, condAddress, mutexAddress, state, observedEpoch, timed, releasedRecursion, posixResult),
|
new PthreadCondWaiter
|
||||||
() => state.SignalEpoch != observedEpoch,
|
{
|
||||||
|
Ctx = ctx,
|
||||||
|
CondAddress = condAddress,
|
||||||
|
MutexAddress = mutexAddress,
|
||||||
|
State = state,
|
||||||
|
ObservedEpoch = observedEpoch,
|
||||||
|
Timed = timed,
|
||||||
|
ReleasedRecursion = releasedRecursion,
|
||||||
|
PosixResult = posixResult,
|
||||||
|
},
|
||||||
timed ? GuestThreadExecution.ComputeDeadlineTimestamp(GetCondWaitTimeout(timeoutUsec)) : 0))
|
timed ? GuestThreadExecution.ComputeDeadlineTimestamp(GetCondWaitTimeout(timeoutUsec)) : 0))
|
||||||
{
|
{
|
||||||
TracePthreadCond(timed ? "wait-block-timed" : "wait-block", condAddress, mutexAddress, state, timed, waitResult);
|
TracePthreadCond(timed ? "wait-block-timed" : "wait-block", condAddress, mutexAddress, state, timed, waitResult);
|
||||||
@@ -1775,6 +1838,7 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
if (!InitializeMutexObject(ctx, handle, createdState))
|
if (!InitializeMutexObject(ctx, handle, createdState))
|
||||||
{
|
{
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
resolvedAddress = 0;
|
resolvedAddress = 0;
|
||||||
state = null;
|
state = null;
|
||||||
return false;
|
return false;
|
||||||
@@ -1784,12 +1848,14 @@ public static class KernelPthreadCompatExports
|
|||||||
{
|
{
|
||||||
if (_mutexStates.TryGetValue(mutexAddress, out state))
|
if (_mutexStates.TryGetValue(mutexAddress, out state))
|
||||||
{
|
{
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
resolvedAddress = mutexAddress;
|
resolvedAddress = mutexAddress;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_mutexStates.TryGetValue(handle, out state))
|
if (_mutexStates.TryGetValue(handle, out state))
|
||||||
{
|
{
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
resolvedAddress = handle;
|
resolvedAddress = handle;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1803,6 +1869,7 @@ public static class KernelPthreadCompatExports
|
|||||||
_mutexStates.TryRemove(mutexAddress, out _);
|
_mutexStates.TryRemove(mutexAddress, out _);
|
||||||
_mutexStates.TryRemove(handle, out _);
|
_mutexStates.TryRemove(handle, out _);
|
||||||
|
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
resolvedAddress = 0;
|
resolvedAddress = 0;
|
||||||
state = null;
|
state = null;
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -164,6 +164,17 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class RwlockWaiter : IGuestThreadBlockWaiter
|
||||||
|
{
|
||||||
|
public required PthreadRwlockState Rwlock { get; init; }
|
||||||
|
public required ulong ThreadId { get; init; }
|
||||||
|
public required bool Write { get; init; }
|
||||||
|
|
||||||
|
public int Resume() => (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
|
||||||
|
public bool TryWake() => TryAcquireBlockedRwlock(Rwlock, ThreadId, Write);
|
||||||
|
}
|
||||||
|
|
||||||
private readonly record struct TlsKeyState(ulong Destructor);
|
private readonly record struct TlsKeyState(ulong Destructor);
|
||||||
|
|
||||||
private readonly record struct PthreadAttrState(
|
private readonly record struct PthreadAttrState(
|
||||||
@@ -382,6 +393,47 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "oIRFTjoILbg",
|
||||||
|
ExportName = "scePthreadSetschedparam",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadSetschedparam(CpuContext ctx) => PosixPthreadSetschedparam(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "P41kTWUS3EI",
|
||||||
|
ExportName = "scePthreadGetschedparam",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadGetschedparam(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thread = ctx[CpuRegister.Rdi];
|
||||||
|
var outPolicyAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var outSchedParamAddress = ctx[CpuRegister.Rdx];
|
||||||
|
if (thread == 0 || outPolicyAddress == 0 || outSchedParamAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
int policy;
|
||||||
|
int priority;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
var state = GetOrCreateThreadStateLocked(thread);
|
||||||
|
policy = state.Attributes.SchedPolicy;
|
||||||
|
priority = state.Priority;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryWriteInt32(outPolicyAddress, policy) ||
|
||||||
|
!ctx.TryWriteInt32(outSchedParamAddress, priority))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "nsYoNRywwNg",
|
Nid = "nsYoNRywwNg",
|
||||||
ExportName = "scePthreadAttrInit",
|
ExportName = "scePthreadAttrInit",
|
||||||
@@ -1318,8 +1370,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
ctx,
|
ctx,
|
||||||
"pthread_rwlock_wrlock",
|
"pthread_rwlock_wrlock",
|
||||||
rwlock.WakeKey,
|
rwlock.WakeKey,
|
||||||
static () => (int)OrbisGen2Result.ORBIS_GEN2_OK,
|
new RwlockWaiter { Rwlock = rwlock, ThreadId = currentThreadId, Write = true }))
|
||||||
() => TryAcquireBlockedRwlock(rwlock, currentThreadId, write: true)))
|
|
||||||
{
|
{
|
||||||
transferredToScheduler = true;
|
transferredToScheduler = true;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
@@ -1355,8 +1406,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
ctx,
|
ctx,
|
||||||
"pthread_rwlock_rdlock",
|
"pthread_rwlock_rdlock",
|
||||||
rwlock.WakeKey,
|
rwlock.WakeKey,
|
||||||
static () => (int)OrbisGen2Result.ORBIS_GEN2_OK,
|
new RwlockWaiter { Rwlock = rwlock, ThreadId = currentThreadId, Write = false }))
|
||||||
() => TryAcquireBlockedRwlock(rwlock, currentThreadId, write: false)))
|
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
using SharpEmu.Libs.Fiber;
|
using SharpEmu.Libs.Fiber;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
@@ -48,9 +49,6 @@ public static class KernelRuntimeCompatExports
|
|||||||
private const int MapFlagFixed = 0x10;
|
private const int MapFlagFixed = 0x10;
|
||||||
private const ulong DefaultVirtualRangeAlignment = 0x4000UL;
|
private const ulong DefaultVirtualRangeAlignment = 0x4000UL;
|
||||||
private const int AioInitParamSize = 0x3C;
|
private const int AioInitParamSize = 0x3C;
|
||||||
private const uint MemCommit = 0x1000;
|
|
||||||
private const uint MemReserve = 0x2000;
|
|
||||||
private const uint PageExecuteReadWrite = 0x40;
|
|
||||||
private static readonly object _stateGate = new();
|
private static readonly object _stateGate = new();
|
||||||
private static readonly long _processStartCounter = Stopwatch.GetTimestamp();
|
private static readonly long _processStartCounter = Stopwatch.GetTimestamp();
|
||||||
private static readonly RdtscDelegate? _rdtscReader = CreateRdtscReader();
|
private static readonly RdtscDelegate? _rdtscReader = CreateRdtscReader();
|
||||||
@@ -1893,7 +1891,7 @@ public static class KernelRuntimeCompatExports
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
nint stubAddress = VirtualAlloc(nint.Zero, (nuint)16, MemCommit | MemReserve, PageExecuteReadWrite);
|
nint stubAddress = unchecked((nint)HostPlatform.Current.Memory.Allocate(0, 16, HostPageProtection.ReadWriteExecute));
|
||||||
if (stubAddress == 0)
|
if (stubAddress == 0)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
@@ -1923,9 +1921,6 @@ public static class KernelRuntimeCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
private static extern nint VirtualAlloc(nint lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
|
||||||
|
|
||||||
private static bool TryReserveVirtualRange(
|
private static bool TryReserveVirtualRange(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
ulong desiredAddress,
|
ulong desiredAddress,
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ public static class KernelSemaphoreCompatExports
|
|||||||
private sealed class KernelSemaphoreState
|
private sealed class KernelSemaphoreState
|
||||||
{
|
{
|
||||||
public required string Name { get; init; }
|
public required string Name { get; init; }
|
||||||
|
// Formatted once at creation; signal/wait/cancel/delete all wake through this key.
|
||||||
|
public required string WakeKey { get; init; }
|
||||||
public required int InitialCount { get; init; }
|
public required int InitialCount { get; init; }
|
||||||
public required int MaxCount { get; init; }
|
public required int MaxCount { get; init; }
|
||||||
public int Count { get; set; }
|
public int Count { get; set; }
|
||||||
@@ -28,14 +30,26 @@ public static class KernelSemaphoreCompatExports
|
|||||||
public object Gate { get; } = new();
|
public object Gate { get; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class SemaphoreWaiter
|
private sealed class SemaphoreWaiter : IGuestThreadBlockWaiter
|
||||||
{
|
{
|
||||||
|
public required KernelSemaphoreState Semaphore { get; init; }
|
||||||
public required int NeedCount { get; init; }
|
public required int NeedCount { get; init; }
|
||||||
public required int CancelEpochAtBlock { get; init; }
|
public required int CancelEpochAtBlock { get; init; }
|
||||||
public bool Timed { get; init; }
|
public bool Timed { get; init; }
|
||||||
|
|
||||||
|
// Timed-wait completion state; unused when Timed is false.
|
||||||
|
public CpuContext? Ctx { get; init; }
|
||||||
|
public ulong TimeoutAddress { get; init; }
|
||||||
|
public long DeadlineTimestamp { get; init; }
|
||||||
|
|
||||||
// Written and read only under the owning semaphore's Gate.
|
// Written and read only under the owning semaphore's Gate.
|
||||||
public int? Result { get; set; }
|
public int? Result { get; set; }
|
||||||
|
|
||||||
|
public int Resume() => Timed
|
||||||
|
? CompleteBlockedTimedSemaWait(Ctx!, Semaphore, this, TimeoutAddress, DeadlineTimestamp)
|
||||||
|
: CompleteBlockedSemaWait(Semaphore, this);
|
||||||
|
|
||||||
|
public bool TryWake() => TryConsumeBlockedSemaWait(Semaphore, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetSemaphoreWakeKey(uint handle) => $"kernel_sema:0x{handle:X8}";
|
private static string GetSemaphoreWakeKey(uint handle) => $"kernel_sema:0x{handle:X8}";
|
||||||
@@ -79,6 +93,7 @@ public static class KernelSemaphoreCompatExports
|
|||||||
var state = new KernelSemaphoreState
|
var state = new KernelSemaphoreState
|
||||||
{
|
{
|
||||||
Name = name,
|
Name = name,
|
||||||
|
WakeKey = GetSemaphoreWakeKey(handle),
|
||||||
InitialCount = initialCount,
|
InitialCount = initialCount,
|
||||||
MaxCount = maxCount,
|
MaxCount = maxCount,
|
||||||
Count = initialCount,
|
Count = initialCount,
|
||||||
@@ -96,11 +111,14 @@ public static class KernelSemaphoreCompatExports
|
|||||||
state.Deleted = true;
|
state.Deleted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(state.WakeKey);
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceSemaphore($"create handle=0x{handle:X8} name='{name}' attr=0x{attr:X} init={initialCount} max={maxCount}");
|
if (_traceSema)
|
||||||
|
{
|
||||||
|
TraceSemaphore($"create handle=0x{handle:X8} name='{name}' attr=0x{attr:X} init={initialCount} max={maxCount}");
|
||||||
|
}
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +149,10 @@ public static class KernelSemaphoreCompatExports
|
|||||||
if (semaphore.Count >= needCount)
|
if (semaphore.Count >= needCount)
|
||||||
{
|
{
|
||||||
semaphore.Count -= needCount;
|
semaphore.Count -= needCount;
|
||||||
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
if (_traceSema)
|
||||||
|
{
|
||||||
|
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||||
|
}
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +166,10 @@ public static class KernelSemaphoreCompatExports
|
|||||||
if (timeoutMicros == 0)
|
if (timeoutMicros == 0)
|
||||||
{
|
{
|
||||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||||
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
if (_traceSema)
|
||||||
|
{
|
||||||
|
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||||
|
}
|
||||||
pollTimedOut = true;
|
pollTimedOut = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -153,27 +177,36 @@ public static class KernelSemaphoreCompatExports
|
|||||||
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromTicks((long)timeoutMicros * 10L));
|
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromTicks((long)timeoutMicros * 10L));
|
||||||
var timedWaiter = new SemaphoreWaiter
|
var timedWaiter = new SemaphoreWaiter
|
||||||
{
|
{
|
||||||
|
Semaphore = semaphore,
|
||||||
NeedCount = needCount,
|
NeedCount = needCount,
|
||||||
CancelEpochAtBlock = semaphore.CancelEpoch,
|
CancelEpochAtBlock = semaphore.CancelEpoch,
|
||||||
Timed = true,
|
Timed = true,
|
||||||
|
Ctx = ctx,
|
||||||
|
TimeoutAddress = timeoutAddress,
|
||||||
|
DeadlineTimestamp = deadline,
|
||||||
};
|
};
|
||||||
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
ctx,
|
ctx,
|
||||||
"sceKernelWaitSema",
|
"sceKernelWaitSema",
|
||||||
GetSemaphoreWakeKey(handle),
|
semaphore.WakeKey,
|
||||||
resumeHandler: () => CompleteBlockedTimedSemaWait(ctx, semaphore, timedWaiter, timeoutAddress, deadline),
|
timedWaiter,
|
||||||
wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, timedWaiter),
|
|
||||||
blockDeadlineTimestamp: deadline))
|
blockDeadlineTimestamp: deadline))
|
||||||
{
|
{
|
||||||
semaphore.WaitingThreads++;
|
semaphore.WaitingThreads++;
|
||||||
TraceSemaphore($"wait-block-timed handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout_us={timeoutMicros} waiters={semaphore.WaitingThreads}");
|
if (_traceSema)
|
||||||
|
{
|
||||||
|
TraceSemaphore($"wait-block-timed handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout_us={timeoutMicros} waiters={semaphore.WaitingThreads}");
|
||||||
|
}
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Host-owned threads cannot park in the guest scheduler; degrade to the
|
// Host-owned threads cannot park in the guest scheduler; degrade to the
|
||||||
// immediate-timeout poll the callers already tolerate.
|
// immediate-timeout poll the callers already tolerate.
|
||||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||||
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
if (_traceSema)
|
||||||
|
{
|
||||||
|
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||||
|
}
|
||||||
pollTimedOut = true;
|
pollTimedOut = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,22 +215,28 @@ public static class KernelSemaphoreCompatExports
|
|||||||
{
|
{
|
||||||
var waiter = new SemaphoreWaiter
|
var waiter = new SemaphoreWaiter
|
||||||
{
|
{
|
||||||
|
Semaphore = semaphore,
|
||||||
NeedCount = needCount,
|
NeedCount = needCount,
|
||||||
CancelEpochAtBlock = semaphore.CancelEpoch,
|
CancelEpochAtBlock = semaphore.CancelEpoch,
|
||||||
};
|
};
|
||||||
if (!GuestThreadExecution.RequestCurrentThreadBlock(
|
if (!GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
ctx,
|
ctx,
|
||||||
"sceKernelWaitSema",
|
"sceKernelWaitSema",
|
||||||
GetSemaphoreWakeKey(handle),
|
semaphore.WakeKey,
|
||||||
resumeHandler: () => CompleteBlockedSemaWait(semaphore, waiter),
|
waiter))
|
||||||
wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, waiter)))
|
|
||||||
{
|
{
|
||||||
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
if (_traceSema)
|
||||||
|
{
|
||||||
|
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||||
|
}
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
semaphore.WaitingThreads++;
|
semaphore.WaitingThreads++;
|
||||||
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
if (_traceSema)
|
||||||
|
{
|
||||||
|
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||||
|
}
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -239,12 +278,18 @@ public static class KernelSemaphoreCompatExports
|
|||||||
{
|
{
|
||||||
if (semaphore.Count < needCount)
|
if (semaphore.Count < needCount)
|
||||||
{
|
{
|
||||||
TraceSemaphore($"poll-busy handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
if (_traceSema)
|
||||||
|
{
|
||||||
|
TraceSemaphore($"poll-busy handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||||
|
}
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||||
}
|
}
|
||||||
|
|
||||||
semaphore.Count -= needCount;
|
semaphore.Count -= needCount;
|
||||||
TraceSemaphore($"poll handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
if (_traceSema)
|
||||||
|
{
|
||||||
|
TraceSemaphore($"poll handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||||
|
}
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -277,14 +322,17 @@ public static class KernelSemaphoreCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
semaphore.Count += signalCount;
|
semaphore.Count += signalCount;
|
||||||
TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
if (_traceSema)
|
||||||
|
{
|
||||||
|
TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wake after releasing the gate (lock order: scheduler gate -> semaphore gate).
|
// Wake after releasing the gate (lock order: scheduler gate -> semaphore gate).
|
||||||
// Wake everyone; the wake handler consumes the count per waiter, so a waiter
|
// Wake everyone; the wake handler consumes the count per waiter, so a waiter
|
||||||
// whose needCount exceeds the remaining count stays parked while a smaller
|
// whose needCount exceeds the remaining count stays parked while a smaller
|
||||||
// waiter can proceed.
|
// waiter can proceed.
|
||||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(semaphore.WakeKey);
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,10 +370,13 @@ public static class KernelSemaphoreCompatExports
|
|||||||
// exactly once in its wake handler. Zeroing here as well would double-count
|
// exactly once in its wake handler. Zeroing here as well would double-count
|
||||||
// and silently absorb the increment of a waiter that parks between this
|
// and silently absorb the increment of a waiter that parks between this
|
||||||
// gate release and the wake-all below.
|
// gate release and the wake-all below.
|
||||||
TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}");
|
if (_traceSema)
|
||||||
|
{
|
||||||
|
TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(semaphore.WakeKey);
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,8 +400,11 @@ public static class KernelSemaphoreCompatExports
|
|||||||
semaphore.Deleted = true;
|
semaphore.Deleted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(semaphore.WakeKey);
|
||||||
TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'");
|
if (_traceSema)
|
||||||
|
{
|
||||||
|
TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'");
|
||||||
|
}
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,7 +430,10 @@ public static class KernelSemaphoreCompatExports
|
|||||||
{
|
{
|
||||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED;
|
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED;
|
||||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||||
TraceSemaphore($"wake-deleted name='{semaphore.Name}' need={waiter.NeedCount}");
|
if (_traceSema)
|
||||||
|
{
|
||||||
|
TraceSemaphore($"wake-deleted name='{semaphore.Name}' need={waiter.NeedCount}");
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,7 +441,10 @@ public static class KernelSemaphoreCompatExports
|
|||||||
{
|
{
|
||||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_CANCELED;
|
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_CANCELED;
|
||||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||||
TraceSemaphore($"wake-canceled name='{semaphore.Name}' need={waiter.NeedCount}");
|
if (_traceSema)
|
||||||
|
{
|
||||||
|
TraceSemaphore($"wake-canceled name='{semaphore.Name}' need={waiter.NeedCount}");
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,7 +453,10 @@ public static class KernelSemaphoreCompatExports
|
|||||||
semaphore.Count -= waiter.NeedCount;
|
semaphore.Count -= waiter.NeedCount;
|
||||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||||
TraceSemaphore($"wake-consume name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
if (_traceSema)
|
||||||
|
{
|
||||||
|
TraceSemaphore($"wake-consume name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,7 +497,10 @@ public static class KernelSemaphoreCompatExports
|
|||||||
{
|
{
|
||||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||||
TraceSemaphore($"wake-timeout name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
if (_traceSema)
|
||||||
|
{
|
||||||
|
TraceSemaphore($"wake-timeout name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result = waiter.Result!.Value;
|
result = waiter.Result!.Value;
|
||||||
@@ -456,11 +522,13 @@ public static class KernelSemaphoreCompatExports
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Call sites must check this before building the interpolated message; the trace
|
||||||
|
// strings would otherwise be allocated on every semaphore op even with tracing off.
|
||||||
|
private static readonly bool _traceSema =
|
||||||
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal);
|
||||||
|
|
||||||
private static void TraceSemaphore(string message)
|
private static void TraceSemaphore(string message)
|
||||||
{
|
{
|
||||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal))
|
Console.Error.WriteLine($"[LOADER][TRACE] sema.{message}");
|
||||||
{
|
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] sema.{message}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,12 @@
|
|||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Kernel;
|
namespace SharpEmu.Libs.Kernel;
|
||||||
|
|
||||||
internal static class KernelVirtualRangeAllocator
|
internal static class KernelVirtualRangeAllocator
|
||||||
{
|
{
|
||||||
private static readonly ConcurrentDictionary<Type, Accessor> _accessors = new();
|
|
||||||
|
|
||||||
public static bool TryReserve(
|
public static bool TryReserve(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
ulong desiredAddress,
|
ulong desiredAddress,
|
||||||
@@ -31,38 +28,24 @@ internal static class KernelVirtualRangeAllocator
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!TryResolveAccessor(ctx.Memory, out var target, out var accessor))
|
if (!TryResolveAddressSpace(ctx.Memory, out var addressSpace))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt missing on {ctx.Memory.GetType().FullName}");
|
Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt missing on {ctx.Memory.GetType().FullName}");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allowSearch && accessor.AllocateAtOrAbove is not null)
|
if (allowSearch &&
|
||||||
|
addressSpace.TryAllocateAtOrAbove(desiredAddress, length, executable, alignment, out var searchedAddress) &&
|
||||||
|
searchedAddress != 0)
|
||||||
{
|
{
|
||||||
var searchArgs = new object[] { desiredAddress, length, executable, alignment, 0UL };
|
mappedAddress = searchedAddress;
|
||||||
var searchResult = accessor.AllocateAtOrAbove.Invoke(target, searchArgs);
|
return true;
|
||||||
if (searchResult is bool trueValue && trueValue &&
|
|
||||||
searchArgs[4] is ulong searchedAddress && searchedAddress != 0)
|
|
||||||
{
|
|
||||||
mappedAddress = searchedAddress;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accessor.AllocateAt is null)
|
var allocated = addressSpace.AllocateAt(desiredAddress, length, executable, allowAllocateAtAlternative);
|
||||||
|
if (allocated == 0)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt missing on {target.GetType().FullName}");
|
Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt returned {typeof(ulong).FullName} value=0");
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var invokeArgs = accessor.AllocateAtHasAllowAlternativeArg
|
|
||||||
? new object[] { desiredAddress, length, executable, allowAllocateAtAlternative }
|
|
||||||
: new object[] { desiredAddress, length, executable };
|
|
||||||
var result = accessor.AllocateAt.Invoke(target, invokeArgs);
|
|
||||||
if (result is not ulong allocated || allocated == 0)
|
|
||||||
{
|
|
||||||
var resultType = result?.GetType().FullName ?? "null";
|
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt returned {resultType} value={result ?? "null"}");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,84 +59,36 @@ internal static class KernelVirtualRangeAllocator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryResolveAccessor(object rootMemory, out object target, out Accessor accessor)
|
/// <summary>
|
||||||
|
/// Finds the <see cref="IGuestAddressSpace"/> behind <paramref name="rootMemory"/>,
|
||||||
|
/// unwrapping decorators (bounded, like the reflection walker this replaced).
|
||||||
|
/// </summary>
|
||||||
|
public static bool TryResolveAddressSpace(ICpuMemory rootMemory, [NotNullWhen(true)] out IGuestAddressSpace? addressSpace)
|
||||||
{
|
{
|
||||||
target = rootMemory;
|
var target = rootMemory;
|
||||||
accessor = default;
|
|
||||||
|
|
||||||
for (var depth = 0; depth < 4; depth++)
|
for (var depth = 0; depth < 4; depth++)
|
||||||
{
|
{
|
||||||
accessor = _accessors.GetOrAdd(target.GetType(), DiscoverAccessor);
|
if (target is IGuestAddressSpace resolved)
|
||||||
if (accessor.AllocateAt is not null || accessor.AllocateAtOrAbove is not null)
|
|
||||||
{
|
{
|
||||||
|
addressSpace = resolved;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accessor.InnerProperty is null)
|
if (target is not ICpuMemoryWrapper wrapper)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
var innerValue = accessor.InnerProperty.GetValue(target);
|
var inner = wrapper.Inner;
|
||||||
if (innerValue is null || ReferenceEquals(innerValue, target))
|
if (inner is null || ReferenceEquals(inner, target))
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
target = innerValue;
|
target = inner;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addressSpace = null;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Accessor DiscoverAccessor(Type type)
|
|
||||||
{
|
|
||||||
MethodInfo? allocateAt = null;
|
|
||||||
MethodInfo? allocateAtOrAbove = null;
|
|
||||||
var allocateAtHasAllowAlternativeArg = false;
|
|
||||||
|
|
||||||
foreach (var candidate in type.GetMethods(BindingFlags.Public | BindingFlags.Instance))
|
|
||||||
{
|
|
||||||
var parameters = candidate.GetParameters();
|
|
||||||
if (string.Equals(candidate.Name, "TryAllocateAtOrAbove", StringComparison.Ordinal) &&
|
|
||||||
parameters.Length == 5 &&
|
|
||||||
parameters[0].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[1].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[2].ParameterType == typeof(bool) &&
|
|
||||||
parameters[3].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[4].ParameterType == typeof(ulong).MakeByRefType())
|
|
||||||
{
|
|
||||||
allocateAtOrAbove = candidate;
|
|
||||||
}
|
|
||||||
else if (string.Equals(candidate.Name, "AllocateAt", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
if (parameters.Length == 3 &&
|
|
||||||
parameters[0].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[1].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[2].ParameterType == typeof(bool))
|
|
||||||
{
|
|
||||||
allocateAt = candidate;
|
|
||||||
allocateAtHasAllowAlternativeArg = false;
|
|
||||||
}
|
|
||||||
else if (parameters.Length == 4 &&
|
|
||||||
parameters[0].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[1].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[2].ParameterType == typeof(bool) &&
|
|
||||||
parameters[3].ParameterType == typeof(bool))
|
|
||||||
{
|
|
||||||
allocateAt = candidate;
|
|
||||||
allocateAtHasAllowAlternativeArg = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var innerProperty = type.GetProperty("Inner", BindingFlags.Public | BindingFlags.Instance);
|
|
||||||
return new Accessor(allocateAt, allocateAtOrAbove, allocateAtHasAllowAlternativeArg, innerProperty);
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly record struct Accessor(
|
|
||||||
MethodInfo? AllocateAt,
|
|
||||||
MethodInfo? AllocateAtOrAbove,
|
|
||||||
bool AllocateAtHasAllowAlternativeArg,
|
|
||||||
PropertyInfo? InnerProperty);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ public static class LibcInternalExports
|
|||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "NWtTN10cJzE",
|
Nid = "NWtTN10cJzE",
|
||||||
ExportName = "LibcHeapGetTraceInfo",
|
ExportName = "sceLibcHeapGetTraceInfo",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "LibcInternalExt")]
|
LibraryName = "LibcInternalExt")]
|
||||||
public static int LibcHeapGetTraceInfo(CpuContext ctx)
|
public static int LibcHeapGetTraceInfo(CpuContext ctx)
|
||||||
|
|||||||
@@ -7,6 +7,17 @@ namespace SharpEmu.Libs.Mouse;
|
|||||||
|
|
||||||
public static class MouseExports
|
public static class MouseExports
|
||||||
{
|
{
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Qs0wWulgl7U",
|
||||||
|
ExportName = "sceMouseInit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceMouse")]
|
||||||
|
public static int MouseInit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
// Returns 0 read entries: no mouse is connected. This NID was previously misbound
|
// Returns 0 read entries: no mouse is connected. This NID was previously misbound
|
||||||
// as an sceNgs2VoiceGetState alias.
|
// as an sceNgs2VoiceGetState alias.
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
|
|||||||
@@ -146,6 +146,13 @@ public static class NetCtlExports
|
|||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK, typeof(long));
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK, typeof(long));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "1NE9OWdBIww",
|
||||||
|
ExportName = "sceNetCtlRegisterCallbackV6",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNetCtl")]
|
||||||
|
public static int NetCtlRegisterCallbackV6(CpuContext ctx) => NetCtlRegisterCallback(ctx);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "obuxdTiwkF8",
|
Nid = "obuxdTiwkF8",
|
||||||
ExportName = "sceNetCtlGetInfo",
|
ExportName = "sceNetCtlGetInfo",
|
||||||
|
|||||||
@@ -237,6 +237,27 @@ public static class Ngs2Exports
|
|||||||
LibraryName = "libSceNgs2")]
|
LibraryName = "libSceNgs2")]
|
||||||
public static int Ngs2PanInit(CpuContext ctx) => ctx.SetReturn(0);
|
public static int Ngs2PanInit(CpuContext ctx) => ctx.SetReturn(0);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "1WsleK-MTkE",
|
||||||
|
ExportName = "sceNgs2GeomCalcListener",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2GeomCalcListener(CpuContext ctx) => ctx.SetReturn(0);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "0lbbayqDNoE",
|
||||||
|
ExportName = "sceNgs2GeomResetSourceParam",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2GeomResetSourceParam(CpuContext ctx) => ctx.SetReturn(0);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "7Lcfo8SmpsU",
|
||||||
|
ExportName = "sceNgs2GeomResetListenerParam",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2GeomResetListenerParam(CpuContext ctx) => ctx.SetReturn(0);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "i0VnXM-C9fc",
|
Nid = "i0VnXM-C9fc",
|
||||||
ExportName = "sceNgs2SystemRender",
|
ExportName = "sceNgs2SystemRender",
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Np;
|
||||||
|
|
||||||
|
// Stub for sce::Np::CppWebApi: titles abort PS5-component startup if
|
||||||
|
// Common::initialize returns a negative SCE error, so no-op success is required to boot.
|
||||||
|
public static class NpCppWebApiExports
|
||||||
|
{
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "UYPxv8MIzGo",
|
||||||
|
ExportName = "_ZN3sce2Np9CppWebApi6Common10initializeERKNS2_10InitParamsERNS2_10LibContextE",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpCppWebApi")]
|
||||||
|
public static int CppWebApiCommonInitialize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
// int Common::initialize(const InitParams&, LibContext&) — 0 on success.
|
||||||
|
TraceCppWebApi("common_initialize", ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi]);
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TraceCppWebApi(string operation, ulong arg0, ulong arg1)
|
||||||
|
{
|
||||||
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NP"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] np_cppwebapi.{operation} arg0=0x{arg0:X16} arg1=0x{arg1:X16}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -59,7 +59,7 @@ public static class NpManagerExports
|
|||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "rbknaUjpqWo",
|
Nid = "rbknaUjpqWo",
|
||||||
ExportName = "sceNpGetOnlineIdA",
|
ExportName = "sceNpGetAccountIdA",
|
||||||
Target = Generation.Gen5,
|
Target = Generation.Gen5,
|
||||||
LibraryName = "libSceNpManager")]
|
LibraryName = "libSceNpManager")]
|
||||||
public static int NpGetOnlineIdA(CpuContext ctx)
|
public static int NpGetOnlineIdA(CpuContext ctx)
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ namespace SharpEmu.Libs.Np;
|
|||||||
public static class NpUniversalDataSystemExports
|
public static class NpUniversalDataSystemExports
|
||||||
{
|
{
|
||||||
private const int NpUniversalDataSystemErrorInvalidArgument = unchecked((int)0x80553102);
|
private const int NpUniversalDataSystemErrorInvalidArgument = unchecked((int)0x80553102);
|
||||||
|
private static readonly object _eventGate = new();
|
||||||
|
private static readonly HashSet<int> _createdEvents = [];
|
||||||
private static int _nextHandle = 1;
|
private static int _nextHandle = 1;
|
||||||
|
private static int _nextEvent = 1;
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "sjaobBgqeB4",
|
Nid = "sjaobBgqeB4",
|
||||||
@@ -67,6 +70,114 @@ public static class NpUniversalDataSystemExports
|
|||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "p+GcLqwpL9M",
|
||||||
|
ExportName = "sceNpUniversalDataSystemCreateEvent",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpUniversalDataSystem")]
|
||||||
|
public static int NpUniversalDataSystemCreateEvent(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var parameterAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (parameterAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(NpUniversalDataSystemErrorInvalidArgument, typeof(long));
|
||||||
|
}
|
||||||
|
|
||||||
|
var eventId = Interlocked.Increment(ref _nextEvent);
|
||||||
|
lock (_eventGate)
|
||||||
|
{
|
||||||
|
_createdEvents.Add(eventId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.TryWriteInt32(ctx[CpuRegister.Rdx], eventId, checkNil: true) ||
|
||||||
|
ctx.TryWriteInt32(ctx[CpuRegister.Rcx], eventId, checkNil: true))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(0, typeof(long));
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_eventGate)
|
||||||
|
{
|
||||||
|
_createdEvents.Remove(eventId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "wG+84pnNIuo",
|
||||||
|
ExportName = "sceNpUniversalDataSystemDestroyEvent",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpUniversalDataSystem")]
|
||||||
|
public static int NpUniversalDataSystemDestroyEvent(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var eventId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
lock (_eventGate)
|
||||||
|
{
|
||||||
|
_createdEvents.Remove(eventId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.SetReturn(0, typeof(long));
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "MfDb+4Nln64",
|
||||||
|
ExportName = "sceNpUniversalDataSystemEventPropertyObjectSetString",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpUniversalDataSystem")]
|
||||||
|
public static int NpUniversalDataSystemEventPropertyObjectSetString(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var propertyObjectAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var valueAddress = ctx[CpuRegister.Rdx];
|
||||||
|
if (propertyObjectAddress == 0 || valueAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(NpUniversalDataSystemErrorInvalidArgument, typeof(long));
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> probe = stackalloc byte[1];
|
||||||
|
return ctx.Memory.TryRead(propertyObjectAddress, probe) &&
|
||||||
|
ctx.Memory.TryRead(valueAddress, probe)
|
||||||
|
? ctx.SetReturn(0, typeof(long))
|
||||||
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Wxbg5x3pTXA",
|
||||||
|
ExportName = "sceNpUniversalDataSystemEventPropertyObjectSetArray",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpUniversalDataSystem")]
|
||||||
|
public static int NpUniversalDataSystemEventPropertyObjectSetArray(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var propertyObjectAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var valueAddress = ctx[CpuRegister.Rdx];
|
||||||
|
if (propertyObjectAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(NpUniversalDataSystemErrorInvalidArgument, typeof(long));
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> probe = stackalloc byte[1];
|
||||||
|
if (!ctx.Memory.TryRead(propertyObjectAddress, probe))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueAddress != 0 && !ctx.Memory.TryRead(valueAddress, probe))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.SetReturn(0, typeof(long));
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "CzkKf7ahIyU",
|
||||||
|
ExportName = "sceNpUniversalDataSystemPostEvent",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpUniversalDataSystem")]
|
||||||
|
public static int NpUniversalDataSystemPostEvent(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(0, typeof(long));
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "tpFJ8LIKvPw",
|
Nid = "tpFJ8LIKvPw",
|
||||||
ExportName = "sceNpUniversalDataSystemRegisterContext",
|
ExportName = "sceNpUniversalDataSystemRegisterContext",
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ public static class NpWebApi2Exports
|
|||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "WV1GwM32NgY",
|
Nid = "WV1GwM32NgY",
|
||||||
ExportName = "sceNpWebApi2InitializeForToolkit",
|
ExportName = "sceNpWebApi2PushEventCreateHandle",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libSceNpWebApi2")]
|
LibraryName = "libSceNpWebApi2")]
|
||||||
public static int NpWebApi2InitializeAlt(CpuContext ctx)
|
public static int NpWebApi2InitializeAlt(CpuContext ctx)
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Pad;
|
||||||
|
|
||||||
|
// Minimal libSceBluetoothHid stub: no host Bluetooth passthrough, so report
|
||||||
|
// success and let the Pad path provide input (SHARPEMU_BTHID_UNAVAILABLE=1 fails instead).
|
||||||
|
public static class BluetoothHidExports
|
||||||
|
{
|
||||||
|
private const int BluetoothHidUnavailable = unchecked((int)0x80960001);
|
||||||
|
|
||||||
|
private static readonly bool _reportUnavailable = string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_BTHID_UNAVAILABLE"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
|
||||||
|
private static int Result(CpuContext ctx) =>
|
||||||
|
ctx.SetReturn(_reportUnavailable ? BluetoothHidUnavailable : 0);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "tul3-GzejQc",
|
||||||
|
ExportName = "sceBluetoothHidInit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceBluetoothHid")]
|
||||||
|
public static int BluetoothHidInit(CpuContext ctx) => Result(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "4FUZ+c52d2k",
|
||||||
|
ExportName = "sceBluetoothHidRegisterDevice",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceBluetoothHid")]
|
||||||
|
public static int BluetoothHidRegisterDevice(CpuContext ctx) => Result(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "4Ypfo9RIwfM",
|
||||||
|
ExportName = "sceBluetoothHidRegisterCallback",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceBluetoothHid")]
|
||||||
|
public static int BluetoothHidRegisterCallback(CpuContext ctx) => Result(ctx);
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ public static class PadExports
|
|||||||
private static PadState _cachedInputState;
|
private static PadState _cachedInputState;
|
||||||
|
|
||||||
private static bool _initialized;
|
private static bool _initialized;
|
||||||
|
private static int _controlsAnnouncementLogged;
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "hv1luiJrqQM",
|
Nid = "hv1luiJrqQM",
|
||||||
@@ -47,7 +48,18 @@ public static class PadExports
|
|||||||
ExportName = "scePadOpen",
|
ExportName = "scePadOpen",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libScePad")]
|
LibraryName = "libScePad")]
|
||||||
public static int PadOpen(CpuContext ctx)
|
public static int PadOpen(CpuContext ctx) => PadOpenCore(ctx, extended: false);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "WFIiSfXGUq8",
|
||||||
|
ExportName = "scePadOpenExt",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePad")]
|
||||||
|
public static int PadOpenExt(CpuContext ctx) => PadOpenCore(ctx, extended: true);
|
||||||
|
|
||||||
|
// scePadOpen rejects a non-null 4th arg and non-standard ports; scePadOpenExt accepts a
|
||||||
|
// ScePadOpenExtParam* plus ports 1/2 (racing titles retry scePadOpenExt(type=2) forever if rejected).
|
||||||
|
private static int PadOpenCore(CpuContext ctx, bool extended)
|
||||||
{
|
{
|
||||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
var type = unchecked((int)ctx[CpuRegister.Rsi]);
|
var type = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
@@ -63,21 +75,39 @@ public static class PadExports
|
|||||||
return ctx.SetReturn(OrbisPadErrorDeviceNoHandle);
|
return ctx.SetReturn(OrbisPadErrorDeviceNoHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userId != PrimaryUserId || type != StandardPortType || index != 0 || parameterAddress != 0)
|
var typeAccepted = extended ? type is 0 or 1 or 2 : type == StandardPortType;
|
||||||
|
if (userId != PrimaryUserId || !typeAccepted || index != 0 || (!extended && parameterAddress != 0))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
|
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
|
||||||
}
|
}
|
||||||
|
|
||||||
DualSenseReader.EnsureStarted();
|
DualSenseReader.EnsureStarted();
|
||||||
XInputReader.EnsureStarted();
|
XInputReader.EnsureStarted();
|
||||||
Console.Error.WriteLine(DualSenseReader.TryGetState(out _)
|
if (Interlocked.Exchange(ref _controlsAnnouncementLogged, 1) == 0)
|
||||||
? "[LOADER][INFO] Controls: DualSense connected (keyboard fallback also active)."
|
{
|
||||||
: XInputReader.TryGetState(out _)
|
Console.Error.WriteLine(DualSenseReader.TryGetState(out _)
|
||||||
? "[LOADER][INFO] Controls: Xbox controller connected (keyboard fallback also active)."
|
? "[LOADER][INFO] Controls: DualSense connected (keyboard fallback also active)."
|
||||||
: "[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options. A DualSense or Xbox controller will be used automatically when plugged in.");
|
: XInputReader.TryGetState(out _)
|
||||||
|
? "[LOADER][INFO] Controls: Xbox controller connected (keyboard fallback also active)."
|
||||||
|
: "[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options. A DualSense or Xbox controller will be used automatically when plugged in.");
|
||||||
|
}
|
||||||
|
|
||||||
return ctx.SetReturn(PrimaryPadHandle);
|
return ctx.SetReturn(PrimaryPadHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "6ncge5+l5Qs",
|
||||||
|
ExportName = "scePadClose",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePad")]
|
||||||
|
public static int PadClose(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
return handle == PrimaryPadHandle
|
||||||
|
? ctx.SetReturn(0)
|
||||||
|
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "clVvL4ZDntw",
|
Nid = "clVvL4ZDntw",
|
||||||
ExportName = "scePadSetMotionSensorState",
|
ExportName = "scePadSetMotionSensorState",
|
||||||
@@ -126,6 +156,47 @@ public static class PadExports
|
|||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "hGbf2QTBmqc",
|
||||||
|
ExportName = "scePadGetExtControllerInformation",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePad")]
|
||||||
|
public static int PadGetExtControllerInformation(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var informationAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (handle != PrimaryPadHandle)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (informationAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base ScePadControllerInformation + device-class/connection fields: report a connected
|
||||||
|
// DualSense so the guest's open -> get-ext-info -> close probe loop resolves.
|
||||||
|
Span<byte> information = stackalloc byte[0x40];
|
||||||
|
information.Clear();
|
||||||
|
BinaryPrimitives.WriteSingleLittleEndian(information[0x00..], 44.86f);
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(information[0x04..], 1920);
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(information[0x06..], 943);
|
||||||
|
information[0x08] = 30;
|
||||||
|
information[0x09] = 30;
|
||||||
|
information[0x0A] = StandardPortType;
|
||||||
|
information[0x0B] = 1; // connected count
|
||||||
|
information[0x0C] = 1; // connected
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(information[0x10..], 0);
|
||||||
|
information[0x1C] = 0; // deviceClass: 0 = standard controller / DualSense
|
||||||
|
information[0x1D] = 1; // connected (ext)
|
||||||
|
information[0x1E] = 0; // connectionType: local
|
||||||
|
|
||||||
|
return ctx.Memory.TryWrite(informationAddress, information)
|
||||||
|
? ctx.SetReturn(0)
|
||||||
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "YndgXqQVV7c",
|
Nid = "YndgXqQVV7c",
|
||||||
ExportName = "scePadReadState",
|
ExportName = "scePadReadState",
|
||||||
@@ -192,9 +263,43 @@ public static class PadExports
|
|||||||
LibraryName = "libScePad")]
|
LibraryName = "libScePad")]
|
||||||
public static int PadSetTriggerEffect(CpuContext ctx)
|
public static int PadSetTriggerEffect(CpuContext ctx)
|
||||||
{
|
{
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var parameterAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (handle != PrimaryPadHandle)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameterAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> parameter = stackalloc byte[120];
|
||||||
|
if (!ctx.Memory.TryRead(parameterAddress, parameter))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var triggerMask = parameter[0];
|
||||||
|
XInputReader.SetTriggerRumble(
|
||||||
|
(triggerMask & 0x01) != 0 ? DecodeTriggerVibration(parameter[8..64]) : null,
|
||||||
|
(triggerMask & 0x02) != 0 ? DecodeTriggerVibration(parameter[64..120]) : null);
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static byte DecodeTriggerVibration(ReadOnlySpan<byte> command)
|
||||||
|
{
|
||||||
|
var mode = BinaryPrimitives.ReadUInt32LittleEndian(command);
|
||||||
|
var amplitude = mode switch
|
||||||
|
{
|
||||||
|
3 when command[10] != 0 => command[9],
|
||||||
|
6 when command[8] != 0 => command[9..19].ToArray().Max(),
|
||||||
|
_ => (byte)0,
|
||||||
|
};
|
||||||
|
return (byte)(Math.Min(amplitude, (byte)8) * 255 / 8);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "yFVnOdGxvZY",
|
Nid = "yFVnOdGxvZY",
|
||||||
ExportName = "scePadSetVibration",
|
ExportName = "scePadSetVibration",
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ internal static class XInputReader
|
|||||||
private static int _slot = -1; // connected XInput user index, -1 when none
|
private static int _slot = -1; // connected XInput user index, -1 when none
|
||||||
private static byte _motorLeft;
|
private static byte _motorLeft;
|
||||||
private static byte _motorRight;
|
private static byte _motorRight;
|
||||||
|
private static byte _triggerLeft;
|
||||||
|
private static byte _triggerRight;
|
||||||
|
|
||||||
/// <summary>Starts the background reader once; safe to call repeatedly.</summary>
|
/// <summary>Starts the background reader once; safe to call repeatedly.</summary>
|
||||||
internal static void EnsureStarted()
|
internal static void EnsureStarted()
|
||||||
@@ -99,6 +101,31 @@ internal static class XInputReader
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Approximates per-trigger vibration on the two XInput body motors.</summary>
|
||||||
|
internal static void SetTriggerRumble(byte? leftTrigger, byte? rightTrigger)
|
||||||
|
{
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
var changed = false;
|
||||||
|
if (leftTrigger is { } left)
|
||||||
|
{
|
||||||
|
changed |= _triggerLeft != left;
|
||||||
|
_triggerLeft = left;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rightTrigger is { } right)
|
||||||
|
{
|
||||||
|
changed |= _triggerRight != right;
|
||||||
|
_triggerRight = right;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed)
|
||||||
|
{
|
||||||
|
SendRumbleLocked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void SendRumbleLocked()
|
private static void SendRumbleLocked()
|
||||||
{
|
{
|
||||||
if (_slot < 0)
|
if (_slot < 0)
|
||||||
@@ -108,8 +135,8 @@ internal static class XInputReader
|
|||||||
|
|
||||||
var vibration = new XInputVibration
|
var vibration = new XInputVibration
|
||||||
{
|
{
|
||||||
LeftMotorSpeed = (ushort)(_motorLeft * 257), // 0..255 -> 0..65535
|
LeftMotorSpeed = (ushort)(Math.Max(_motorLeft, _triggerLeft) * 257),
|
||||||
RightMotorSpeed = (ushort)(_motorRight * 257),
|
RightMotorSpeed = (ushort)(Math.Max(_motorRight, _triggerRight) * 257),
|
||||||
};
|
};
|
||||||
_ = XInputSetState((uint)_slot, ref vibration);
|
_ = XInputSetState((uint)_slot, ref vibration);
|
||||||
}
|
}
|
||||||
@@ -147,6 +174,8 @@ internal static class XInputReader
|
|||||||
_slot = -1;
|
_slot = -1;
|
||||||
_motorLeft = 0;
|
_motorLeft = 0;
|
||||||
_motorRight = 0;
|
_motorRight = 0;
|
||||||
|
_triggerLeft = 0;
|
||||||
|
_triggerRight = 0;
|
||||||
_state = default;
|
_state = default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ public static class SaveDataDialogExports
|
|||||||
private const int ErrorArgNull = unchecked((int)0x80B8000D);
|
private const int ErrorArgNull = unchecked((int)0x80B8000D);
|
||||||
|
|
||||||
private const int ResultSize = 0x48;
|
private const int ResultSize = 0x48;
|
||||||
|
private const int ButtonIdAffirmative = 1;
|
||||||
private static int _status;
|
private static int _status;
|
||||||
private static int _lastMode;
|
private static int _lastMode;
|
||||||
private static ulong _lastUserData;
|
private static ulong _lastUserData;
|
||||||
@@ -61,10 +62,10 @@ public static class SaveDataDialogExports
|
|||||||
_lastMode = TryReadInt32(ctx, paramAddress, out var mode) ? mode : 0;
|
_lastMode = TryReadInt32(ctx, paramAddress, out var mode) ? mode : 0;
|
||||||
_lastUserData = ctx.TryReadUInt64(paramAddress + 0xC8, out var userData) ? userData : 0;
|
_lastUserData = ctx.TryReadUInt64(paramAddress + 0xC8, out var userData) ? userData : 0;
|
||||||
|
|
||||||
// There is no host save dialog yet. Complete immediately with OK so
|
// There is no host save dialog yet. Enter RUNNING so the close path sees a live
|
||||||
// guest polling sees a finished dialog instead of spinning forever.
|
// dialog; the guest's next status poll auto-dismisses it (see PollStatus).
|
||||||
Interlocked.Exchange(ref _status, StatusFinished);
|
Interlocked.Exchange(ref _status, StatusRunning);
|
||||||
TraceSaveDataDialog($"open mode={_lastMode} userData=0x{_lastUserData:X16} -> finished");
|
TraceSaveDataDialog($"open mode={_lastMode} userData=0x{_lastUserData:X16} -> running");
|
||||||
return ctx.SetReturn(ErrorOk);
|
return ctx.SetReturn(ErrorOk);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,14 +74,24 @@ public static class SaveDataDialogExports
|
|||||||
ExportName = "sceSaveDataDialogGetStatus",
|
ExportName = "sceSaveDataDialogGetStatus",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libSceSaveDataDialog")]
|
LibraryName = "libSceSaveDataDialog")]
|
||||||
public static int SaveDataDialogGetStatus(CpuContext ctx) => ctx.SetReturn(Volatile.Read(ref _status));
|
public static int SaveDataDialogGetStatus(CpuContext ctx) => ctx.SetReturn(PollStatus());
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "KK3Bdg1RWK0",
|
Nid = "KK3Bdg1RWK0",
|
||||||
ExportName = "sceSaveDataDialogUpdateStatus",
|
ExportName = "sceSaveDataDialogUpdateStatus",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libSceSaveDataDialog")]
|
LibraryName = "libSceSaveDataDialog")]
|
||||||
public static int SaveDataDialogUpdateStatus(CpuContext ctx) => ctx.SetReturn(Volatile.Read(ref _status));
|
public static int SaveDataDialogUpdateStatus(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(
|
[SysAbiExport(
|
||||||
Nid = "en7gNVnh878",
|
Nid = "en7gNVnh878",
|
||||||
@@ -107,11 +118,13 @@ public static class SaveDataDialogExports
|
|||||||
return ctx.SetReturn(ErrorNotFinished);
|
return ctx.SetReturn(ErrorNotFinished);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Report the affirmative button so save 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];
|
Span<byte> result = stackalloc byte[ResultSize];
|
||||||
result.Clear();
|
result.Clear();
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(result[0x00..], _lastMode);
|
BinaryPrimitives.WriteInt32LittleEndian(result[0x00..], _lastMode);
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(result[0x04..], 0);
|
BinaryPrimitives.WriteInt32LittleEndian(result[0x04..], 0);
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(result[0x08..], 0);
|
BinaryPrimitives.WriteInt32LittleEndian(result[0x08..], ButtonIdAffirmative);
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(result[0x20..], _lastUserData);
|
BinaryPrimitives.WriteUInt64LittleEndian(result[0x20..], _lastUserData);
|
||||||
|
|
||||||
if (!ctx.Memory.TryWrite(resultAddress, result))
|
if (!ctx.Memory.TryWrite(resultAddress, result))
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ public static class SaveDataExports
|
|||||||
private const int MountResultSize = 0x40;
|
private const int MountResultSize = 0x40;
|
||||||
private static readonly object _stateGate = new();
|
private static readonly object _stateGate = new();
|
||||||
private static readonly HashSet<int> _transactionResources = [];
|
private static readonly HashSet<int> _transactionResources = [];
|
||||||
|
private static readonly HashSet<int> _preparedTransactionResources = [];
|
||||||
private static string? _titleId;
|
private static string? _titleId;
|
||||||
private static int _nextTransactionResource;
|
private static int _nextTransactionResource;
|
||||||
|
|
||||||
@@ -40,6 +41,7 @@ public static class SaveDataExports
|
|||||||
{
|
{
|
||||||
_titleId = string.IsNullOrWhiteSpace(titleId) ? null : SanitizePathSegment(titleId.Trim());
|
_titleId = string.IsNullOrWhiteSpace(titleId) ? null : SanitizePathSegment(titleId.Trim());
|
||||||
_transactionResources.Clear();
|
_transactionResources.Clear();
|
||||||
|
_preparedTransactionResources.Clear();
|
||||||
_nextTransactionResource = 0;
|
_nextTransactionResource = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,12 +280,104 @@ public static class SaveDataExports
|
|||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
_transactionResources.Remove(resource);
|
_transactionResources.Remove(resource);
|
||||||
|
_preparedTransactionResources.Remove(resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceSaveData($"delete_transaction_resource resource={resource}");
|
TraceSaveData($"delete_transaction_resource resource={resource}");
|
||||||
return ctx.SetReturn(0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "sDCBrmc61XU",
|
||||||
|
ExportName = "sceSaveDataPrepare",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSaveData")]
|
||||||
|
public static int SaveDataPrepare(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var mountPointAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var resource = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
|
if (mountPointAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadFixedAscii(ctx, mountPointAddress, 16, out var mountPoint))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(mountPoint))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
if (resource != 0)
|
||||||
|
{
|
||||||
|
_preparedTransactionResources.Add(resource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceSaveData($"prepare mount_point={mountPoint} resource={resource}");
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "ie7qhZ4X0Cc",
|
||||||
|
ExportName = "sceSaveDataCommit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSaveData")]
|
||||||
|
public static int SaveDataCommit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var commitAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (commitAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
_preparedTransactionResources.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceSaveData($"commit commit=0x{commitAddress:X16}");
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "uW4vfTwMQVo",
|
||||||
|
ExportName = "sceSaveDataUmount2",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSaveData")]
|
||||||
|
public static int SaveDataUmount2(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var mountPointAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (mountPointAddress == 0)
|
||||||
|
{
|
||||||
|
mountPointAddress = ctx[CpuRegister.Rsi];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mountPointAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadFixedAscii(ctx, mountPointAddress, 16, out var mountPoint))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(mountPoint))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
var unmounted = KernelMemoryCompatExports.TryUnregisterGuestPathMount(mountPoint);
|
||||||
|
TraceSaveData($"umount2 mount_point={mountPoint} unregistered={unmounted}");
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryReadSearchCond(CpuContext ctx, ulong address, out SearchCond cond)
|
private static bool TryReadSearchCond(CpuContext ctx, ulong address, out SearchCond cond)
|
||||||
{
|
{
|
||||||
cond = default;
|
cond = default;
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<ProjectReference Include="..\SharpEmu.HLE\SharpEmu.HLE.csproj" />
|
<ProjectReference Include="..\SharpEmu.HLE\SharpEmu.HLE.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="SharpEmu.Libs.Tests" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Silk.NET.Vulkan" />
|
<PackageReference Include="Silk.NET.Vulkan" />
|
||||||
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" />
|
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" />
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user