mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 081760be3f | |||
| e604fb606d | |||
| df53ff59d9 | |||
| 4c35831cb8 | |||
| 90fdd20f9a | |||
| ae5ef0abe7 | |||
| 5e2c21edf1 | |||
| 3fb9d4db1c | |||
| de13735972 | |||
| fc0efca297 | |||
| 2a9a261913 | |||
| 290f5fd3d7 | |||
| c06c70cad7 | |||
| 5e54250752 | |||
| be6a6a5935 | |||
| caf859cc52 | |||
| d2f3511002 | |||
| 90a5d5176f | |||
| 28a43e09c7 | |||
| 093cfa1f3e | |||
| d8397b022e | |||
| 85cc2b9892 | |||
| 293194c40b | |||
| 1f09de8896 | |||
| ddc452b4fc | |||
| 61a97baf85 | |||
| e80f96ecf5 | |||
| d49c0f1f10 | |||
| 1d33ef90fc | |||
| 26a570633c | |||
| e4f89445b9 | |||
| 1254cc1564 | |||
| 503b3f4d6b | |||
| 6b37ab54f2 | |||
| a84d2344fb | |||
| 787d3a1efb | |||
| 884584da67 | |||
| d43edc865a | |||
| cf6964710a | |||
| 4f028d0483 | |||
| 4db98bd8fe | |||
| 4600a2ed1f | |||
| c5c5ee1f36 | |||
| b48b1a5e09 | |||
| 511a01e03a | |||
| aaebfe017b | |||
| 9cdc8550ec | |||
| d6fccedab8 | |||
| fed7a6d062 | |||
| 50f78f3713 | |||
| 63b440efcd | |||
| 0565d01744 | |||
| 56bf00e9a5 | |||
| 46b729c5b4 |
@@ -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>
|
||||||
@@ -8,6 +8,7 @@ path = [
|
|||||||
"**/packages.lock.json",
|
"**/packages.lock.json",
|
||||||
"scripts/ps5_names.txt",
|
"scripts/ps5_names.txt",
|
||||||
"src/SharpEmu.HLE/Aerolib/aerolib.bin",
|
"src/SharpEmu.HLE/Aerolib/aerolib.bin",
|
||||||
|
"src/SharpEmu.GUI/Languages/**",
|
||||||
"_logs/**",
|
"_logs/**",
|
||||||
".github/images/**",
|
".github/images/**",
|
||||||
"assets/images/**"
|
"assets/images/**"
|
||||||
|
|||||||
@@ -12,4 +12,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<Project Path="src/SharpEmu.Libs/SharpEmu.Libs.csproj" />
|
<Project Path="src/SharpEmu.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())
|
||||||
@@ -3,8 +3,8 @@
|
|||||||
|
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import struct
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import struct
|
||||||
from base64 import b64encode as base64enc
|
from base64 import b64encode as base64enc
|
||||||
from binascii import unhexlify as uhx
|
from binascii import unhexlify as uhx
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -21,7 +21,7 @@ def name2nid(name):
|
|||||||
def generate():
|
def generate():
|
||||||
names_path = Path(NAMES)
|
names_path = Path(NAMES)
|
||||||
output_path = Path(OUTPUT)
|
output_path = Path(OUTPUT)
|
||||||
|
|
||||||
entries = []
|
entries = []
|
||||||
with open(names_path, 'r', encoding='utf-8') as f:
|
with open(names_path, 'r', encoding='utf-8') as f:
|
||||||
for line in f:
|
for line in f:
|
||||||
@@ -29,12 +29,12 @@ def generate():
|
|||||||
if name:
|
if name:
|
||||||
nid = name2nid(name)
|
nid = name2nid(name)
|
||||||
entries.append((nid, name))
|
entries.append((nid, name))
|
||||||
|
|
||||||
print(f"Found {len(entries)} entries")
|
print(f"Found {len(entries)} entries")
|
||||||
|
|
||||||
data = bytearray()
|
data = bytearray()
|
||||||
data.extend(struct.pack('<I', len(entries)))
|
data.extend(struct.pack('<I', len(entries)))
|
||||||
|
|
||||||
for nid, name in entries:
|
for nid, name in entries:
|
||||||
nid_bytes = nid.encode('utf-8')
|
nid_bytes = nid.encode('utf-8')
|
||||||
name_bytes = name.encode('utf-8')
|
name_bytes = name.encode('utf-8')
|
||||||
@@ -42,10 +42,11 @@ def generate():
|
|||||||
data.extend(nid_bytes)
|
data.extend(nid_bytes)
|
||||||
data.extend(struct.pack('<H', len(name_bytes)))
|
data.extend(struct.pack('<H', len(name_bytes)))
|
||||||
data.extend(name_bytes)
|
data.extend(name_bytes)
|
||||||
|
|
||||||
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
with open(output_path, 'wb') as f:
|
with open(output_path, 'wb') as f:
|
||||||
f.write(data)
|
f.write(data)
|
||||||
|
|
||||||
print(f"Generated: {output_path} ({len(data):,} bytes)")
|
print(f"Generated: {output_path} ({len(data):,} bytes)")
|
||||||
print(f"Total entries: {len(entries)}")
|
print(f"Total entries: {len(entries)}")
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||||
@@ -95,6 +96,7 @@ internal static partial class Program
|
|||||||
SharpEmuLog.MinimumLevel = logLevel;
|
SharpEmuLog.MinimumLevel = logLevel;
|
||||||
|
|
||||||
Log.Info(BuildInfo.Banner);
|
Log.Info(BuildInfo.Banner);
|
||||||
|
Log.Info(HostSystemInfo.Summary);
|
||||||
|
|
||||||
ebootPath = Path.GetFullPath(ebootPath);
|
ebootPath = Path.GetFullPath(ebootPath);
|
||||||
Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}");
|
Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}");
|
||||||
@@ -110,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}");
|
||||||
@@ -122,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))
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
<Version>0.0.1</Version>
|
<Version>0.0.1</Version>
|
||||||
|
<ServerGarbageCollection>true</ServerGarbageCollection>
|
||||||
|
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
|
||||||
|
<TieredPGO>true</TieredPGO>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||||
@@ -48,6 +51,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<TargetPath>LICENSE.txt</TargetPath>
|
<TargetPath>LICENSE.txt</TargetPath>
|
||||||
<Visible>False</Visible>
|
<Visible>False</Visible>
|
||||||
</Content>
|
</Content>
|
||||||
|
<Content Include="..\SharpEmu.GUI\Languages\*.json" Condition="'$(Configuration)' != 'Release'">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
<TargetPath>Languages\%(Filename)%(Extension)</TargetPath>
|
||||||
|
<Visible>False</Visible>
|
||||||
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Keep glfw as a loose file next to the executable; every other native
|
<!-- Keep glfw as a loose file next to the executable; every other native
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
*(ulong*)(xmmSlot + 8));
|
*(ulong*)(xmmSlot + 8));
|
||||||
}
|
}
|
||||||
cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL;
|
cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL;
|
||||||
if (string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
|
if (importStubEntry.Kind == ImportStubKind.BootstrapBridge)
|
||||||
{
|
{
|
||||||
NormalizeKernelDynlibDlsymArguments(cpuContext, out _, out _);
|
NormalizeKernelDynlibDlsymArguments(cpuContext, out _, out _);
|
||||||
*(ulong*)argPackPtr = cpuContext[CpuRegister.Rdi];
|
*(ulong*)argPackPtr = cpuContext[CpuRegister.Rdi];
|
||||||
@@ -238,6 +238,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;
|
||||||
@@ -245,12 +261,13 @@ public sealed partial class DirectExecutionBackend
|
|||||||
bool flag4 = !string.IsNullOrWhiteSpace(_importFilter);
|
bool flag4 = !string.IsNullOrWhiteSpace(_importFilter);
|
||||||
bool flag5 = false;
|
bool flag5 = false;
|
||||||
ExportedFunction? matchedExport = importStubEntry.Export;
|
ExportedFunction? matchedExport = importStubEntry.Export;
|
||||||
|
var traceFlags = importStubEntry.TraceFlags;
|
||||||
bool periodicTrace = num <= 128 ||
|
bool periodicTrace = num <= 128 ||
|
||||||
(num >= 240 && num <= 400) ||
|
(num >= 240 && num <= 400) ||
|
||||||
(num >= 900 && num <= 1300) ||
|
(num >= 900 && num <= 1300) ||
|
||||||
num % 100000 == 0L ||
|
num % 100000 == 0L ||
|
||||||
(importStubEntry.Nid == "tsvEmnenz48" && (num <= 256 || num % 1000 == 0L)) ||
|
((traceFlags & ImportStubTraceFlags.PeriodicEvery1000) != 0 && (num <= 256 || num % 1000 == 0L)) ||
|
||||||
(importStubEntry.Nid == "rTXw65xmLIA" && (num <= 256 || num % 128 == 0)) ||
|
((traceFlags & ImportStubTraceFlags.PeriodicEvery128) != 0 && (num <= 256 || num % 128 == 0)) ||
|
||||||
flag ||
|
flag ||
|
||||||
flag2 ||
|
flag2 ||
|
||||||
flag3;
|
flag3;
|
||||||
@@ -311,15 +328,15 @@ public sealed partial class DirectExecutionBackend
|
|||||||
cpuContext[CpuRegister.Rsi],
|
cpuContext[CpuRegister.Rsi],
|
||||||
cpuContext[CpuRegister.Rdx]);
|
cpuContext[CpuRegister.Rdx]);
|
||||||
}
|
}
|
||||||
if (importStubEntry.Nid == "8zTFvBIAIN8" && num <= 256)
|
if ((traceFlags & ImportStubTraceFlags.Memset) != 0 && num <= 256)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] memset#{num}: dst=0x{cpuContext[CpuRegister.Rdi]:X16} val=0x{cpuContext[CpuRegister.Rsi] & 0xFF:X2} len=0x{cpuContext[CpuRegister.Rdx]:X16} ret=0x{num7:X16}");
|
Console.Error.WriteLine($"[LOADER][TRACE] memset#{num}: dst=0x{cpuContext[CpuRegister.Rdi]:X16} val=0x{cpuContext[CpuRegister.Rsi] & 0xFF:X2} len=0x{cpuContext[CpuRegister.Rdx]:X16} ret=0x{num7:X16}");
|
||||||
}
|
}
|
||||||
if (importStubEntry.Nid == "tsvEmnenz48" && num <= 64)
|
if ((traceFlags & ImportStubTraceFlags.CxaAtexit) != 0 && num <= 64)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] __cxa_atexit#{num}: func=0x{cpuContext[CpuRegister.Rdi]:X16} arg=0x{cpuContext[CpuRegister.Rsi]:X16} dso=0x{cpuContext[CpuRegister.Rdx]:X16} ret=0x{num7:X16}");
|
Console.Error.WriteLine($"[LOADER][TRACE] __cxa_atexit#{num}: func=0x{cpuContext[CpuRegister.Rdi]:X16} arg=0x{cpuContext[CpuRegister.Rsi]:X16} dso=0x{cpuContext[CpuRegister.Rdx]:X16} ret=0x{num7:X16}");
|
||||||
}
|
}
|
||||||
if (importStubEntry.Nid == "bzQExy189ZI" || importStubEntry.Nid == "8G2LB+A3rzg")
|
if ((traceFlags & ImportStubTraceFlags.RawArgs) != 0)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] {importStubEntry.Nid}#{num}: rdi=0x{cpuContext[CpuRegister.Rdi]:X16} rsi=0x{cpuContext[CpuRegister.Rsi]:X16} rdx=0x{cpuContext[CpuRegister.Rdx]:X16} ret=0x{num7:X16}");
|
Console.Error.WriteLine($"[LOADER][TRACE] {importStubEntry.Nid}#{num}: rdi=0x{cpuContext[CpuRegister.Rdi]:X16} rsi=0x{cpuContext[CpuRegister.Rsi]:X16} rdx=0x{cpuContext[CpuRegister.Rdx]:X16} ret=0x{num7:X16}");
|
||||||
}
|
}
|
||||||
@@ -349,7 +366,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
Console.Error.Flush();
|
Console.Error.Flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (importStubEntry.Nid == "Ou3iL1abvng")
|
if ((traceFlags & ImportStubTraceFlags.StackChkFail) != 0)
|
||||||
{
|
{
|
||||||
if (_logStackCheck)
|
if (_logStackCheck)
|
||||||
{
|
{
|
||||||
@@ -381,7 +398,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
ActiveGuestReturnSlotAddress);
|
ActiveGuestReturnSlotAddress);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
|
if (importStubEntry.Kind == ImportStubKind.BootstrapBridge)
|
||||||
{
|
{
|
||||||
if (_logBootstrap)
|
if (_logBootstrap)
|
||||||
{
|
{
|
||||||
@@ -390,12 +407,11 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
orbisGen2Result = DispatchBootstrapBridge();
|
orbisGen2Result = DispatchBootstrapBridge();
|
||||||
}
|
}
|
||||||
else if (string.Equals(importStubEntry.Nid, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal) ||
|
else if (importStubEntry.Kind == ImportStubKind.KernelDynlibDlsym)
|
||||||
string.Equals(importStubEntry.Nid, "LwG8g3niqwA", StringComparison.Ordinal))
|
|
||||||
{
|
{
|
||||||
orbisGen2Result = DispatchKernelDynlibDlsym();
|
orbisGen2Result = DispatchKernelDynlibDlsym();
|
||||||
}
|
}
|
||||||
else if (string.Equals(importStubEntry.Nid, "r8mvOaWdi28", StringComparison.Ordinal))
|
else if (importStubEntry.Kind == ImportStubKind.Il2CppApiLookupSymbol)
|
||||||
{
|
{
|
||||||
orbisGen2Result = DispatchIl2CppApiLookupSymbol();
|
orbisGen2Result = DispatchIl2CppApiLookupSymbol();
|
||||||
}
|
}
|
||||||
@@ -736,6 +752,9 @@ public sealed partial class DirectExecutionBackend
|
|||||||
var expectedEqueueTimeout =
|
var expectedEqueueTimeout =
|
||||||
string.Equals(nid, "fzyMKs9kim0", StringComparison.Ordinal) &&
|
string.Equals(nid, "fzyMKs9kim0", StringComparison.Ordinal) &&
|
||||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
result == OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||||
|
var expectedEventFlagTimeout =
|
||||||
|
string.Equals(nid, "JTvBflhYazQ", StringComparison.Ordinal) &&
|
||||||
|
result == OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||||
var expectedMutexTrylockBusy =
|
var expectedMutexTrylockBusy =
|
||||||
string.Equals(nid, "K-jXhbt2gn4", StringComparison.Ordinal) &&
|
string.Equals(nid, "K-jXhbt2gn4", StringComparison.Ordinal) &&
|
||||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||||
@@ -748,6 +767,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
if (!expectedFileProbeMiss &&
|
if (!expectedFileProbeMiss &&
|
||||||
!expectedTimedWaitTimeout &&
|
!expectedTimedWaitTimeout &&
|
||||||
!expectedEqueueTimeout &&
|
!expectedEqueueTimeout &&
|
||||||
|
!expectedEventFlagTimeout &&
|
||||||
!expectedMutexTrylockBusy &&
|
!expectedMutexTrylockBusy &&
|
||||||
!expectedUserServiceNoEvent &&
|
!expectedUserServiceNoEvent &&
|
||||||
!expectedPrivacyInvalidParameter)
|
!expectedPrivacyInvalidParameter)
|
||||||
@@ -810,11 +830,15 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return !_logUsleep;
|
return !_logUsleep;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mutex lock uses this block-capable leaf path. Keep it out of the no-block subset.
|
||||||
return nid is
|
return nid is
|
||||||
"9UK1vLZQft4" or // scePthreadMutexLock
|
"9UK1vLZQft4" or // scePthreadMutexLock
|
||||||
"tn3VlD0hG60" or // scePthreadMutexUnlock
|
|
||||||
"7H0iTOciTLo" or // pthread_mutex_lock
|
"7H0iTOciTLo" or // pthread_mutex_lock
|
||||||
|
"tn3VlD0hG60" or // scePthreadMutexUnlock
|
||||||
"2Z+PpY6CaJg" or // pthread_mutex_unlock
|
"2Z+PpY6CaJg" or // pthread_mutex_unlock
|
||||||
|
"EgmLo6EWgso" or // pthread_rwlock_unlock
|
||||||
|
"+L98PIbGttk" or // scePthreadRwlockUnlock
|
||||||
|
"q1cHNfGycLI" or // scePadRead
|
||||||
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
|
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
|
||||||
"zgXifHT9ErY" or // sceVideoOutIsFlipPending
|
"zgXifHT9ErY" or // sceVideoOutIsFlipPending
|
||||||
"V++UgBtQhn0" or // sceAgcGetDataPacketPayloadAddress
|
"V++UgBtQhn0" or // sceAgcGetDataPacketPayloadAddress
|
||||||
@@ -873,7 +897,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
|
||||||
@@ -888,12 +912,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
"6ULAa0fq4jA" or // scePthreadRwlockInit
|
"6ULAa0fq4jA" or // scePthreadRwlockInit
|
||||||
"1471ajPzxh0" or // pthread_rwlock_destroy
|
"1471ajPzxh0" or // pthread_rwlock_destroy
|
||||||
"BB+kb08Tl9A" or // scePthreadRwlockDestroy
|
"BB+kb08Tl9A" or // scePthreadRwlockDestroy
|
||||||
"iGjsr1WAtI0" or // pthread_rwlock_rdlock
|
// rwlock rd/wr lock removed (can block); init/destroy/unlock stay.
|
||||||
"Ox9i0c7L5w0" or // scePthreadRwlockRdlock
|
|
||||||
"sIlRvQqsN2Y" or // pthread_rwlock_wrlock
|
|
||||||
"mqdNorrB+gI" or // scePthreadRwlockWrlock
|
|
||||||
"EgmLo6EWgso" or // pthread_rwlock_unlock
|
|
||||||
"+L98PIbGttk" or // scePthreadRwlockUnlock
|
|
||||||
"aI+OeCz8xrQ" or // scePthreadSelf
|
"aI+OeCz8xrQ" or // scePthreadSelf
|
||||||
"EotR8a3ASf4" or // pthread_self
|
"EotR8a3ASf4" or // pthread_self
|
||||||
"eoht7mQOCmo" or // scePthreadGetspecific
|
"eoht7mQOCmo" or // scePthreadGetspecific
|
||||||
@@ -972,6 +991,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;
|
||||||
@@ -1078,7 +1122,15 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsImportLoopGuardBoundary(string nid) =>
|
private static bool IsImportLoopGuardBoundary(string nid) =>
|
||||||
string.Equals(nid, "1jfXLRVzisc", StringComparison.Ordinal);
|
nid switch
|
||||||
|
{
|
||||||
|
"1jfXLRVzisc" => true, // sceKernelUsleep
|
||||||
|
"QcteRwbsnV0" => true, // usleep
|
||||||
|
"n88vx3C5nW8" => true, // gettimeofday
|
||||||
|
"Zxa0VhQVTsk" => true, // sceKernelWaitSema
|
||||||
|
"T72hz6ffq08" => true, // scePthreadYield
|
||||||
|
_ => false
|
||||||
|
};
|
||||||
|
|
||||||
private void ResetImportLoopPattern()
|
private void ResetImportLoopPattern()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,6 +25,26 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
|
|
||||||
private const int DefaultImportLoopGuardSeconds = 5;
|
private const int DefaultImportLoopGuardSeconds = 5;
|
||||||
|
|
||||||
|
private enum ImportStubKind : byte
|
||||||
|
{
|
||||||
|
Normal = 0,
|
||||||
|
BootstrapBridge = 1,
|
||||||
|
KernelDynlibDlsym = 2,
|
||||||
|
Il2CppApiLookupSymbol = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
private enum ImportStubTraceFlags : byte
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Memset = 1 << 0,
|
||||||
|
CxaAtexit = 1 << 1,
|
||||||
|
RawArgs = 1 << 2,
|
||||||
|
StackChkFail = 1 << 3,
|
||||||
|
PeriodicEvery1000 = 1 << 4,
|
||||||
|
PeriodicEvery128 = 1 << 5,
|
||||||
|
}
|
||||||
|
|
||||||
private readonly struct ImportStubEntry
|
private readonly struct ImportStubEntry
|
||||||
{
|
{
|
||||||
public ulong Address { get; }
|
public ulong Address { get; }
|
||||||
@@ -33,12 +53,36 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
|
|
||||||
public ExportedFunction? Export { get; }
|
public ExportedFunction? Export { get; }
|
||||||
|
|
||||||
|
public ImportStubKind Kind { get; }
|
||||||
|
|
||||||
|
public ImportStubTraceFlags TraceFlags { get; }
|
||||||
|
|
||||||
public ImportStubEntry(ulong address, string nid, ExportedFunction? export)
|
public ImportStubEntry(ulong address, string nid, ExportedFunction? export)
|
||||||
{
|
{
|
||||||
Address = address;
|
Address = address;
|
||||||
Nid = nid;
|
Nid = nid;
|
||||||
Export = export;
|
Export = export;
|
||||||
|
Kind = ClassifyKind(nid);
|
||||||
|
TraceFlags = ClassifyTraceFlags(nid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ImportStubKind ClassifyKind(string nid) => nid switch
|
||||||
|
{
|
||||||
|
RuntimeStubNids.BootstrapBridge => ImportStubKind.BootstrapBridge,
|
||||||
|
RuntimeStubNids.KernelDynlibDlsym or "LwG8g3niqwA" => ImportStubKind.KernelDynlibDlsym,
|
||||||
|
"r8mvOaWdi28" => ImportStubKind.Il2CppApiLookupSymbol,
|
||||||
|
_ => ImportStubKind.Normal,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static ImportStubTraceFlags ClassifyTraceFlags(string nid) => nid switch
|
||||||
|
{
|
||||||
|
"8zTFvBIAIN8" => ImportStubTraceFlags.Memset,
|
||||||
|
"tsvEmnenz48" => ImportStubTraceFlags.CxaAtexit | ImportStubTraceFlags.PeriodicEvery1000,
|
||||||
|
"bzQExy189ZI" or "8G2LB+A3rzg" => ImportStubTraceFlags.RawArgs,
|
||||||
|
"Ou3iL1abvng" => ImportStubTraceFlags.StackChkFail,
|
||||||
|
"rTXw65xmLIA" => ImportStubTraceFlags.PeriodicEvery128,
|
||||||
|
_ => ImportStubTraceFlags.None,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly record struct RecentImportTraceEntry(
|
private readonly record struct RecentImportTraceEntry(
|
||||||
@@ -125,6 +169,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
|
|
||||||
private const ulong GuestThreadRegionStride = 0x0100_0000UL;
|
private const ulong GuestThreadRegionStride = 0x0100_0000UL;
|
||||||
|
|
||||||
|
private const int GuestThreadRegionSlotCount = 256;
|
||||||
|
|
||||||
private const uint PAGE_EXECUTE_READWRITE = 64u;
|
private const uint PAGE_EXECUTE_READWRITE = 64u;
|
||||||
|
|
||||||
private const uint PAGE_READWRITE = 4u;
|
private const uint PAGE_READWRITE = 4u;
|
||||||
@@ -530,6 +576,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
// thread entering a freed stub).
|
// thread entering a freed stub).
|
||||||
private volatile bool _guestTeardownRequested;
|
private volatile bool _guestTeardownRequested;
|
||||||
|
|
||||||
|
private volatile bool _hostShutdownRequested;
|
||||||
|
|
||||||
|
private static volatile DirectExecutionBackend? _activeSessionBackend;
|
||||||
|
|
||||||
private int _readyGuestThreadCount;
|
private int _readyGuestThreadCount;
|
||||||
|
|
||||||
private readonly Dictionary<ulong, GuestThreadState> _guestThreads = new Dictionary<ulong, GuestThreadState>();
|
private readonly Dictionary<ulong, GuestThreadState> _guestThreads = new Dictionary<ulong, GuestThreadState>();
|
||||||
@@ -766,7 +816,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
|
|
||||||
private bool ActiveForcedGuestExit
|
private bool ActiveForcedGuestExit
|
||||||
{
|
{
|
||||||
get => HasActiveExecutionThread ? _activeForcedGuestExit : _forcedGuestExit;
|
get => _hostShutdownRequested ||
|
||||||
|
(HasActiveExecutionThread ? _activeForcedGuestExit : _forcedGuestExit);
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (HasActiveExecutionThread)
|
if (HasActiveExecutionThread)
|
||||||
@@ -928,7 +979,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
_importLoopGuardSeconds = GetImportLoopGuardSeconds();
|
_importLoopGuardSeconds = GetImportLoopGuardSeconds();
|
||||||
_entryReturnSentinelRip = 0uL;
|
_entryReturnSentinelRip = 0uL;
|
||||||
_forcedGuestExit = false;
|
_forcedGuestExit = false;
|
||||||
|
_hostShutdownRequested = false;
|
||||||
_guestTeardownRequested = false;
|
_guestTeardownRequested = false;
|
||||||
|
_activeSessionBackend = this;
|
||||||
|
HostSessionControl.SetShutdownHandler(RequestHostShutdown);
|
||||||
_importLoopSignatureCount = 0;
|
_importLoopSignatureCount = 0;
|
||||||
_importLoopSignatureWriteIndex = 0;
|
_importLoopSignatureWriteIndex = 0;
|
||||||
_importLoopPatternHits = 0;
|
_importLoopPatternHits = 0;
|
||||||
@@ -976,12 +1030,28 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
HostSessionControl.SetShutdownHandler(null);
|
||||||
|
if (ReferenceEquals(_activeSessionBackend, this))
|
||||||
|
{
|
||||||
|
_activeSessionBackend = null;
|
||||||
|
}
|
||||||
DrainDeferredBootstrapTraces();
|
DrainDeferredBootstrapTraces();
|
||||||
GuestThreadExecution.Scheduler = previousGuestThreadScheduler;
|
GuestThreadExecution.Scheduler = previousGuestThreadScheduler;
|
||||||
Console.Error.WriteLine("[LOADER][INFO] === Execute END (LastError: " + (LastError ?? "null") + ") ===");
|
Console.Error.WriteLine("[LOADER][INFO] === Execute END (LastError: " + (LastError ?? "null") + ") ===");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void RequestHostShutdown(string reason)
|
||||||
|
{
|
||||||
|
_hostShutdownRequested = true;
|
||||||
|
_forcedGuestExit = true;
|
||||||
|
_guestTeardownRequested = true;
|
||||||
|
LastError = string.IsNullOrWhiteSpace(reason)
|
||||||
|
? "Host shutdown requested."
|
||||||
|
: $"Host shutdown requested: {reason}";
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] {LastError}");
|
||||||
|
}
|
||||||
|
|
||||||
private bool SetupImportStubs(IReadOnlyDictionary<ulong, string> importStubs)
|
private bool SetupImportStubs(IReadOnlyDictionary<ulong, string> importStubs)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] Setting up {importStubs.Count} import stubs...");
|
Console.Error.WriteLine($"[LOADER][INFO] Setting up {importStubs.Count} import stubs...");
|
||||||
@@ -1992,9 +2062,16 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
|
|
||||||
byte* code = (byte*)ptr;
|
byte* code = (byte*)ptr;
|
||||||
int offset = 0;
|
int offset = 0;
|
||||||
EmitByte(code, ref offset, 0x48); // sub rsp, 0x20
|
// TlsGetValue returns its TLS pointer in RAX. Preserve the guest return value
|
||||||
|
// above the 32-byte Windows shadow space while keeping the call site aligned.
|
||||||
|
EmitByte(code, ref offset, 0x48); // sub rsp, 0x30
|
||||||
EmitByte(code, ref offset, 0x83);
|
EmitByte(code, ref offset, 0x83);
|
||||||
EmitByte(code, ref offset, 0xEC);
|
EmitByte(code, ref offset, 0xEC);
|
||||||
|
EmitByte(code, ref offset, 0x30);
|
||||||
|
EmitByte(code, ref offset, 0x48); // mov [rsp+0x20], rax
|
||||||
|
EmitByte(code, ref offset, 0x89);
|
||||||
|
EmitByte(code, ref offset, 0x44);
|
||||||
|
EmitByte(code, ref offset, 0x24);
|
||||||
EmitByte(code, ref offset, 0x20);
|
EmitByte(code, ref offset, 0x20);
|
||||||
EmitByte(code, ref offset, 0xB9); // mov ecx, tlsIndex
|
EmitByte(code, ref offset, 0xB9); // mov ecx, tlsIndex
|
||||||
EmitUInt32(code, ref offset, _hostRspSlotTlsIndex);
|
EmitUInt32(code, ref offset, _hostRspSlotTlsIndex);
|
||||||
@@ -2004,13 +2081,21 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
offset += sizeof(ulong);
|
offset += sizeof(ulong);
|
||||||
EmitByte(code, ref offset, 0xFF); // call rax
|
EmitByte(code, ref offset, 0xFF); // call rax
|
||||||
EmitByte(code, ref offset, 0xD0);
|
EmitByte(code, ref offset, 0xD0);
|
||||||
EmitByte(code, ref offset, 0x48); // add rsp, 0x20
|
EmitByte(code, ref offset, 0x49); // mov r11, rax
|
||||||
|
EmitByte(code, ref offset, 0x89);
|
||||||
|
EmitByte(code, ref offset, 0xC3);
|
||||||
|
EmitByte(code, ref offset, 0x48); // mov rax, [rsp+0x20]
|
||||||
|
EmitByte(code, ref offset, 0x8B);
|
||||||
|
EmitByte(code, ref offset, 0x44);
|
||||||
|
EmitByte(code, ref offset, 0x24);
|
||||||
|
EmitByte(code, ref offset, 0x20);
|
||||||
|
EmitByte(code, ref offset, 0x48); // add rsp, 0x30
|
||||||
EmitByte(code, ref offset, 0x83);
|
EmitByte(code, ref offset, 0x83);
|
||||||
EmitByte(code, ref offset, 0xC4);
|
EmitByte(code, ref offset, 0xC4);
|
||||||
EmitByte(code, ref offset, 0x20);
|
EmitByte(code, ref offset, 0x30);
|
||||||
EmitByte(code, ref offset, 0x48); // mov rsp, [rax]
|
EmitByte(code, ref offset, 0x49); // mov rsp, [r11]
|
||||||
EmitByte(code, ref offset, 0x8B);
|
EmitByte(code, ref offset, 0x8B);
|
||||||
EmitByte(code, ref offset, 0x20);
|
EmitByte(code, ref offset, 0x23);
|
||||||
EmitHostNonvolatileXmmRestore(code, ref offset);
|
EmitHostNonvolatileXmmRestore(code, ref offset);
|
||||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5F);
|
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5F);
|
||||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5E);
|
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5E);
|
||||||
@@ -2742,7 +2827,21 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
if (hostThread is not null &&
|
if (hostThread is not null &&
|
||||||
!ReferenceEquals(hostThread, Thread.CurrentThread))
|
!ReferenceEquals(hostThread, Thread.CurrentThread))
|
||||||
{
|
{
|
||||||
hostThread.Join(1);
|
// The handle is published before the host thread starts.
|
||||||
|
if ((hostThread.ThreadState & System.Threading.ThreadState.Unstarted) != 0)
|
||||||
|
{
|
||||||
|
Thread.Sleep(1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
hostThread.Join(1);
|
||||||
|
}
|
||||||
|
catch (ThreadStateException)
|
||||||
|
{
|
||||||
|
Thread.Sleep(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -2767,11 +2866,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_guestThreadPumpDepth != 0)
|
if (Interlocked.CompareExchange(ref _guestThreadPumpDepth, 1, 0) != 0)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_guestThreadPumpDepth++;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
for (int i = 0; i < 8; i++)
|
for (int i = 0; i < 8; i++)
|
||||||
@@ -2817,7 +2915,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_guestThreadPumpDepth--;
|
Volatile.Write(ref _guestThreadPumpDepth, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2860,9 +2958,18 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wakeCount != 0 && _logGuestThreads)
|
if (wakeCount != 0)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] guest_threads.wake key={wakeKey} count={wakeCount}");
|
if (_logGuestThreads)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] guest_threads.wake key={wakeKey} count={wakeCount}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pump or the readied thread waits for an import dispatch that never comes.
|
||||||
|
if (_cpuContext is { } wakeContext)
|
||||||
|
{
|
||||||
|
Pump(wakeContext, "wake");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return wakeCount;
|
return wakeCount;
|
||||||
@@ -3423,7 +3530,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
out ulong mappedBase,
|
out ulong mappedBase,
|
||||||
out string? error)
|
out string? error)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < 64; i++)
|
for (int i = 0; i < GuestThreadRegionSlotCount; i++)
|
||||||
{
|
{
|
||||||
var candidateBase = baseAddress - ((ulong)i * GuestThreadRegionStride);
|
var candidateBase = baseAddress - ((ulong)i * GuestThreadRegionStride);
|
||||||
if (!IsGuestThreadRegionFree(virtualMemory, candidateBase, size))
|
if (!IsGuestThreadRegionFree(virtualMemory, candidateBase, size))
|
||||||
@@ -3457,7 +3564,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
out ulong tlsBase,
|
out ulong tlsBase,
|
||||||
out string? error)
|
out string? error)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < 64; i++)
|
for (int i = 0; i < GuestThreadRegionSlotCount; i++)
|
||||||
{
|
{
|
||||||
var candidateBase = GuestThreadTlsBaseAddress - ((ulong)i * GuestThreadRegionStride);
|
var candidateBase = GuestThreadTlsBaseAddress - ((ulong)i * GuestThreadRegionStride);
|
||||||
var mappedBase = candidateBase - GuestThreadTlsPrefixSize;
|
var mappedBase = candidateBase - GuestThreadTlsPrefixSize;
|
||||||
@@ -4342,7 +4449,9 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_CPU_TRAP;
|
result = OrbisGen2Result.ORBIS_GEN2_ERROR_CPU_TRAP;
|
||||||
if (string.IsNullOrEmpty(LastError))
|
if (string.IsNullOrEmpty(LastError))
|
||||||
{
|
{
|
||||||
LastError = "Detected repeating import loop and forced guest unwind to host.";
|
LastError = _hostShutdownRequested
|
||||||
|
? "Host shutdown requested."
|
||||||
|
: "Detected repeating import loop and forced guest unwind to host.";
|
||||||
}
|
}
|
||||||
Console.Error.WriteLine("[LOADER][ERROR] " + LastError);
|
Console.Error.WriteLine("[LOADER][ERROR] " + LastError);
|
||||||
RequestGuestThreadTeardown(3000);
|
RequestGuestThreadTeardown(3000);
|
||||||
@@ -4407,6 +4516,26 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_stallWatchdogStop = false;
|
_stallWatchdogStop = false;
|
||||||
|
|
||||||
|
// Drives woken threads when every guest thread is parked (nothing dispatches then).
|
||||||
|
var dispatcherThread = new Thread(new ThreadStart(delegate
|
||||||
|
{
|
||||||
|
while (!_stallWatchdogStop)
|
||||||
|
{
|
||||||
|
Thread.Sleep(1);
|
||||||
|
WakeExpiredBlockedGuestThreads();
|
||||||
|
if (Volatile.Read(ref _readyGuestThreadCount) > 0 && _cpuContext is { } dispatchContext)
|
||||||
|
{
|
||||||
|
Pump(dispatchContext, "dispatcher");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
{
|
||||||
|
IsBackground = true,
|
||||||
|
Name = "SharpEmu-GuestThreadDispatcher"
|
||||||
|
};
|
||||||
|
dispatcherThread.Start();
|
||||||
|
|
||||||
long num = (long)((double)stallWatchdogSeconds * Stopwatch.Frequency);
|
long num = (long)((double)stallWatchdogSeconds * Stopwatch.Frequency);
|
||||||
_stallWatchdogThread = new Thread(new ThreadStart(delegate
|
_stallWatchdogThread = new Thread(new ThreadStart(delegate
|
||||||
{
|
{
|
||||||
@@ -4435,6 +4564,15 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
MarkExecutionProgress();
|
MarkExecutionProgress();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (IsExpectedBlockingImportStall(out var blockingNid, out var blockingName))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] No import progress for {stallWatchdogSeconds}s while waiting in {blockingName} ({blockingNid}); continuing.");
|
||||||
|
LogStallWatchdogSnapshot();
|
||||||
|
Console.Error.Flush();
|
||||||
|
MarkExecutionProgress();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (Interlocked.Exchange(ref _stallWatchdogTriggered, 1) != 0)
|
if (Interlocked.Exchange(ref _stallWatchdogTriggered, 1) != 0)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@@ -4470,6 +4608,38 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A thread parked in a blocking wait is idle by design, not stalled.
|
||||||
|
private bool IsExpectedBlockingImportStall(out string nid, out string name)
|
||||||
|
{
|
||||||
|
nid = string.Empty;
|
||||||
|
name = string.Empty;
|
||||||
|
var cpuContext = _cpuContext;
|
||||||
|
if (cpuContext is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var importAddress = cpuContext.Rip & 0xFFFFFFFFFFFFFFF0uL;
|
||||||
|
foreach (var entry in _importEntries)
|
||||||
|
{
|
||||||
|
if (entry.Address != importAddress)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
nid = entry.Nid;
|
||||||
|
name = _moduleManager.TryGetExport(nid, out var export)
|
||||||
|
? $"{export.LibraryName}:{export.Name}"
|
||||||
|
: nid;
|
||||||
|
return nid is
|
||||||
|
"Op8TBGY5KHg" or // pthread_cond_wait
|
||||||
|
"27bAgiJmOh0" or // pthread_cond_timedwait
|
||||||
|
"fzyMKs9kim0"; // sceKernelWaitEqueue
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private void StopStallWatchdog()
|
private void StopStallWatchdog()
|
||||||
{
|
{
|
||||||
_stallWatchdogStop = true;
|
_stallWatchdogStop = true;
|
||||||
|
|||||||
@@ -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 &&
|
||||||
|
|||||||
@@ -550,6 +550,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;
|
||||||
|
|||||||
@@ -127,6 +127,34 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<!-- Top-level page switcher (Library / Options): plain transparent
|
||||||
|
buttons, not TabItem, so there is no Fluent selected-tab underline.
|
||||||
|
The active page is conveyed by brightness alone; LB/RB gamepad
|
||||||
|
hints flank the pair. -->
|
||||||
|
<Style Selector="Button.segment">
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<Setter Property="BorderThickness" Value="0" />
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource MutedBrush}" />
|
||||||
|
<Setter Property="FontSize" Value="22" />
|
||||||
|
<Setter Property="FontWeight" Value="Bold" />
|
||||||
|
<Setter Property="Padding" Value="6,4" />
|
||||||
|
<Setter Property="CornerRadius" Value="8" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button.segment:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button.segment.active">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Gamepad shoulder-button hint chip (LB/RB, L1/R1). -->
|
||||||
|
<Style Selector="Border.padHint">
|
||||||
|
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
|
||||||
|
<Setter Property="CornerRadius" Value="6" />
|
||||||
|
<Setter Property="Padding" Value="8,3" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
<Style Selector="ContextMenu">
|
<Style Selector="ContextMenu">
|
||||||
<Setter Property="Background" Value="{StaticResource CardBrush}" />
|
<Setter Property="Background" Value="{StaticResource CardBrush}" />
|
||||||
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
|
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
|
||||||
|
|||||||
@@ -27,8 +27,10 @@ public sealed class ConsoleWindow : Window
|
|||||||
Action clear,
|
Action clear,
|
||||||
bool autoScroll)
|
bool autoScroll)
|
||||||
{
|
{
|
||||||
|
var loc = Localization.Instance;
|
||||||
|
|
||||||
_sourceLines = lines;
|
_sourceLines = lines;
|
||||||
Title = "SharpEmu Console";
|
Title = loc.Get("Console.WindowTitle");
|
||||||
Width = 980;
|
Width = 980;
|
||||||
Height = 620;
|
Height = 620;
|
||||||
MinWidth = 520;
|
MinWidth = 520;
|
||||||
@@ -36,10 +38,15 @@ public sealed class ConsoleWindow : Window
|
|||||||
Background = new SolidColorBrush(Color.Parse("#0D1017"));
|
Background = new SolidColorBrush(Color.Parse("#0D1017"));
|
||||||
Icon = new WindowIcon(AssetLoader.Open(new Uri("avares://SharpEmu.GUI/Assets/SharpEmu.ico")));
|
Icon = new WindowIcon(AssetLoader.Open(new Uri("avares://SharpEmu.GUI/Assets/SharpEmu.ico")));
|
||||||
|
|
||||||
_searchBox = new TextBox { Watermark = "Search...", Width = 320, Margin = new Thickness(0, 0, 12, 0) };
|
_searchBox = new TextBox
|
||||||
|
{
|
||||||
|
Watermark = loc.Get("Console.SearchWatermark"),
|
||||||
|
Width = 320,
|
||||||
|
Margin = new Thickness(0, 0, 12, 0),
|
||||||
|
};
|
||||||
_autoScrollCheck = new CheckBox
|
_autoScrollCheck = new CheckBox
|
||||||
{
|
{
|
||||||
Content = "Auto-scroll",
|
Content = loc.Get("Console.AutoScroll"),
|
||||||
IsChecked = autoScroll,
|
IsChecked = autoScroll,
|
||||||
FontSize = 12,
|
FontSize = 12,
|
||||||
Margin = new Thickness(0, 0, 12, 0),
|
Margin = new Thickness(0, 0, 12, 0),
|
||||||
@@ -48,11 +55,16 @@ public sealed class ConsoleWindow : Window
|
|||||||
var copyButton = new Button
|
var copyButton = new Button
|
||||||
{
|
{
|
||||||
Classes = { "ghost" },
|
Classes = { "ghost" },
|
||||||
Content = "Copy",
|
Content = loc.Get("Console.Copy"),
|
||||||
Padding = new Thickness(10, 4),
|
Padding = new Thickness(10, 4),
|
||||||
Margin = new Thickness(0, 0, 8, 0),
|
Margin = new Thickness(0, 0, 8, 0),
|
||||||
};
|
};
|
||||||
var clearButton = new Button { Classes = { "ghost" }, Content = "Clear", Padding = new Thickness(10, 4) };
|
var clearButton = new Button
|
||||||
|
{
|
||||||
|
Classes = { "ghost" },
|
||||||
|
Content = loc.Get("Console.Clear"),
|
||||||
|
Padding = new Thickness(10, 4),
|
||||||
|
};
|
||||||
copyButton.Click += async (_, _) => await CopyAsync();
|
copyButton.Click += async (_, _) => await CopyAsync();
|
||||||
clearButton.Click += (_, _) => clear();
|
clearButton.Click += (_, _) => clear();
|
||||||
_searchBox.TextChanged += (_, _) => RefreshVisibleLines();
|
_searchBox.TextChanged += (_, _) => RefreshVisibleLines();
|
||||||
@@ -87,7 +99,7 @@ public sealed class ConsoleWindow : Window
|
|||||||
new TextBlock
|
new TextBlock
|
||||||
{
|
{
|
||||||
Classes = { "sectionTitle" },
|
Classes = { "sectionTitle" },
|
||||||
Text = "CONSOLE",
|
Text = loc.Get("Console.Title"),
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
},
|
},
|
||||||
_searchBox.WithGridColumn(1),
|
_searchBox.WithGridColumn(1),
|
||||||
|
|||||||
@@ -29,10 +29,12 @@ public sealed class GameEntry : INotifyPropertyChanged
|
|||||||
private long _sizeBytes;
|
private long _sizeBytes;
|
||||||
|
|
||||||
public GameEntry(
|
public GameEntry(
|
||||||
string name, string? titleId, string path, long sizeBytes, string? coverPath, string? backgroundPath)
|
string name, string? titleId, string? version, string path, long sizeBytes,
|
||||||
|
string? coverPath, string? backgroundPath)
|
||||||
{
|
{
|
||||||
Name = name;
|
Name = name;
|
||||||
TitleId = titleId;
|
TitleId = titleId;
|
||||||
|
Version = version;
|
||||||
Path = path;
|
Path = path;
|
||||||
_sizeBytes = sizeBytes;
|
_sizeBytes = sizeBytes;
|
||||||
CoverPath = coverPath;
|
CoverPath = coverPath;
|
||||||
@@ -46,6 +48,9 @@ public sealed class GameEntry : INotifyPropertyChanged
|
|||||||
|
|
||||||
public string? TitleId { get; }
|
public string? TitleId { get; }
|
||||||
|
|
||||||
|
/// <summary>Content version from sce_sys/param.json, e.g. "01.000.000".</summary>
|
||||||
|
public string? Version { get; }
|
||||||
|
|
||||||
public string Path { get; }
|
public string Path { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -65,7 +70,7 @@ public sealed class GameEntry : INotifyPropertyChanged
|
|||||||
|
|
||||||
_sizeBytes = value;
|
_sizeBytes = value;
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SizeBytes)));
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SizeBytes)));
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Detail)));
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SizeText)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,9 +111,15 @@ public sealed class GameEntry : INotifyPropertyChanged
|
|||||||
|
|
||||||
public bool HasCover => _cover is not null;
|
public bool HasCover => _cover is not null;
|
||||||
|
|
||||||
public string Detail => TitleId is not null
|
public bool HasTitleId => TitleId is not null;
|
||||||
? $"{TitleId} • {FormatSize(SizeBytes)}"
|
|
||||||
: FormatSize(SizeBytes);
|
/// <summary>Badge text shown in the launch bar, e.g. "v01.000.000".</summary>
|
||||||
|
public string? VersionText => Version is null ? null : $"v{Version}";
|
||||||
|
|
||||||
|
public bool HasVersion => Version is not null;
|
||||||
|
|
||||||
|
/// <summary>Formatted install size badge shown in the launch bar.</summary>
|
||||||
|
public string SizeText => FormatSize(SizeBytes);
|
||||||
|
|
||||||
private static string ComputeInitials(string name)
|
private static string ComputeInitials(string name)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ public sealed class GuiSettings
|
|||||||
|
|
||||||
public string? EmulatorPath { get; set; }
|
public string? EmulatorPath { get; set; }
|
||||||
|
|
||||||
|
/// <summary>UI language, matching a file code under Languages/ (e.g. "en", "tr").</summary>
|
||||||
|
public string Language { get; set; } = "en";
|
||||||
|
|
||||||
/// <summary>Publish launcher/game status to Discord Rich Presence.</summary>
|
/// <summary>Publish launcher/game status to Discord Rich Presence.</summary>
|
||||||
public bool DiscordRichPresence { get; set; } = true;
|
public bool DiscordRichPresence { get; set; } = true;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"_languageName": "العربية",
|
||||||
|
|
||||||
|
"Page.Library": "المكتبة",
|
||||||
|
"Page.Options": "الخيارات",
|
||||||
|
"Page.GameCount.One": "لعبة واحدة",
|
||||||
|
"Page.GameCount.Other": "{0} لعبة",
|
||||||
|
|
||||||
|
"Library.SearchWatermark": "ابحث في المكتبة...",
|
||||||
|
"Library.AddFolder": "+ إضافة مجلد",
|
||||||
|
"Library.Rescan": "⟳ إعادة الفحص",
|
||||||
|
"Library.OpenFile": "فتح ملف...",
|
||||||
|
|
||||||
|
"Library.Context.Launch": "تشغيل",
|
||||||
|
"Library.Context.OpenFolder": "فتح مجلد اللعبة",
|
||||||
|
"Library.Context.CopyPath": "نسخ المسار",
|
||||||
|
"Library.Context.CopyTitleId": "نسخ معرف العنوان",
|
||||||
|
"Library.Context.Remove": "إزالة من المكتبة",
|
||||||
|
|
||||||
|
"Library.Empty.Title": "مكتبتك فارغة",
|
||||||
|
"Library.Empty.Hint": "أضف مجلداً يحتوي على ألعابك للبدء.",
|
||||||
|
"Library.Empty.SearchTitle": "لا توجد ألعاب تطابق بحثك",
|
||||||
|
"Library.Empty.SearchHint": "لا شيء في المكتبة يطابق “{0}”.",
|
||||||
|
"Library.Empty.AddFolder": "+ إضافة مجلد ألعاب",
|
||||||
|
|
||||||
|
"Library.Loading": "جارٍ تحميل المكتبة...",
|
||||||
|
|
||||||
|
"Options.General": "عام",
|
||||||
|
"Options.Section.Emulation": "المحاكاة",
|
||||||
|
"Options.Section.Logging": "التسجيل",
|
||||||
|
"Options.Section.Launcher": "المُشغِّل",
|
||||||
|
|
||||||
|
"Options.CpuEngine.Label": "محرك المعالج",
|
||||||
|
"Options.CpuEngine.Desc": "محرك التنفيذ المستخدم لتشغيل كود اللعبة.",
|
||||||
|
"Options.CpuEngine.Native": "أصلي",
|
||||||
|
|
||||||
|
"Options.Strict.Label": "ربط صارم للمكتبات الديناميكية (dynlib)",
|
||||||
|
"Options.Strict.Desc": "إفشال التشغيل عندما يتعذر التعرف على رمز مستورد.",
|
||||||
|
|
||||||
|
"Options.LogLevel.Label": "مستوى التسجيل",
|
||||||
|
"Options.LogLevel.Desc": "مدى تفصيل مخرجات نافذة سجلات المحاكي.",
|
||||||
|
"Options.LogLevel.Trace": "تتبع",
|
||||||
|
"Options.LogLevel.Debug": "تصحيح الأخطاء",
|
||||||
|
"Options.LogLevel.Info": "معلومات",
|
||||||
|
"Options.LogLevel.Warning": "تحذير",
|
||||||
|
"Options.LogLevel.Error": "خطأ",
|
||||||
|
"Options.LogLevel.Critical": "حرج",
|
||||||
|
|
||||||
|
"Options.TraceImports.Label": "حد تتبع الاستيراد",
|
||||||
|
"Options.TraceImports.Desc": "تتبع أول N استيراد لكل وحدة (0 = إيقاف).",
|
||||||
|
|
||||||
|
"Options.LogToFile.Label": "التسجيل في ملف",
|
||||||
|
"Options.LogToFile.Desc": "نسخ مخرجات المحاكي إلى ملف سجل.",
|
||||||
|
|
||||||
|
"Options.LogFilePath.Label": "مسار ملف السجل",
|
||||||
|
"Options.LogFilePath.Default": "لا يوجد مسار مخصص — تُحفظ السجلات في user/logs بجوار المحاكي.",
|
||||||
|
"Options.LogFilePath.Select": "تحديد...",
|
||||||
|
|
||||||
|
"Options.OverrideLogFile.Label": "الكتابة فوق ملف السجل",
|
||||||
|
"Options.OverrideLogFile.Desc": "استخدام مسار الملف الدقيق بدلاً من إلحاق معرف العنوان والطابع الزمني.",
|
||||||
|
|
||||||
|
"Options.TitleMusic.Label": "موسيقى اللعبة",
|
||||||
|
"Options.TitleMusic.Desc": "تكرار موسيقى المعاينة للعبة المحددة في المكتبة.",
|
||||||
|
|
||||||
|
"Options.Discord.Label": "حالة دسكورد",
|
||||||
|
"Options.Discord.Desc": "إظهار اللعبة قيد التشغيل في ملفك الشخصي على دسكورد.",
|
||||||
|
|
||||||
|
"Options.Language.Label": "لغة المحاكي",
|
||||||
|
"Options.Language.Desc": "اللغة المستخدمة في جميع أنحاء المشغل. تُطبق فوراً.",
|
||||||
|
|
||||||
|
"Common.On": "تشغيل",
|
||||||
|
"Common.Off": "إيقاف",
|
||||||
|
|
||||||
|
"Console.Title": "نافذة السجلات",
|
||||||
|
"Console.SearchWatermark": "بحث...",
|
||||||
|
"Console.AutoScroll": "تمرير تلقائي",
|
||||||
|
"Console.Split": "تقسيم",
|
||||||
|
"Console.Copy": "نسخ",
|
||||||
|
"Console.Clear": "مسح",
|
||||||
|
"Console.WindowTitle": "نافذة سجلات SharpEmu",
|
||||||
|
|
||||||
|
"Launch.NoGameSelected": "لم تُحدد أي لعبة",
|
||||||
|
"Launch.NoGameHint": "اختر لعبة من المكتبة، أو افتح ملف eboot.bin مباشرة.",
|
||||||
|
"Launch.Idle": "خامل",
|
||||||
|
"Launch.Console": "≡ نافذة السجلات",
|
||||||
|
"Launch.Launch": "▶ تشغيل",
|
||||||
|
"Launch.Stop": "■ إيقاف",
|
||||||
|
"Launch.Running": "قيد التشغيل — {0}",
|
||||||
|
"Launch.Stopping": "جارٍ الإيقاف...",
|
||||||
|
"Launch.Exited": "انتهى برمز {0} ({1})",
|
||||||
|
"Launch.ExeNotFound": "لم يُعثر على الملف التنفيذي SharpEmu. ابنِ مشروع SharpEmu.CLI أولاً (dotnet build).",
|
||||||
|
"Launch.LogFile": "ملف السجل: {0}",
|
||||||
|
"Launch.Command": "$ SharpEmu {0}",
|
||||||
|
"Launch.StartFailed": "فشل بدء تشغيل المحاكي: {0}",
|
||||||
|
"Launch.ProcessExited": "انتهت العملية برمز {0} ({1}).",
|
||||||
|
|
||||||
|
"Exit.Ok": "موافق",
|
||||||
|
"Exit.InvalidArguments": "معطيات غير صالحة",
|
||||||
|
"Exit.EbootNotFound": "لم يُعثر على eboot",
|
||||||
|
"Exit.RuntimeException": "استثناء وقت التشغيل",
|
||||||
|
"Exit.EmulationError": "خطأ في المحاكاة",
|
||||||
|
"Exit.Unknown": "غير معروف",
|
||||||
|
|
||||||
|
"Status.EmulatorLocating": "المحاكي: جارٍ تحديد الموقع...",
|
||||||
|
"Status.EmulatorPath": "المحاكي: {0}",
|
||||||
|
"Status.EmulatorNotFound": "المحاكي: لم يُعثر على الملف التنفيذي SharpEmu — ابنِ SharpEmu.CLI أولاً.",
|
||||||
|
"Status.ScanningLibrary": "جارٍ فحص المكتبة...",
|
||||||
|
"Status.AddFolderPrompt": "أضف مجلد ألعاب لملء المكتبة.",
|
||||||
|
"Status.LibraryScanned": "فُحصت المكتبة: {0} لعبة في {1} مجلد.",
|
||||||
|
"Status.CouldNotOpenFolder": "تعذر فتح المجلد: {0}",
|
||||||
|
"Status.CopiedToClipboard": "نُسخ {0} إلى الحافظة.",
|
||||||
|
"Status.RemovedFromLibrary": "أُزيل “{0}” من المكتبة. أعد إضافة مجلده لاستعادته.",
|
||||||
|
"Status.Running": "جارٍ تشغيل {0}",
|
||||||
|
"Status.Stopping": "جارٍ الإيقاف...",
|
||||||
|
"Status.Idle": "خامل",
|
||||||
|
|
||||||
|
"Clipboard.Path": "المسار",
|
||||||
|
"Clipboard.TitleId": "معرف العنوان",
|
||||||
|
|
||||||
|
"Discord.Playing": "يلعب {0}",
|
||||||
|
"Discord.Browsing": "يتصفح المكتبة",
|
||||||
|
|
||||||
|
"Dialog.ChooseGameFolder": "اختر مجلداً يحتوي على ألعاب",
|
||||||
|
"Dialog.OpenExecutable": "افتح ملفاً تنفيذياً لتشغيله",
|
||||||
|
"Dialog.PsExecutables": "ملفات PS التنفيذية",
|
||||||
|
"Dialog.SaveLogFile": "حدد مكان حفظ ملف السجل",
|
||||||
|
"Dialog.PlainTextFiles": "ملفات نصية عادية",
|
||||||
|
"Dialog.LogFiles": "ملفات السجل"
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"_languageName": "Português (Brasil)",
|
||||||
|
|
||||||
|
"Page.Library": "Biblioteca",
|
||||||
|
"Page.Options": "Opções",
|
||||||
|
"Page.GameCount.One": "1 Jogo",
|
||||||
|
"Page.GameCount.Other": "{0} jogos",
|
||||||
|
|
||||||
|
"Library.SearchWatermark": "Pesquisar na biblioteca…",
|
||||||
|
"Library.AddFolder": "+ Adicionar pasta",
|
||||||
|
"Library.Rescan": "⟳ Atualizar biblioteca",
|
||||||
|
"Library.OpenFile": "Abrir arquivo…",
|
||||||
|
|
||||||
|
"Library.Context.Launch": "Jogar",
|
||||||
|
"Library.Context.OpenFolder": "Abrir pasta do jogo",
|
||||||
|
"Library.Context.CopyPath": "Copiar o caminho",
|
||||||
|
"Library.Context.CopyTitleId": "Copiar ID do título",
|
||||||
|
"Library.Context.Remove": "Remover da biblioteca",
|
||||||
|
|
||||||
|
"Library.Empty.Title": "Sua biblioteca está vazia",
|
||||||
|
"Library.Empty.Hint": "Adicione uma pasta contendo seus jogos para começar.",
|
||||||
|
"Library.Empty.SearchTitle": "Nenhum jogo corresponde à sua busca",
|
||||||
|
"Library.Empty.SearchHint": "Nada na biblioteca corresponde a “{0}”.",
|
||||||
|
"Library.Empty.AddFolder": "+ Adicionar pasta do jogo",
|
||||||
|
|
||||||
|
"Library.Loading": "Carregando biblioteca…",
|
||||||
|
|
||||||
|
"Options.General": "Opções Gerais",
|
||||||
|
"Options.Section.Emulation": "EMULAÇÃO",
|
||||||
|
"Options.Section.Logging": "LOGS",
|
||||||
|
"Options.Section.Launcher": "INICIALIZADOR",
|
||||||
|
|
||||||
|
"Options.CpuEngine.Label": "Motor da CPU",
|
||||||
|
"Options.CpuEngine.Desc": "Motor de execução usado para executar o código do jogo.",
|
||||||
|
"Options.CpuEngine.Native": "Nativo",
|
||||||
|
|
||||||
|
"Options.Strict.Label": "Resolução estrita de bibliotecas dinâmicas",
|
||||||
|
"Options.Strict.Desc": "Interrompe a inicialização caso um símbolo importado não possa ser vinculado.",
|
||||||
|
|
||||||
|
"Options.LogLevel.Label": "Nível de log",
|
||||||
|
"Options.LogLevel.Desc": "Nível de detalhamento das mensagens exibidas no console do emulador.",
|
||||||
|
"Options.LogLevel.Trace": "Trace",
|
||||||
|
"Options.LogLevel.Debug": "Debug",
|
||||||
|
"Options.LogLevel.Info": "Info",
|
||||||
|
"Options.LogLevel.Warning": "Warning",
|
||||||
|
"Options.LogLevel.Error": "Error",
|
||||||
|
"Options.LogLevel.Critical": "Critical",
|
||||||
|
|
||||||
|
"Options.TraceImports.Label": "Limite de rastreamento de importações",
|
||||||
|
"Options.TraceImports.Desc": "Rastreia as primeiras N importações de cada módulo (0 = desativado).",
|
||||||
|
|
||||||
|
"Options.LogToFile.Label": "Salvar log em arquivo",
|
||||||
|
"Options.LogToFile.Desc": "Copia a saída do emulador para um arquivo de log.",
|
||||||
|
|
||||||
|
"Options.LogFilePath.Label": "Caminho do arquivo de log",
|
||||||
|
"Options.LogFilePath.Default": "Nenhum caminho definido — logs vão para user/logs na pasta do emulador.",
|
||||||
|
"Options.LogFilePath.Select": "Selecionar…",
|
||||||
|
|
||||||
|
"Options.OverrideLogFile.Label": "Sobrescrever arquivo de log",
|
||||||
|
"Options.OverrideLogFile.Desc": "Use o caminho exato do arquivo em vez de adicionar o ID do título e o log de data e hora.",
|
||||||
|
|
||||||
|
"Options.TitleMusic.Label": "Música de prévia",
|
||||||
|
"Options.TitleMusic.Desc": "Reproduz em loop a música de prévia do jogo selecionado na biblioteca.",
|
||||||
|
|
||||||
|
"Options.Discord.Label": "Status do Discord",
|
||||||
|
"Options.Discord.Desc": "Exibir o jogo em execução no seu perfil do Discord.",
|
||||||
|
|
||||||
|
"Options.Language.Label": "Idioma do emulador",
|
||||||
|
"Options.Language.Desc": "Idioma usado em toda a interface do emulador. A alteração é aplicada imediatamente.",
|
||||||
|
|
||||||
|
"Common.On": "Ativado",
|
||||||
|
"Common.Off": "Desativado",
|
||||||
|
|
||||||
|
"Console.Title": "CONSOLE",
|
||||||
|
"Console.SearchWatermark": "Pesquisar...",
|
||||||
|
"Console.AutoScroll": "Rolagem automática",
|
||||||
|
"Console.Split": "Recortar",
|
||||||
|
"Console.Copy": "Copiar",
|
||||||
|
"Console.Clear": "Limpar",
|
||||||
|
"Console.WindowTitle": "Console do SharpEmu",
|
||||||
|
|
||||||
|
"Launch.NoGameSelected": "Nenhum jogo selecionado",
|
||||||
|
"Launch.NoGameHint": "Selecione um jogo na biblioteca ou abra um arquivo eboot.bin diretamente.",
|
||||||
|
"Launch.Idle": "Ocioso",
|
||||||
|
"Launch.Console": "≡ Console",
|
||||||
|
"Launch.Launch": "▶ Iniciar",
|
||||||
|
"Launch.Stop": "■ Parar",
|
||||||
|
"Launch.Running": "Em execução — {0}",
|
||||||
|
"Launch.Stopping": "Encerrando…",
|
||||||
|
"Launch.Exited": "Encerrado com código {0} ({1})",
|
||||||
|
"Launch.ExeNotFound": "Executável do SharpEmu não encontrado. Compile primeiro o projeto SharpEmu.CLI (dotnet build).",
|
||||||
|
"Launch.LogFile": "Arquivo de log: {0}",
|
||||||
|
"Launch.Command": "$ SharpEmu {0}",
|
||||||
|
"Launch.StartFailed": "Falha ao iniciar o emulador: {0}",
|
||||||
|
"Launch.ProcessExited": "O processo foi encerrado com código {0} ({1}).",
|
||||||
|
|
||||||
|
"Exit.Ok": "OK",
|
||||||
|
"Exit.InvalidArguments": "argumentos inválidos",
|
||||||
|
"Exit.EbootNotFound": "eboot.bin não encontrado",
|
||||||
|
"Exit.RuntimeException": "exceção em tempo de execução",
|
||||||
|
"Exit.EmulationError": "erro de emulação",
|
||||||
|
"Exit.Unknown": "desconhecido",
|
||||||
|
|
||||||
|
"Status.EmulatorLocating": "Emulador: localizando…",
|
||||||
|
"Status.EmulatorPath": "Emulador: {0}",
|
||||||
|
"Status.EmulatorNotFound": "Emulador: executável do SharpEmu não encontrado — compile o SharpEmu.CLI primeiro.",
|
||||||
|
"Status.ScanningLibrary": "Verificando biblioteca…",
|
||||||
|
"Status.AddFolderPrompt": "Adicione uma pasta de jogos para preencher a biblioteca.",
|
||||||
|
"Status.LibraryScanned": "Biblioteca verificada: {0} jogo(s) em {1} pasta(s).",
|
||||||
|
"Status.CouldNotOpenFolder": "Não foi possível abrir a pasta: {0}",
|
||||||
|
"Status.CopiedToClipboard": "{0} copiado para a área de transferência.",
|
||||||
|
"Status.RemovedFromLibrary": "“{0}” removido da biblioteca. Adicione novamente sua pasta para restaurá-lo.",
|
||||||
|
"Status.Running": "Executando {0}",
|
||||||
|
"Status.Stopping": "Encerrando…",
|
||||||
|
"Status.Idle": "Ocioso",
|
||||||
|
|
||||||
|
"Clipboard.Path": "Caminho",
|
||||||
|
"Clipboard.TitleId": "ID do título",
|
||||||
|
|
||||||
|
"Discord.Playing": "Jogando {0}",
|
||||||
|
"Discord.Browsing": "Navegando pela biblioteca",
|
||||||
|
|
||||||
|
"Dialog.ChooseGameFolder": "Escolha uma pasta contendo jogos",
|
||||||
|
"Dialog.OpenExecutable": "Abrir um executável para iniciar",
|
||||||
|
"Dialog.PsExecutables": "Executáveis de PS",
|
||||||
|
"Dialog.SaveLogFile": "Selecione onde salvar o arquivo de log",
|
||||||
|
"Dialog.PlainTextFiles": "Arquivos de texto simples",
|
||||||
|
"Dialog.LogFiles": "Arquivos de log"
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"_languageName": "Deutsch",
|
||||||
|
|
||||||
|
"Page.Library": "Bibliothek",
|
||||||
|
"Page.Options": "Optionen",
|
||||||
|
"Page.GameCount.One": "1 Spiel",
|
||||||
|
"Page.GameCount.Other": "{0} Spiele",
|
||||||
|
|
||||||
|
"Library.SearchWatermark": "Bibliothek durchsuchen…",
|
||||||
|
"Library.AddFolder": "+ Spielordner hinzufügen",
|
||||||
|
"Library.Rescan": "⟳ Neu scannen",
|
||||||
|
"Library.OpenFile": "Datei öffnen…",
|
||||||
|
|
||||||
|
"Library.Context.Launch": "Starten",
|
||||||
|
"Library.Context.OpenFolder": "Spielordner öffnen",
|
||||||
|
"Library.Context.CopyPath": "Pfad kopieren",
|
||||||
|
"Library.Context.CopyTitleId": "Title ID kopieren",
|
||||||
|
"Library.Context.Remove": "Aus Bibliothek entfernen",
|
||||||
|
|
||||||
|
"Library.Empty.Title": "Deine Bibliothek ist leer",
|
||||||
|
"Library.Empty.Hint": "Füge einen Ordner mit deinen Spielen hinzu, um zu beginnen.",
|
||||||
|
"Library.Empty.SearchTitle": "Keine Spiele gefunden",
|
||||||
|
"Library.Empty.SearchHint": "Nichts in der Bibliothek entspricht “{0}”.",
|
||||||
|
"Library.Empty.AddFolder": "+ Spielordner hinzufügen",
|
||||||
|
|
||||||
|
"Library.Loading": "Bibliothek wird geladen…",
|
||||||
|
|
||||||
|
"Options.General": "Allgemein",
|
||||||
|
"Options.Section.Emulation": "EMULATION",
|
||||||
|
"Options.Section.Logging": "PROTOKOLLIERUNG",
|
||||||
|
"Options.Section.Launcher": "LAUNCHER",
|
||||||
|
|
||||||
|
"Options.CpuEngine.Label": "CPU-Engine",
|
||||||
|
"Options.CpuEngine.Desc": "Ausführungs-Engine, die zum Ausführen des Spiel-Codes verwendet wird.",
|
||||||
|
"Options.CpuEngine.Native": "Nativ",
|
||||||
|
|
||||||
|
"Options.Strict.Label": "Strikte dynlib-Auflösung",
|
||||||
|
"Options.Strict.Desc": "Starten abbrechen, wenn ein importiertes Symbol nicht aufgelöst werden kann.",
|
||||||
|
|
||||||
|
"Options.LogLevel.Label": "Protokollstufe",
|
||||||
|
"Options.LogLevel.Desc": "Ausführlichkeit der Emulator-Konsolenausgabe.",
|
||||||
|
"Options.LogLevel.Trace": "Trace",
|
||||||
|
"Options.LogLevel.Debug": "Debug",
|
||||||
|
"Options.LogLevel.Info": "Info",
|
||||||
|
"Options.LogLevel.Warning": "Warnung",
|
||||||
|
"Options.LogLevel.Error": "Fehler",
|
||||||
|
"Options.LogLevel.Critical": "Kritisch",
|
||||||
|
|
||||||
|
"Options.TraceImports.Label": "Import-Trace-Limit",
|
||||||
|
"Options.TraceImports.Desc": "Die ersten N Imports pro Modul verfolgen (0 = aus).",
|
||||||
|
|
||||||
|
"Options.LogToFile.Label": "In Datei protokollieren",
|
||||||
|
"Options.LogToFile.Desc": "Emulator-Ausgabe zusätzlich in eine Log-Datei schreiben.",
|
||||||
|
|
||||||
|
"Options.LogFilePath.Label": "Protokolldatei-Pfad",
|
||||||
|
"Options.LogFilePath.Default": "Kein benutzerdefinierter Pfad – Logs werden im Ordner user/logs neben dem Emulator gespeichert.",
|
||||||
|
"Options.LogFilePath.Select": "Auswählen…",
|
||||||
|
|
||||||
|
"Options.OverrideLogFile.Label": "Protokolldatei überschreiben",
|
||||||
|
"Options.OverrideLogFile.Desc": "Genauen Dateipfad verwenden, statt Title-ID und Zeitstempel anzuhängen.",
|
||||||
|
|
||||||
|
"Options.TitleMusic.Label": "Titel-Musik",
|
||||||
|
"Options.TitleMusic.Desc": "Die Vorschau-Musik des ausgewählten Spiels in der Bibliothek loopend abspielen.",
|
||||||
|
|
||||||
|
"Options.Discord.Label": "Discord-Präsenz",
|
||||||
|
"Options.Discord.Desc": "Zeigt das aktuell gespielte Spiel in deinem Discord-Profil an.",
|
||||||
|
|
||||||
|
"Options.Language.Label": "Emulator-Sprache",
|
||||||
|
"Options.Language.Desc": "Sprache der Benutzeroberfläche. Wird sofort angewendet.",
|
||||||
|
|
||||||
|
"Common.On": "An",
|
||||||
|
"Common.Off": "Aus",
|
||||||
|
|
||||||
|
"Console.Title": "KONSOLE",
|
||||||
|
"Console.SearchWatermark": "Suchen...",
|
||||||
|
"Console.AutoScroll": "Auto-Scroll",
|
||||||
|
"Console.Split": "Teilen",
|
||||||
|
"Console.Copy": "Kopieren",
|
||||||
|
"Console.Clear": "Leeren",
|
||||||
|
"Console.WindowTitle": "SharpEmu Konsole",
|
||||||
|
|
||||||
|
"Launch.NoGameSelected": "Kein Spiel ausgewählt",
|
||||||
|
"Launch.NoGameHint": "Wähle ein Spiel aus der Bibliothek aus oder öffne eine eboot.bin direkt.",
|
||||||
|
"Launch.Idle": "Bereit",
|
||||||
|
"Launch.Console": "≡ Konsole",
|
||||||
|
"Launch.Launch": "▶ Starten",
|
||||||
|
"Launch.Stop": "■ Stoppen",
|
||||||
|
"Launch.Running": "Läuft -- {0}",
|
||||||
|
"Launch.Stopping": "Wird beendet…",
|
||||||
|
"Launch.Exited": "Beendet mit Code {0} ({1})",
|
||||||
|
"Launch.ExeNotFound": "SharpEmu-Executable wurde nicht gefunden. Baue zuerst das SharpEmu.CLI-Projekt (dotnet build).",
|
||||||
|
"Launch.LogFile": "Log-Datei: {0}",
|
||||||
|
"Launch.Command": "$ SharpEmu {0}",
|
||||||
|
"Launch.StartFailed": "Emulator konnte nicht gestartet werden: {0}",
|
||||||
|
"Launch.ProcessExited": "Prozess wurde mit Code {0} ({1}) beendet.",
|
||||||
|
|
||||||
|
"Exit.Ok": "OK",
|
||||||
|
"Exit.InvalidArguments": "Ungültige Argumente",
|
||||||
|
"Exit.EbootNotFound": "eboot nicht gefunden",
|
||||||
|
"Exit.RuntimeException": "Laufzeitfehler",
|
||||||
|
"Exit.EmulationError": "Emulationsfehler",
|
||||||
|
"Exit.Unknown": "unbekannt",
|
||||||
|
|
||||||
|
"Status.EmulatorLocating": "Emulator: wird gesucht…",
|
||||||
|
"Status.EmulatorPath": "Emulator: {0}",
|
||||||
|
"Status.EmulatorNotFound": "Emulator: SharpEmu-Executable nicht gefunden -- baue zuerst SharpEmu.CLI.",
|
||||||
|
"Status.ScanningLibrary": "Bibliothek wird gescannt…",
|
||||||
|
"Status.AddFolderPrompt": "Füge einen Spielordner hinzu, um die Bibliothek zu füllen.",
|
||||||
|
"Status.LibraryScanned": "Bibliothek gescannt: {0} Spiel(e) in {1} Ordner(n).",
|
||||||
|
"Status.CouldNotOpenFolder": "Ordner konnte nicht geöffnet werden: {0}",
|
||||||
|
"Status.CopiedToClipboard": "{0} in die Zwischenablage kopiert.",
|
||||||
|
"Status.RemovedFromLibrary": "“{0}” wurde aus der Bibliothek entfernt. Füge den Ordner erneut hinzu, um es wiederherzustellen.",
|
||||||
|
"Status.Running": "Läuft {0}",
|
||||||
|
"Status.Stopping": "Wird gestoppt…",
|
||||||
|
"Status.Idle": "Bereit",
|
||||||
|
|
||||||
|
"Clipboard.Path": "Pfad",
|
||||||
|
"Clipboard.TitleId": "Title ID",
|
||||||
|
|
||||||
|
"Discord.Playing": "Spielt {0}",
|
||||||
|
"Discord.Browsing": "Durchsucht die Bibliothek",
|
||||||
|
|
||||||
|
"Dialog.ChooseGameFolder": "Spielordner auswählen",
|
||||||
|
"Dialog.OpenExecutable": "Ausführbare Datei zum Starten öffnen",
|
||||||
|
"Dialog.PsExecutables": "PS-Ausführbare Dateien",
|
||||||
|
"Dialog.SaveLogFile": "Protokolldatei speichern unter",
|
||||||
|
"Dialog.PlainTextFiles": "Textdateien",
|
||||||
|
"Dialog.LogFiles": "Protokolldateien"
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"_languageName": "Dansk",
|
||||||
|
|
||||||
|
"Page.Library": "Bibliotek",
|
||||||
|
"Page.Options": "Indstillinger",
|
||||||
|
"Page.GameCount.One": "1 spil",
|
||||||
|
"Page.GameCount.Other": "{0} spil",
|
||||||
|
|
||||||
|
"Library.SearchWatermark": "Søg i biblioteket…",
|
||||||
|
"Library.AddFolder": "+ Tilføj mappe",
|
||||||
|
"Library.Rescan": "⟳ Genindlæs",
|
||||||
|
"Library.OpenFile": "Åbn fil…",
|
||||||
|
|
||||||
|
"Library.Context.Launch": "Start",
|
||||||
|
"Library.Context.OpenFolder": "Åbn spilmappe",
|
||||||
|
"Library.Context.CopyPath": "Kopiér sti",
|
||||||
|
"Library.Context.CopyTitleId": "Kopiér titel-ID",
|
||||||
|
"Library.Context.Remove": "Fjern fra bibliotek",
|
||||||
|
|
||||||
|
"Library.Empty.Title": "Dit bibliotek er tomt",
|
||||||
|
"Library.Empty.Hint": "Tilføj en mappe med dine spil for at komme i gang.",
|
||||||
|
"Library.Empty.SearchTitle": "Ingen spil matcher din søgning",
|
||||||
|
"Library.Empty.SearchHint": "Intet i biblioteket matcher “{0}”.",
|
||||||
|
"Library.Empty.AddFolder": "+ Tilføj spilmappe",
|
||||||
|
|
||||||
|
"Library.Loading": "Indlæser bibliotek…",
|
||||||
|
|
||||||
|
"Options.General": "Generelt",
|
||||||
|
"Options.Section.Emulation": "EMULERING",
|
||||||
|
"Options.Section.Logging": "LOGGING",
|
||||||
|
"Options.Section.Launcher": "LAUNCHER",
|
||||||
|
|
||||||
|
"Options.CpuEngine.Label": "CPU-engine",
|
||||||
|
"Options.CpuEngine.Desc": "Den eksekveringsengine der bruges til at køre spilkode.",
|
||||||
|
"Options.CpuEngine.Native": "Native",
|
||||||
|
|
||||||
|
"Options.Strict.Label": "Streng dynlib-opløsning",
|
||||||
|
"Options.Strict.Desc": "Afbryd opstarten, når et importeret symbol ikke kan findes.",
|
||||||
|
|
||||||
|
"Options.LogLevel.Label": "Logniveau",
|
||||||
|
"Options.LogLevel.Desc": "Detaljeringsgrad for emulatorens konsoloutput.",
|
||||||
|
"Options.LogLevel.Trace": "Trace",
|
||||||
|
"Options.LogLevel.Debug": "Debug",
|
||||||
|
"Options.LogLevel.Info": "Info",
|
||||||
|
"Options.LogLevel.Warning": "Advarsel",
|
||||||
|
"Options.LogLevel.Error": "Fejl",
|
||||||
|
"Options.LogLevel.Critical": "Kritisk",
|
||||||
|
|
||||||
|
"Options.TraceImports.Label": "Grænse for import-trace",
|
||||||
|
"Options.TraceImports.Desc": "Spor de første N imports pr. modul (0 = fra).",
|
||||||
|
|
||||||
|
"Options.LogToFile.Label": "Log til fil",
|
||||||
|
"Options.LogToFile.Desc": "Spejl emulatorens output til en logfil.",
|
||||||
|
|
||||||
|
"Options.LogFilePath.Label": "Sti til logfil",
|
||||||
|
"Options.LogFilePath.Default": "Ingen brugerdefineret sti — logs gemmes i user/logs ved siden af emulatoren.",
|
||||||
|
"Options.LogFilePath.Select": "Vælg…",
|
||||||
|
|
||||||
|
"Options.OverrideLogFile.Label": "Tilsidesæt logfil",
|
||||||
|
"Options.OverrideLogFile.Desc": "Brug den præcise filsti i stedet for at tilføje titel-ID og tidsstempel.",
|
||||||
|
|
||||||
|
"Options.TitleMusic.Label": "Titelmusik",
|
||||||
|
"Options.TitleMusic.Desc": "Gentag det valgte spils forhåndsvisningsmusik i biblioteket.",
|
||||||
|
|
||||||
|
"Options.Discord.Label": "Discord-tilstedeværelse",
|
||||||
|
"Options.Discord.Desc": "Vis det kørende spil på din Discord-profil.",
|
||||||
|
|
||||||
|
"Options.Language.Label": "Emulatorsprog",
|
||||||
|
"Options.Language.Desc": "Sprog der bruges i hele launcheren. Anvendes med det samme.",
|
||||||
|
|
||||||
|
"Common.On": "Til",
|
||||||
|
"Common.Off": "Fra",
|
||||||
|
|
||||||
|
"Console.Title": "KONSOL",
|
||||||
|
"Console.SearchWatermark": "Søg...",
|
||||||
|
"Console.AutoScroll": "Auto-scroll",
|
||||||
|
"Console.Split": "Opdel",
|
||||||
|
"Console.Copy": "Kopiér",
|
||||||
|
"Console.Clear": "Ryd",
|
||||||
|
"Console.WindowTitle": "SharpEmu-konsol",
|
||||||
|
|
||||||
|
"Launch.NoGameSelected": "Intet spil valgt",
|
||||||
|
"Launch.NoGameHint": "Vælg et spil fra biblioteket, eller åbn en eboot.bin direkte.",
|
||||||
|
"Launch.Idle": "Inaktiv",
|
||||||
|
"Launch.Console": "≡ Konsol",
|
||||||
|
"Launch.Launch": "▶ Start",
|
||||||
|
"Launch.Stop": "■ Stop",
|
||||||
|
"Launch.Running": "Kører — {0}",
|
||||||
|
"Launch.Stopping": "Stopper…",
|
||||||
|
"Launch.Exited": "Afsluttet med kode {0} ({1})",
|
||||||
|
"Launch.ExeNotFound": "SharpEmu-programmet blev ikke fundet. Byg SharpEmu.CLI-projektet først (dotnet build).",
|
||||||
|
"Launch.LogFile": "Logfil: {0}",
|
||||||
|
"Launch.Command": "$ SharpEmu {0}",
|
||||||
|
"Launch.StartFailed": "Kunne ikke starte emulatoren: {0}",
|
||||||
|
"Launch.ProcessExited": "Processen afsluttede med kode {0} ({1}).",
|
||||||
|
|
||||||
|
"Exit.Ok": "OK",
|
||||||
|
"Exit.InvalidArguments": "ugyldige argumenter",
|
||||||
|
"Exit.EbootNotFound": "eboot ikke fundet",
|
||||||
|
"Exit.RuntimeException": "runtime-fejl",
|
||||||
|
"Exit.EmulationError": "emuleringsfejl",
|
||||||
|
"Exit.Unknown": "ukendt",
|
||||||
|
|
||||||
|
"Status.EmulatorLocating": "Emulator: lokaliserer…",
|
||||||
|
"Status.EmulatorPath": "Emulator: {0}",
|
||||||
|
"Status.EmulatorNotFound": "Emulator: SharpEmu-programmet blev ikke fundet — byg SharpEmu.CLI først.",
|
||||||
|
"Status.ScanningLibrary": "Skanner bibliotek…",
|
||||||
|
"Status.AddFolderPrompt": "Tilføj en spilmappe for at udfylde biblioteket.",
|
||||||
|
"Status.LibraryScanned": "Bibliotek skannet: {0} spil i {1} mappe(r).",
|
||||||
|
"Status.CouldNotOpenFolder": "Kunne ikke åbne mappe: {0}",
|
||||||
|
"Status.CopiedToClipboard": "{0} kopieret til udklipsholderen.",
|
||||||
|
"Status.RemovedFromLibrary": "“{0}” fjernet fra biblioteket. Tilføj mappen igen for at gendanne det.",
|
||||||
|
"Status.Running": "Kører {0}",
|
||||||
|
"Status.Stopping": "Stopper…",
|
||||||
|
"Status.Idle": "Inaktiv",
|
||||||
|
|
||||||
|
"Clipboard.Path": "Sti",
|
||||||
|
"Clipboard.TitleId": "Titel-ID",
|
||||||
|
|
||||||
|
"Discord.Playing": "Spiller {0}",
|
||||||
|
"Discord.Browsing": "Gennemser biblioteket",
|
||||||
|
|
||||||
|
"Dialog.ChooseGameFolder": "Vælg en mappe der indeholder spil",
|
||||||
|
"Dialog.OpenExecutable": "Åbn et program der skal startes",
|
||||||
|
"Dialog.PsExecutables": "PS-programmer",
|
||||||
|
"Dialog.SaveLogFile": "Vælg hvor logfilen skal gemmes",
|
||||||
|
"Dialog.PlainTextFiles": "Almindelige tekstfiler",
|
||||||
|
"Dialog.LogFiles": "Logfiler"
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
{
|
||||||
|
"_languageName": "English",
|
||||||
|
|
||||||
|
"Page.Library": "Library",
|
||||||
|
"Page.Options": "Options",
|
||||||
|
"Page.GameCount.One": "1 game",
|
||||||
|
"Page.GameCount.Other": "{0} games",
|
||||||
|
|
||||||
|
"Library.SearchWatermark": "Search library…",
|
||||||
|
"Library.AddFolder": "+ Add folder",
|
||||||
|
"Library.Rescan": "⟳ Rescan",
|
||||||
|
"Library.OpenFile": "Open file…",
|
||||||
|
|
||||||
|
"Library.Context.Launch": "Launch",
|
||||||
|
"Library.Context.OpenFolder": "Open game folder",
|
||||||
|
"Library.Context.CopyPath": "Copy path",
|
||||||
|
"Library.Context.CopyTitleId": "Copy title ID",
|
||||||
|
"Library.Context.Remove": "Remove from library",
|
||||||
|
|
||||||
|
"Library.Empty.Title": "Your library is empty",
|
||||||
|
"Library.Empty.Hint": "Add a folder containing your games to get started.",
|
||||||
|
"Library.Empty.SearchTitle": "No games match your search",
|
||||||
|
"Library.Empty.SearchHint": "Nothing in the library matches “{0}”.",
|
||||||
|
"Library.Empty.AddFolder": "+ Add game folder",
|
||||||
|
|
||||||
|
"Library.Loading": "Loading library…",
|
||||||
|
|
||||||
|
"Options.General": "General",
|
||||||
|
"Options.Section.Emulation": "EMULATION",
|
||||||
|
"Options.Section.Logging": "LOGGING",
|
||||||
|
"Options.Section.Launcher": "LAUNCHER",
|
||||||
|
|
||||||
|
"Options.CpuEngine.Label": "CPU engine",
|
||||||
|
"Options.CpuEngine.Desc": "Execution engine used to run game code.",
|
||||||
|
"Options.CpuEngine.Native": "Native",
|
||||||
|
|
||||||
|
"Options.Strict.Label": "Strict dynlib resolution",
|
||||||
|
"Options.Strict.Desc": "Fail the launch when an imported symbol cannot be resolved.",
|
||||||
|
|
||||||
|
"Options.LogLevel.Label": "Log level",
|
||||||
|
"Options.LogLevel.Desc": "Verbosity of the emulator console output.",
|
||||||
|
"Options.LogLevel.Trace": "Trace",
|
||||||
|
"Options.LogLevel.Debug": "Debug",
|
||||||
|
"Options.LogLevel.Info": "Info",
|
||||||
|
"Options.LogLevel.Warning": "Warning",
|
||||||
|
"Options.LogLevel.Error": "Error",
|
||||||
|
"Options.LogLevel.Critical": "Critical",
|
||||||
|
|
||||||
|
"Options.TraceImports.Label": "Import trace limit",
|
||||||
|
"Options.TraceImports.Desc": "Trace the first N imports per module (0 = off).",
|
||||||
|
|
||||||
|
"Options.LogToFile.Label": "Log to file",
|
||||||
|
"Options.LogToFile.Desc": "Mirror emulator output to a log file.",
|
||||||
|
|
||||||
|
"Options.LogFilePath.Label": "Log file path",
|
||||||
|
"Options.LogFilePath.Default": "No custom path — logs go to user/logs next to the emulator.",
|
||||||
|
"Options.LogFilePath.Select": "Select…",
|
||||||
|
|
||||||
|
"Options.OverrideLogFile.Label": "Override log file",
|
||||||
|
"Options.OverrideLogFile.Desc": "Use the exact file path instead of appending title ID and timestamp.",
|
||||||
|
|
||||||
|
"Options.TitleMusic.Label": "Title music",
|
||||||
|
"Options.TitleMusic.Desc": "Loop the selected game's preview music in the library.",
|
||||||
|
|
||||||
|
"Options.Discord.Label": "Discord presence",
|
||||||
|
"Options.Discord.Desc": "Show the running game on your Discord profile.",
|
||||||
|
|
||||||
|
"Options.Language.Label": "Emulator language",
|
||||||
|
"Options.Language.Desc": "Language used throughout the launcher. Applies immediately.",
|
||||||
|
|
||||||
|
"Common.On": "On",
|
||||||
|
"Common.Off": "Off",
|
||||||
|
|
||||||
|
"Console.Title": "CONSOLE",
|
||||||
|
"Console.SearchWatermark": "Search...",
|
||||||
|
"Console.AutoScroll": "Auto-scroll",
|
||||||
|
"Console.Split": "Split",
|
||||||
|
"Console.Copy": "Copy",
|
||||||
|
"Console.Clear": "Clear",
|
||||||
|
"Console.WindowTitle": "SharpEmu Console",
|
||||||
|
|
||||||
|
"Launch.NoGameSelected": "No game selected",
|
||||||
|
"Launch.NoGameHint": "Pick a game from the library, or open an eboot.bin directly.",
|
||||||
|
"Launch.Idle": "Idle",
|
||||||
|
"Launch.Console": "≡ Console",
|
||||||
|
"Launch.Launch": "▶ Launch",
|
||||||
|
"Launch.Stop": "■ Stop",
|
||||||
|
"Launch.Running": "Running — {0}",
|
||||||
|
"Launch.Stopping": "Stopping…",
|
||||||
|
"Launch.Exited": "Exited with code {0} ({1})",
|
||||||
|
"Launch.ExeNotFound": "SharpEmu executable not found. Build the SharpEmu.CLI project first (dotnet build).",
|
||||||
|
"Launch.LogFile": "Log file: {0}",
|
||||||
|
"Launch.Command": "$ SharpEmu {0}",
|
||||||
|
"Launch.StartFailed": "Failed to start the emulator: {0}",
|
||||||
|
"Launch.ProcessExited": "Process exited with code {0} ({1}).",
|
||||||
|
|
||||||
|
"Exit.Ok": "OK",
|
||||||
|
"Exit.InvalidArguments": "invalid arguments",
|
||||||
|
"Exit.EbootNotFound": "eboot not found",
|
||||||
|
"Exit.RuntimeException": "runtime exception",
|
||||||
|
"Exit.EmulationError": "emulation error",
|
||||||
|
"Exit.Unknown": "unknown",
|
||||||
|
|
||||||
|
"Status.EmulatorLocating": "Emulator: locating…",
|
||||||
|
"Status.EmulatorPath": "Emulator: {0}",
|
||||||
|
"Status.EmulatorNotFound": "Emulator: SharpEmu executable not found — build SharpEmu.CLI first.",
|
||||||
|
"Status.ScanningLibrary": "Scanning library…",
|
||||||
|
"Status.AddFolderPrompt": "Add a game folder to populate the library.",
|
||||||
|
"Status.LibraryScanned": "Library scanned: {0} game(s) in {1} folder(s).",
|
||||||
|
"Status.CouldNotOpenFolder": "Could not open folder: {0}",
|
||||||
|
"Status.CopiedToClipboard": "{0} copied to clipboard.",
|
||||||
|
"Status.RemovedFromLibrary": "Removed “{0}” from the library. Re-add its folder to restore it.",
|
||||||
|
"Status.Running": "Running {0}",
|
||||||
|
"Status.Stopping": "Stopping…",
|
||||||
|
"Status.Idle": "Idle",
|
||||||
|
|
||||||
|
"Clipboard.Path": "Path",
|
||||||
|
"Clipboard.TitleId": "Title ID",
|
||||||
|
|
||||||
|
"Discord.Playing": "Playing {0}",
|
||||||
|
"Discord.Browsing": "Browsing the library",
|
||||||
|
|
||||||
|
"Dialog.ChooseGameFolder": "Choose a folder containing games",
|
||||||
|
"Dialog.OpenExecutable": "Open an executable to launch",
|
||||||
|
"Dialog.PsExecutables": "PS executables",
|
||||||
|
"Dialog.SaveLogFile": "Select where to save the Log file",
|
||||||
|
"Dialog.PlainTextFiles": "Plain Text Files",
|
||||||
|
"Dialog.LogFiles": "Log Files",
|
||||||
|
|
||||||
|
"Options.About" : "About",
|
||||||
|
"About.Github.Label": "GitHub",
|
||||||
|
"About.Github.Desc": "Source code, issues and project development.",
|
||||||
|
"About.Discord.Label": "Discord",
|
||||||
|
"About.Discord.Desc": "Join the community, get support and follow development.",
|
||||||
|
"About.GithubButton": "Contribute in GitHub!",
|
||||||
|
"About.DiscordButton": "Join our Discord!"
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
{
|
||||||
|
"_languageName": "Español",
|
||||||
|
|
||||||
|
"Page.Library": "Biblioteca",
|
||||||
|
"Page.Options": "Opciones",
|
||||||
|
"Page.GameCount.One": "1 juego",
|
||||||
|
"Page.GameCount.Other": "{0} juegos",
|
||||||
|
|
||||||
|
"Library.SearchWatermark": "Buscar en la biblioteca…",
|
||||||
|
"Library.AddFolder": "+ Añadir carpeta",
|
||||||
|
"Library.Rescan": "⟳ Volver a escanear",
|
||||||
|
"Library.OpenFile": "Abrir archivo…",
|
||||||
|
|
||||||
|
"Library.Context.Launch": "Iniciar",
|
||||||
|
"Library.Context.OpenFolder": "Abrir carpeta de juegos",
|
||||||
|
"Library.Context.CopyPath": "Copiar ruta",
|
||||||
|
"Library.Context.CopyTitleId": "Copiar ID del título",
|
||||||
|
"Library.Context.Remove": "Eliminar de la biblioteca",
|
||||||
|
|
||||||
|
"Library.Empty.Title": "Tu biblioteca está vacía",
|
||||||
|
"Library.Empty.Hint": "Añade una carpeta que contenga tus juegos para empezar.",
|
||||||
|
"Library.Empty.SearchTitle": "Ningún juego coincide con la búsqueda",
|
||||||
|
"Library.Empty.SearchHint": "No se ha encontrado nada en la biblioteca que coincida con “{0}”.",
|
||||||
|
"Library.Empty.AddFolder": "+ Añadir carpeta de juegos",
|
||||||
|
|
||||||
|
"Library.Loading": "Cargando biblioteca…",
|
||||||
|
|
||||||
|
"Options.General": "General",
|
||||||
|
"Options.Section.Emulation": "EMULACIÓN",
|
||||||
|
"Options.Section.Logging": "LOGS",
|
||||||
|
"Options.Section.Launcher": "LAUNCHER",
|
||||||
|
|
||||||
|
"Options.CpuEngine.Label": "Motor de CPU",
|
||||||
|
"Options.CpuEngine.Desc": "Motor utilizado para ejecutar el código del juego.",
|
||||||
|
"Options.CpuEngine.Native": "Nativo",
|
||||||
|
|
||||||
|
"Options.Strict.Label": "Resolución estricta de dynlib (Bibliotecas dinámicas)",
|
||||||
|
"Options.Strict.Desc": "Detener la ejecución cuando un símbolo importado no se pueda resolver.",
|
||||||
|
|
||||||
|
"Options.LogLevel.Label": "Nivel de Log",
|
||||||
|
"Options.LogLevel.Desc": "Verbosidad de la salida en consola del emulador.",
|
||||||
|
"Options.LogLevel.Trace": "Trazas",
|
||||||
|
"Options.LogLevel.Debug": "Depuración",
|
||||||
|
"Options.LogLevel.Info": "Información",
|
||||||
|
"Options.LogLevel.Warning": "Advertencia",
|
||||||
|
"Options.LogLevel.Error": "Error",
|
||||||
|
"Options.LogLevel.Critical": "Crítico",
|
||||||
|
|
||||||
|
"Options.TraceImports.Label": "Límite de trazado de importaciones",
|
||||||
|
"Options.TraceImports.Desc": "Trazar las primeras N importaciones por módulo (0 = off).",
|
||||||
|
|
||||||
|
"Options.LogToFile.Label": "Registrar log en archivo",
|
||||||
|
"Options.LogToFile.Desc": "Duplicar la salida del emulador en un archivo de logs.",
|
||||||
|
|
||||||
|
"Options.LogFilePath.Label": "Ruta del archivo de Log",
|
||||||
|
"Options.LogFilePath.Default": "Sin ruta personalizada — los logs van a user/logs al lado del emulador.",
|
||||||
|
"Options.LogFilePath.Select": "Seleccionar…",
|
||||||
|
|
||||||
|
"Options.OverrideLogFile.Label": "Sobreescribir archivo de logs",
|
||||||
|
"Options.OverrideLogFile.Desc": "Utilizar la misma ruta para el archivo de logs en vez de añadir la ID del título y marca de tiempo.",
|
||||||
|
|
||||||
|
"Options.TitleMusic.Label": "Música del título",
|
||||||
|
"Options.TitleMusic.Desc": "Repetir en bucle la preview de la música del juego seleccionado en la biblioteca.",
|
||||||
|
|
||||||
|
"Options.Discord.Label": "Actividad de Discord",
|
||||||
|
"Options.Discord.Desc": "Mostrar juego en ejecución en tu perfil de Discord.",
|
||||||
|
|
||||||
|
"Options.Language.Label": "Idioma del emulador",
|
||||||
|
"Options.Language.Desc": "Idioma utilizado en todo el launcher. Se aplica inmediatamente.",
|
||||||
|
|
||||||
|
"Common.On": "Encendido",
|
||||||
|
"Common.Off": "Apagado",
|
||||||
|
|
||||||
|
"Console.Title": "CONSOLA",
|
||||||
|
"Console.SearchWatermark": "Buscar...",
|
||||||
|
"Console.AutoScroll": "Desplazamiento automático",
|
||||||
|
"Console.Split": "Desacoplar",
|
||||||
|
"Console.Copy": "Copiar",
|
||||||
|
"Console.Clear": "Limpiar",
|
||||||
|
"Console.WindowTitle": "Consola SharpEmu",
|
||||||
|
|
||||||
|
"Launch.NoGameSelected": "No hay ningún juego seleccionado",
|
||||||
|
"Launch.NoGameHint": "Selecciona un juego de la biblioteca o abre un eboot.bin directamente.",
|
||||||
|
"Launch.Idle": "Inactivo",
|
||||||
|
"Launch.Console": "≡ Consola",
|
||||||
|
"Launch.Launch": "▶ Iniciar",
|
||||||
|
"Launch.Stop": "■ Detener",
|
||||||
|
"Launch.Running": "En ejecución — {0}",
|
||||||
|
"Launch.Stopping": "Deteniendo…",
|
||||||
|
"Launch.Exited": "Finalizó con el código {0} ({1})",
|
||||||
|
"Launch.ExeNotFound": "No se ha encontrado el ejecutable de SharpEmu. Compila previamente el proyecto SharpEmu.CLI (dotnet build).",
|
||||||
|
"Launch.LogFile": "Archivo de Log: {0}",
|
||||||
|
"Launch.Command": "$ SharpEmu {0}",
|
||||||
|
"Launch.StartFailed": "Error al iniciar el emulador: {0}",
|
||||||
|
"Launch.ProcessExited": "El proceso finalizó con el código {0} ({1}).",
|
||||||
|
|
||||||
|
"Exit.Ok": "OK",
|
||||||
|
"Exit.InvalidArguments": "argumentos no válidos",
|
||||||
|
"Exit.EbootNotFound": "no se encontró eboot",
|
||||||
|
"Exit.RuntimeException": "excepción en tiempo de ejecución",
|
||||||
|
"Exit.EmulationError": "error de emulación",
|
||||||
|
"Exit.Unknown": "desconocido",
|
||||||
|
|
||||||
|
"Status.EmulatorLocating": "Emulador: localizando…",
|
||||||
|
"Status.EmulatorPath": "Emulador: {0}",
|
||||||
|
"Status.EmulatorNotFound": "Emulador: No se encontró el ejecutable de SharpEmu — compila previamente SharpEmu.CLI.",
|
||||||
|
"Status.ScanningLibrary": "Escaneando biblioteca…",
|
||||||
|
"Status.AddFolderPrompt": "Añade una carpeta de juegos para poblar la biblioteca.",
|
||||||
|
"Status.LibraryScanned": "Biblioteca escaneada: Se encontraron {0} juego(s) en {1} carpeta(s).",
|
||||||
|
"Status.CouldNotOpenFolder": "No se ha podido abrir la carpeta: {0}",
|
||||||
|
"Status.CopiedToClipboard": "{0} copiado al portapapeles.",
|
||||||
|
"Status.RemovedFromLibrary": "Se eliminó “{0}” de la biblioteca. Vuelve a añadir su carpeta para restaurarlo.",
|
||||||
|
"Status.Running": "Ejecutando {0}",
|
||||||
|
"Status.Stopping": "Deteniendo…",
|
||||||
|
"Status.Idle": "Inactivo",
|
||||||
|
|
||||||
|
"Clipboard.Path": "Ruta",
|
||||||
|
"Clipboard.TitleId": "ID del título",
|
||||||
|
|
||||||
|
"Discord.Playing": "Jugando a {0}",
|
||||||
|
"Discord.Browsing": "Navegando en la biblioteca, buscando un juego para divertirse.",
|
||||||
|
|
||||||
|
"Dialog.ChooseGameFolder": "Selecciona una carpeta que contenga juegos",
|
||||||
|
"Dialog.OpenExecutable": "Abrir un ejecutable para iniciar",
|
||||||
|
"Dialog.PsExecutables": "Ejecutables de PS",
|
||||||
|
"Dialog.SaveLogFile": "Selecciona dónde guardar el archivo de Logs",
|
||||||
|
"Dialog.PlainTextFiles": "Archivos en texto plano",
|
||||||
|
"Dialog.LogFiles": "Archivos de Log",
|
||||||
|
|
||||||
|
"Options.About" : "Informacion",
|
||||||
|
"About.Github.Label": "GitHub",
|
||||||
|
"About.Github.Desc": "Código fuente, issues y desarrollo del proyecto.",
|
||||||
|
"About.Discord.Label": "Discord",
|
||||||
|
"About.Discord.Desc": "Únete a la comunidad, recibe soporte y sigue el desarrollo.",
|
||||||
|
"About.GithubButton": "Contribuye en GitHub!",
|
||||||
|
"About.DiscordButton": "Únete a nuestro Discord!"
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"_languageName": "Français",
|
||||||
|
|
||||||
|
"Page.Library": "Bibliothèque",
|
||||||
|
"Page.Options": "Options",
|
||||||
|
"Page.GameCount.One": "1 jeu",
|
||||||
|
"Page.GameCount.Other": "{0} jeux",
|
||||||
|
|
||||||
|
"Library.SearchWatermark": "Rechercher dans la bibliothèque…",
|
||||||
|
"Library.AddFolder": "+ Ajouter un dossier",
|
||||||
|
"Library.Rescan": "⟳ Analyser à nouveau",
|
||||||
|
"Library.OpenFile": "Ouvrir un fichier…",
|
||||||
|
|
||||||
|
"Library.Context.Launch": "Lancer",
|
||||||
|
"Library.Context.OpenFolder": "Ouvrir le dossier du jeu",
|
||||||
|
"Library.Context.CopyPath": "Copier le chemin",
|
||||||
|
"Library.Context.CopyTitleId": "Copier l’identifiant du jeu",
|
||||||
|
"Library.Context.Remove": "Retirer de la bibliothèque",
|
||||||
|
|
||||||
|
"Library.Empty.Title": "Votre bibliothèque est vide",
|
||||||
|
"Library.Empty.Hint": "Ajoutez un dossier contenant vos jeux pour commencer.",
|
||||||
|
"Library.Empty.SearchTitle": "Aucun jeu ne correspond à votre recherche",
|
||||||
|
"Library.Empty.SearchHint": "Aucun élément de la bibliothèque ne correspond à « {0} ».",
|
||||||
|
"Library.Empty.AddFolder": "+ Ajouter un dossier de jeux",
|
||||||
|
|
||||||
|
"Library.Loading": "Chargement de la bibliothèque…",
|
||||||
|
|
||||||
|
"Options.General": "Général",
|
||||||
|
"Options.Section.Emulation": "ÉMULATION",
|
||||||
|
"Options.Section.Logging": "JOURNALISATION",
|
||||||
|
"Options.Section.Launcher": "LANCEUR",
|
||||||
|
|
||||||
|
"Options.CpuEngine.Label": "Moteur CPU",
|
||||||
|
"Options.CpuEngine.Desc": "Moteur d’exécution utilisé pour exécuter le code du jeu.",
|
||||||
|
"Options.CpuEngine.Native": "Natif",
|
||||||
|
|
||||||
|
"Options.Strict.Label": "Résolution stricte des bibliothèques dynamiques",
|
||||||
|
"Options.Strict.Desc": "Interrompre le lancement lorsqu’un symbole importé ne peut pas être résolu.",
|
||||||
|
|
||||||
|
"Options.LogLevel.Label": "Niveau de journalisation",
|
||||||
|
"Options.LogLevel.Desc": "Niveau de détail des messages affichés dans la console de l’émulateur.",
|
||||||
|
"Options.LogLevel.Trace": "Traçage",
|
||||||
|
"Options.LogLevel.Debug": "Débogage",
|
||||||
|
"Options.LogLevel.Info": "Informations",
|
||||||
|
"Options.LogLevel.Warning": "Avertissements",
|
||||||
|
"Options.LogLevel.Error": "Erreurs",
|
||||||
|
"Options.LogLevel.Critical": "Erreurs critiques",
|
||||||
|
|
||||||
|
"Options.TraceImports.Label": "Limite de traçage des imports",
|
||||||
|
"Options.TraceImports.Desc": "Tracer les N premiers imports de chaque module (0 = désactivé).",
|
||||||
|
|
||||||
|
"Options.LogToFile.Label": "Enregistrer dans un fichier",
|
||||||
|
"Options.LogToFile.Desc": "Copier la sortie de l’émulateur dans un fichier journal.",
|
||||||
|
|
||||||
|
"Options.LogFilePath.Label": "Chemin du fichier journal",
|
||||||
|
"Options.LogFilePath.Default": "Aucun chemin personnalisé — les journaux sont enregistrés dans user/logs à côté de l’émulateur.",
|
||||||
|
"Options.LogFilePath.Select": "Sélectionner…",
|
||||||
|
|
||||||
|
"Options.OverrideLogFile.Label": "Remplacer le fichier journal",
|
||||||
|
"Options.OverrideLogFile.Desc": "Utiliser exactement ce chemin au lieu d’ajouter l’identifiant du jeu et l’horodatage.",
|
||||||
|
|
||||||
|
"Options.TitleMusic.Label": "Musique du jeu",
|
||||||
|
"Options.TitleMusic.Desc": "Lire en boucle la musique d’aperçu du jeu sélectionné dans la bibliothèque.",
|
||||||
|
|
||||||
|
"Options.Discord.Label": "Présence Discord",
|
||||||
|
"Options.Discord.Desc": "Afficher le jeu en cours d’exécution sur votre profil Discord.",
|
||||||
|
|
||||||
|
"Options.Language.Label": "Langue de l’émulateur",
|
||||||
|
"Options.Language.Desc": "Langue utilisée dans l’ensemble du lanceur. Le changement est immédiat.",
|
||||||
|
|
||||||
|
"Common.On": "Activé",
|
||||||
|
"Common.Off": "Désactivé",
|
||||||
|
|
||||||
|
"Console.Title": "CONSOLE",
|
||||||
|
"Console.SearchWatermark": "Rechercher…",
|
||||||
|
"Console.AutoScroll": "Défilement automatique",
|
||||||
|
"Console.Split": "Détacher",
|
||||||
|
"Console.Copy": "Copier",
|
||||||
|
"Console.Clear": "Effacer",
|
||||||
|
"Console.WindowTitle": "Console SharpEmu",
|
||||||
|
|
||||||
|
"Launch.NoGameSelected": "Aucun jeu sélectionné",
|
||||||
|
"Launch.NoGameHint": "Choisissez un jeu dans la bibliothèque ou ouvrez directement un fichier eboot.bin.",
|
||||||
|
"Launch.Idle": "Inactif",
|
||||||
|
"Launch.Console": "≡ Console",
|
||||||
|
"Launch.Launch": "▶ Lancer",
|
||||||
|
"Launch.Stop": "■ Arrêter",
|
||||||
|
"Launch.Running": "En cours d’exécution — {0}",
|
||||||
|
"Launch.Stopping": "Arrêt en cours…",
|
||||||
|
"Launch.Exited": "Processus terminé avec le code {0} ({1})",
|
||||||
|
"Launch.ExeNotFound": "L’exécutable SharpEmu est introuvable. Compilez d’abord le projet SharpEmu.CLI (dotnet build).",
|
||||||
|
"Launch.LogFile": "Fichier journal : {0}",
|
||||||
|
"Launch.Command": "$ SharpEmu {0}",
|
||||||
|
"Launch.StartFailed": "Impossible de démarrer l’émulateur : {0}",
|
||||||
|
"Launch.ProcessExited": "Le processus s’est terminé avec le code {0} ({1}).",
|
||||||
|
|
||||||
|
"Exit.Ok": "OK",
|
||||||
|
"Exit.InvalidArguments": "arguments non valides",
|
||||||
|
"Exit.EbootNotFound": "eboot introuvable",
|
||||||
|
"Exit.RuntimeException": "exception d’exécution",
|
||||||
|
"Exit.EmulationError": "erreur d’émulation",
|
||||||
|
"Exit.Unknown": "inconnu",
|
||||||
|
|
||||||
|
"Status.EmulatorLocating": "Émulateur : recherche en cours…",
|
||||||
|
"Status.EmulatorPath": "Émulateur : {0}",
|
||||||
|
"Status.EmulatorNotFound": "Émulateur : l’exécutable SharpEmu est introuvable — compilez d’abord SharpEmu.CLI.",
|
||||||
|
"Status.ScanningLibrary": "Analyse de la bibliothèque…",
|
||||||
|
"Status.AddFolderPrompt": "Ajoutez un dossier de jeux pour remplir la bibliothèque.",
|
||||||
|
"Status.LibraryScanned": "Bibliothèque analysée : {0} jeu(x) dans {1} dossier(s).",
|
||||||
|
"Status.CouldNotOpenFolder": "Impossible d’ouvrir le dossier : {0}",
|
||||||
|
"Status.CopiedToClipboard": "{0} copié dans le presse-papiers.",
|
||||||
|
"Status.RemovedFromLibrary": "« {0} » a été retiré de la bibliothèque. Ajoutez à nouveau son dossier pour le restaurer.",
|
||||||
|
"Status.Running": "Exécution de {0}",
|
||||||
|
"Status.Stopping": "Arrêt en cours…",
|
||||||
|
"Status.Idle": "Inactif",
|
||||||
|
|
||||||
|
"Clipboard.Path": "Chemin",
|
||||||
|
"Clipboard.TitleId": "Identifiant du jeu",
|
||||||
|
|
||||||
|
"Discord.Playing": "Joue à {0}",
|
||||||
|
"Discord.Browsing": "Parcourt la bibliothèque",
|
||||||
|
|
||||||
|
"Dialog.ChooseGameFolder": "Choisir un dossier contenant des jeux",
|
||||||
|
"Dialog.OpenExecutable": "Ouvrir un exécutable à lancer",
|
||||||
|
"Dialog.PsExecutables": "Exécutables PlayStation",
|
||||||
|
"Dialog.SaveLogFile": "Choisir l’emplacement du fichier journal",
|
||||||
|
"Dialog.PlainTextFiles": "Fichiers texte brut",
|
||||||
|
"Dialog.LogFiles": "Fichiers journaux"
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
{
|
||||||
|
"_languageName": "Italiano",
|
||||||
|
|
||||||
|
"Page.Library": "Libreria",
|
||||||
|
"Page.Options": "Opzioni",
|
||||||
|
"Page.GameCount.One": "1 gioco",
|
||||||
|
"Page.GameCount.Other": "{0} giochi",
|
||||||
|
|
||||||
|
"Library.SearchWatermark": "Cerca nella libreria…",
|
||||||
|
"Library.AddFolder": "+ Aggiungi cartella",
|
||||||
|
"Library.Rescan": "⟳ Riscansiona",
|
||||||
|
"Library.OpenFile": "Apri file…",
|
||||||
|
|
||||||
|
"Library.Context.Launch": "Avvia",
|
||||||
|
"Library.Context.OpenFolder": "Apri cartella gioco",
|
||||||
|
"Library.Context.CopyPath": "Copia percorso",
|
||||||
|
"Library.Context.CopyTitleId": "Copia ID titolo",
|
||||||
|
"Library.Context.Remove": "Rimuovi dalla libreria",
|
||||||
|
|
||||||
|
"Library.Empty.Title": "La tua libreria è vuota",
|
||||||
|
"Library.Empty.Hint": "Aggiungi la cartella che contiene i tuoi giochi per partire.",
|
||||||
|
"Library.Empty.SearchTitle": "Nessun gioco corrisponde alla ricerca",
|
||||||
|
"Library.Empty.SearchHint": "Nulla nella libreria corrisponde a “{0}”.",
|
||||||
|
"Library.Empty.AddFolder": "+ Aggiungi cartella giochi",
|
||||||
|
|
||||||
|
"Library.Loading": "Caricamento libreria…",
|
||||||
|
|
||||||
|
"Options.General": "Generale",
|
||||||
|
"Options.Section.Emulation": "EMULAZIONE",
|
||||||
|
"Options.Section.Logging": "LOG",
|
||||||
|
"Options.Section.Launcher": "LAUNCHER",
|
||||||
|
|
||||||
|
"Options.CpuEngine.Label": "CPU engine",
|
||||||
|
"Options.CpuEngine.Desc": "Motore di esecuzione utilizzato per eseguire il codice del gioco.",
|
||||||
|
"Options.CpuEngine.Native": "Nativo",
|
||||||
|
|
||||||
|
"Options.Strict.Label": "Risoluzione rigorosa dynlib",
|
||||||
|
"Options.Strict.Desc": "Interrompi l'avvio quando un simbolo importato non può essere trovato.",
|
||||||
|
|
||||||
|
"Options.LogLevel.Label": "Livello Log",
|
||||||
|
"Options.LogLevel.Desc": "Livello di dettaglio dell'output console dell'emulatore.",
|
||||||
|
"Options.LogLevel.Trace": "Trace",
|
||||||
|
"Options.LogLevel.Debug": "Debug",
|
||||||
|
"Options.LogLevel.Info": "Info",
|
||||||
|
"Options.LogLevel.Warning": "Warning",
|
||||||
|
"Options.LogLevel.Error": "Error",
|
||||||
|
"Options.LogLevel.Critical": "Critical",
|
||||||
|
|
||||||
|
"Options.TraceImports.Label": "Limite tracciamento import",
|
||||||
|
"Options.TraceImports.Desc": "Traccia i primi N import per modulo (0 = disattivato).",
|
||||||
|
|
||||||
|
|
||||||
|
"Options.LogToFile.Label": "Salva log su file",
|
||||||
|
"Options.LogToFile.Desc": "Duplica l'output dell'emulatore in un file di log.",
|
||||||
|
|
||||||
|
|
||||||
|
"Options.LogFilePath.Label": "Percorso file di log",
|
||||||
|
"Options.LogFilePath.Default": "Nessun percorso personalizzato — i log vengono salvati in user/logs accanto all'emulatore.",
|
||||||
|
"Options.LogFilePath.Select": "Seleziona…",
|
||||||
|
|
||||||
|
"Options.OverrideLogFile.Label": "Sovrascrivi file di log",
|
||||||
|
"Options.OverrideLogFile.Desc": "Usa esattamente il percorso specificato invece di aggiungere Title ID e timestamp.",
|
||||||
|
|
||||||
|
|
||||||
|
"Options.TitleMusic.Label": "Musica del titolo",
|
||||||
|
"Options.TitleMusic.Desc": "Riproduci in loop la musica di anteprima del gioco selezionato nella libreria.",
|
||||||
|
|
||||||
|
"Options.Discord.Label": "Presenza Discord",
|
||||||
|
"Options.Discord.Desc": "Mostra il gioco in esecuzione sul tuo profilo Discord.",
|
||||||
|
|
||||||
|
"Options.Language.Label": "Lingua dell'emulatore",
|
||||||
|
"Options.Language.Desc": "Lingua utilizzata in tutto il launcher. Viene applicata immediatamente.",
|
||||||
|
|
||||||
|
"Common.On": "On",
|
||||||
|
"Common.Off": "Off",
|
||||||
|
|
||||||
|
"Console.Title": "CONSOLE",
|
||||||
|
"Console.SearchWatermark": "Cerca...",
|
||||||
|
"Console.AutoScroll": "Scorrimento automatico",
|
||||||
|
"Console.Split": "Dividi",
|
||||||
|
"Console.Copy": "Copia",
|
||||||
|
"Console.Clear": "Cancella",
|
||||||
|
"Console.WindowTitle": "Console SharpEmu",
|
||||||
|
|
||||||
|
"Launch.NoGameSelected": "Nessun gioco selezionato",
|
||||||
|
"Launch.NoGameHint": "Scegli un gioco dalla libreria, oppure apri direttamente un eboot.bin.",
|
||||||
|
"Launch.Idle": "Inattivo",
|
||||||
|
"Launch.Console": "≡ Console",
|
||||||
|
"Launch.Launch": "▶ Avvia",
|
||||||
|
"Launch.Stop": "■ Ferma",
|
||||||
|
"Launch.Running": "In esecuzione — {0}",
|
||||||
|
"Launch.Stopping": "Arresto in corso…",
|
||||||
|
"Launch.Exited": "Terminato con codice {0} ({1})",
|
||||||
|
"Launch.ExeNotFound": "Eseguibile SharpEmu non trovato. Compila prima il progetto SharpEmu.CLI (dotnet build).",
|
||||||
|
"Launch.LogFile": "File di log: {0}",
|
||||||
|
"Launch.Command": "$ SharpEmu {0}",
|
||||||
|
"Launch.StartFailed": "Impossibile avviare l'emulatore: {0}",
|
||||||
|
"Launch.ProcessExited": "Processo terminato con codice {0} ({1}).",
|
||||||
|
|
||||||
|
|
||||||
|
"Exit.Ok": "OK",
|
||||||
|
"Exit.InvalidArguments": "argomenti non validi",
|
||||||
|
"Exit.EbootNotFound": "eboot non trovato",
|
||||||
|
"Exit.RuntimeException": "errore di runtime",
|
||||||
|
"Exit.EmulationError": "errore di emulazione",
|
||||||
|
"Exit.Unknown": "sconosciuto",
|
||||||
|
|
||||||
|
|
||||||
|
"Status.EmulatorLocating": "Emulatore: ricerca in corso…",
|
||||||
|
"Status.EmulatorPath": "Emulatore: {0}",
|
||||||
|
"Status.EmulatorNotFound": "Emulatore: eseguibile SharpEmu non trovato — compila prima SharpEmu.CLI.",
|
||||||
|
"Status.ScanningLibrary": "Scansione della libreria…",
|
||||||
|
"Status.AddFolderPrompt": "Aggiungi una cartella di giochi per popolare la libreria.",
|
||||||
|
"Status.LibraryScanned": "Libreria scansionata: {0} gioco/giochi in {1} cartella/e.",
|
||||||
|
"Status.CouldNotOpenFolder": "Impossibile aprire la cartella: {0}",
|
||||||
|
"Status.CopiedToClipboard": "{0} copiato negli appunti.",
|
||||||
|
"Status.RemovedFromLibrary": "“{0}” rimosso dalla libreria. Riaggiungi la sua cartella per ripristinarlo.",
|
||||||
|
"Status.Running": "In esecuzione: {0}",
|
||||||
|
"Status.Stopping": "Arresto in corso…",
|
||||||
|
"Status.Idle": "Inattivo",
|
||||||
|
|
||||||
|
"Clipboard.Path": "Percorso",
|
||||||
|
"Clipboard.TitleId": "ID Titolo",
|
||||||
|
|
||||||
|
"Discord.Playing": "Sta giocando a {0}",
|
||||||
|
"Discord.Browsing": "Sta esplorando la libreria",
|
||||||
|
|
||||||
|
"Dialog.ChooseGameFolder": "Scegli una cartella contenente giochi",
|
||||||
|
"Dialog.OpenExecutable": "Apri un eseguibile da avviare",
|
||||||
|
"Dialog.PsExecutables": "Eseguibili PS",
|
||||||
|
"Dialog.SaveLogFile": "Scegli dove salvare il file di log",
|
||||||
|
"Dialog.PlainTextFiles": "File di testo semplice",
|
||||||
|
"Dialog.LogFiles": "File di log"
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"_languageName": "日本語",
|
||||||
|
|
||||||
|
"Page.Library": "ライブラリ",
|
||||||
|
"Page.Options": "オプション",
|
||||||
|
"Page.GameCount.One": "ゲーム 1本",
|
||||||
|
"Page.GameCount.Other": "ゲーム {0}本",
|
||||||
|
|
||||||
|
"Library.SearchWatermark": "ライブラリを検索…",
|
||||||
|
"Library.AddFolder": "+ フォルダーを追加",
|
||||||
|
"Library.Rescan": "⟳ 再スキャン",
|
||||||
|
"Library.OpenFile": "ファイルを開く…",
|
||||||
|
|
||||||
|
"Library.Context.Launch": "起動",
|
||||||
|
"Library.Context.OpenFolder": "ゲームフォルダーを開く",
|
||||||
|
"Library.Context.CopyPath": "パスをコピー",
|
||||||
|
"Library.Context.CopyTitleId": "ゲームIDをコピー",
|
||||||
|
"Library.Context.Remove": "ライブラリから削除",
|
||||||
|
|
||||||
|
"Library.Empty.Title": "ライブラリが空です",
|
||||||
|
"Library.Empty.Hint": "開始するには、ゲームが含まれるフォルダーを追加してください。",
|
||||||
|
"Library.Empty.SearchTitle": "検索条件に一致するゲームが見つかりません",
|
||||||
|
"Library.Empty.SearchHint": "ライブラリに「{0}」と一致する項目はありません。",
|
||||||
|
"Library.Empty.AddFolder": "+ ゲームフォルダーを追加",
|
||||||
|
|
||||||
|
"Library.Loading": "ライブラリを読み込み中…",
|
||||||
|
|
||||||
|
"Options.General": "一般",
|
||||||
|
"Options.Section.Emulation": "エミュレーション",
|
||||||
|
"Options.Section.Logging": "ロギング",
|
||||||
|
"Options.Section.Launcher": "ランチャー",
|
||||||
|
|
||||||
|
"Options.CpuEngine.Label": "CPUエンジン",
|
||||||
|
"Options.CpuEngine.Desc": "ゲームコードを実行するために使用される実行エンジン。",
|
||||||
|
"Options.CpuEngine.Native": "ネイティブ",
|
||||||
|
|
||||||
|
"Options.Strict.Label": "厳格な動的ライブラリ解決",
|
||||||
|
"Options.Strict.Desc": "インポートされたシンボルが解決できない場合、起動を中断します。",
|
||||||
|
|
||||||
|
"Options.LogLevel.Label": "ログレベル",
|
||||||
|
"Options.LogLevel.Desc": "エミュレータコンソールに表示されるメッセージの詳細度。",
|
||||||
|
"Options.LogLevel.Trace": "トレース",
|
||||||
|
"Options.LogLevel.Debug": "デバッグ",
|
||||||
|
"Options.LogLevel.Info": "情報",
|
||||||
|
"Options.LogLevel.Warning": "警告",
|
||||||
|
"Options.LogLevel.Error": "エラー",
|
||||||
|
"Options.LogLevel.Critical": "致命的なエラー",
|
||||||
|
|
||||||
|
"Options.TraceImports.Label": "インポートトレース制限",
|
||||||
|
"Options.TraceImports.Desc": "各モジュールの最初のN個のインポートをトレースします(0 = 無効)。",
|
||||||
|
|
||||||
|
"Options.LogToFile.Label": "ファイルに保存",
|
||||||
|
"Options.LogToFile.Desc": "エミュレータの出力をログファイルにコピーします。",
|
||||||
|
|
||||||
|
"Options.LogFilePath.Label": "ログファイルのパス",
|
||||||
|
"Options.LogFilePath.Default": "カスタムパスなし — ログはエミュレータと同じ場所の user/logs フォルダーに保存されます。",
|
||||||
|
"Options.LogFilePath.Select": "選択…",
|
||||||
|
|
||||||
|
"Options.OverrideLogFile.Label": "ログファイルを上書き",
|
||||||
|
"Options.OverrideLogFile.Desc": "ゲームIDやタイムスタンプを追加せず、指定されたパスをそのまま使用します。",
|
||||||
|
|
||||||
|
"Options.TitleMusic.Label": "ゲーム内音楽",
|
||||||
|
"Options.TitleMusic.Desc": "ライブラリで選択したゲームのプレビュー音楽をループ再生します。",
|
||||||
|
|
||||||
|
"Options.Discord.Label": "Discordステータス表示",
|
||||||
|
"Options.Discord.Desc": "現在プレイ中のゲームをDiscordのプロフィールに表示します。",
|
||||||
|
|
||||||
|
"Options.Language.Label": "エミュレータの言語",
|
||||||
|
"Options.Language.Desc": "ランチャー全体で使用される言語。変更はすぐに適用されます。",
|
||||||
|
|
||||||
|
"Common.On": "オン",
|
||||||
|
"Common.Off": "オフ",
|
||||||
|
|
||||||
|
"Console.Title": "コンソール",
|
||||||
|
"Console.SearchWatermark": "検索…",
|
||||||
|
"Console.AutoScroll": "自動スクロール",
|
||||||
|
"Console.Split": "ウィンドウを分離",
|
||||||
|
"Console.Copy": "コピー",
|
||||||
|
"Console.Clear": "消去",
|
||||||
|
"Console.WindowTitle": "SharpEmu コンソール",
|
||||||
|
|
||||||
|
"Launch.NoGameSelected": "ゲームが選択されていません",
|
||||||
|
"Launch.NoGameHint": "ライブラリからゲームを選択するか、eboot.bin ファイルを直接開いてください。",
|
||||||
|
"Launch.Idle": "待機中",
|
||||||
|
"Launch.Console": "≡ コンソール",
|
||||||
|
"Launch.Launch": "▶ 起動",
|
||||||
|
"Launch.Stop": "■ 停止",
|
||||||
|
"Launch.Running": "実行中 — {0}",
|
||||||
|
"Launch.Stopping": "停止中…",
|
||||||
|
"Launch.Exited": "プロセスがコード {0} ({1}) で終了しました",
|
||||||
|
"Launch.ExeNotFound": "SharpEmuの実行ファイルが見つかりません。先に SharpEmu.CLI プロジェクトをビルドしてください(dotnet build)。",
|
||||||
|
"Launch.LogFile": "ログファイル: {0}",
|
||||||
|
"Launch.Command": "$ SharpEmu {0}",
|
||||||
|
"Launch.StartFailed": "エミュレータを起動できませんでした: {0}",
|
||||||
|
"Launch.ProcessExited": "プロセスがコード {0} ({1}) で終了しました。",
|
||||||
|
|
||||||
|
"Exit.Ok": "OK",
|
||||||
|
"Exit.InvalidArguments": "無効な引数",
|
||||||
|
"Exit.EbootNotFound": "eboot が見つかりません",
|
||||||
|
"Exit.RuntimeException": "ランタイム例外",
|
||||||
|
"Exit.EmulationError": "エミュレーションエラー",
|
||||||
|
"Exit.Unknown": "不明",
|
||||||
|
|
||||||
|
"Status.EmulatorLocating": "エミュレータ: 位置を検索中…",
|
||||||
|
"Status.EmulatorPath": "エミュレータ: {0}",
|
||||||
|
"Status.EmulatorNotFound": "エミュレータ: SharpEmuの実行ファイルが見つかりません — 先に SharpEmu.CLI をビルドしてください。",
|
||||||
|
"Status.ScanningLibrary": "ライブラリをスキャン中…",
|
||||||
|
"Status.AddFolderPrompt": "ライブラリに表示するゲームフォルダーを追加してください。",
|
||||||
|
"Status.LibraryScanned": "ライブラリのスキャン完了: {1} 個のフォルダーから {0} 本のゲームを検出。",
|
||||||
|
"Status.CouldNotOpenFolder": "フォルダーを開けませんでした: {0}",
|
||||||
|
"Status.CopiedToClipboard": "「{0}」をクリップボードにコピーしました。",
|
||||||
|
"Status.RemovedFromLibrary": "「{0}」がライブラリから削除されました。復元するにはフォルダーを再追加してください。",
|
||||||
|
"Status.Running": "{0} を実行中",
|
||||||
|
"Status.Stopping": "停止中…",
|
||||||
|
"Status.Idle": "待機中",
|
||||||
|
|
||||||
|
"Clipboard.Path": "パス",
|
||||||
|
"Clipboard.TitleId": "ゲームID",
|
||||||
|
|
||||||
|
"Discord.Playing": "{0} をプレイ中",
|
||||||
|
"Discord.Browsing": "ライブラリを閲覧中",
|
||||||
|
|
||||||
|
"Dialog.ChooseGameFolder": "ゲームが含まれるフォルダーを選択",
|
||||||
|
"Dialog.OpenExecutable": "起動する実行ファイルを開く",
|
||||||
|
"Dialog.PsExecutables": "PlayStation 実行ファイル",
|
||||||
|
"Dialog.SaveLogFile": "ログファイルの保存先を選択",
|
||||||
|
"Dialog.PlainTextFiles": "プレーンテキストファイル",
|
||||||
|
"Dialog.LogFiles": "ログファイル"
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"_languageName": "한국어",
|
||||||
|
|
||||||
|
"Page.Library": "라이브러리",
|
||||||
|
"Page.Options": "옵션",
|
||||||
|
"Page.GameCount.One": "게임 1개",
|
||||||
|
"Page.GameCount.Other": "게임 {0}개",
|
||||||
|
|
||||||
|
"Library.SearchWatermark": "라이브러리 검색…",
|
||||||
|
"Library.AddFolder": "+ 폴더 추가",
|
||||||
|
"Library.Rescan": "⟳ 다시 스캔",
|
||||||
|
"Library.OpenFile": "파일 열기…",
|
||||||
|
|
||||||
|
"Library.Context.Launch": "실행",
|
||||||
|
"Library.Context.OpenFolder": "게임 폴더 열기",
|
||||||
|
"Library.Context.CopyPath": "경로 복사",
|
||||||
|
"Library.Context.CopyTitleId": "게임 ID 복사",
|
||||||
|
"Library.Context.Remove": "라이브러리에서 제거",
|
||||||
|
|
||||||
|
"Library.Empty.Title": "라이브러리가 비어 있습니다",
|
||||||
|
"Library.Empty.Hint": "시작하려면 게임이 포함된 폴더를 추가하세요.",
|
||||||
|
"Library.Empty.SearchTitle": "검색 결과와 일치하는 게임이 없습니다",
|
||||||
|
"Library.Empty.SearchHint": "라이브러리에 '{0}'와(과) 일치하는 항목이 없습니다.",
|
||||||
|
"Library.Empty.AddFolder": "+ 게임 폴더 추가",
|
||||||
|
|
||||||
|
"Library.Loading": "라이브러리 불러오는 중…",
|
||||||
|
|
||||||
|
"Options.General": "일반",
|
||||||
|
"Options.Section.Emulation": "에뮬레이션",
|
||||||
|
"Options.Section.Logging": "로깅",
|
||||||
|
"Options.Section.Launcher": "런처",
|
||||||
|
|
||||||
|
"Options.CpuEngine.Label": "CPU 엔진",
|
||||||
|
"Options.CpuEngine.Desc": "게임 코드를 실행하는 데 사용되는 실행 엔진입니다.",
|
||||||
|
"Options.CpuEngine.Native": "네이티브",
|
||||||
|
|
||||||
|
"Options.Strict.Label": "엄격한 동적 라이브러리 해석",
|
||||||
|
"Options.Strict.Desc": "가져온 심볼을 해석할 수 없는 경우 실행을 중단합니다.",
|
||||||
|
|
||||||
|
"Options.LogLevel.Label": "로그 수준",
|
||||||
|
"Options.LogLevel.Desc": "에뮬레이터 콘솔에 표시할 메시지의 세부 정보 수준입니다.",
|
||||||
|
"Options.LogLevel.Trace": "트레이스",
|
||||||
|
"Options.LogLevel.Debug": "디버그",
|
||||||
|
"Options.LogLevel.Info": "정보",
|
||||||
|
"Options.LogLevel.Warning": "경고",
|
||||||
|
"Options.LogLevel.Error": "오류",
|
||||||
|
"Options.LogLevel.Critical": "치명적 오류",
|
||||||
|
|
||||||
|
"Options.TraceImports.Label": "가져오기 트레이스 한도",
|
||||||
|
"Options.TraceImports.Desc": "각 모듈의 처음 N개 가져오기를 트레이스합니다 (0 = 비활성화).",
|
||||||
|
|
||||||
|
"Options.LogToFile.Label": "파일로 저장",
|
||||||
|
"Options.LogToFile.Desc": "에뮬레이터 출력을 로그 파일에 복사합니다.",
|
||||||
|
|
||||||
|
"Options.LogFilePath.Label": "로그 파일 경로",
|
||||||
|
"Options.LogFilePath.Default": "사용자 지정 경로 없음 — 로그는 에뮬레이터 옆의 user/logs 폴더에 저장됩니다.",
|
||||||
|
"Options.LogFilePath.Select": "선택…",
|
||||||
|
|
||||||
|
"Options.OverrideLogFile.Label": "로그 파일 덮어쓰기",
|
||||||
|
"Options.OverrideLogFile.Desc": "게임 ID와 타임스탬프를 추가하는 대신 정확히 이 경로를 사용합니다.",
|
||||||
|
|
||||||
|
"Options.TitleMusic.Label": "게임 음악",
|
||||||
|
"Options.TitleMusic.Desc": "라이브러리에서 선택한 게임의 미리보기 음악을 반복 재생합니다.",
|
||||||
|
|
||||||
|
"Options.Discord.Label": "디스코드 상태 표시",
|
||||||
|
"Options.Discord.Desc": "디스코드 프로필에 현재 실행 중인 게임을 표시합니다.",
|
||||||
|
|
||||||
|
"Options.Language.Label": "에뮬레이터 언어",
|
||||||
|
"Options.Language.Desc": "런처 전체에 사용되는 언어입니다. 변경 사항은 즉시 적용됩니다.",
|
||||||
|
|
||||||
|
"Common.On": "켬",
|
||||||
|
"Common.Off": "끔",
|
||||||
|
|
||||||
|
"Console.Title": "콘솔",
|
||||||
|
"Console.SearchWatermark": "검색…",
|
||||||
|
"Console.AutoScroll": "자동 스크롤",
|
||||||
|
"Console.Split": "창 분리",
|
||||||
|
"Console.Copy": "복사",
|
||||||
|
"Console.Clear": "지우기",
|
||||||
|
"Console.WindowTitle": "SharpEmu 콘솔",
|
||||||
|
|
||||||
|
"Launch.NoGameSelected": "선택된 게임 없음",
|
||||||
|
"Launch.NoGameHint": "라이브러리에서 게임을 선택하거나 eboot.bin 파일을 직접 여세요.",
|
||||||
|
"Launch.Idle": "대기 중",
|
||||||
|
"Launch.Console": "≡ 콘솔",
|
||||||
|
"Launch.Launch": "▶ 실행",
|
||||||
|
"Launch.Stop": "■ 중지",
|
||||||
|
"Launch.Running": "실행 중 — {0}",
|
||||||
|
"Launch.Stopping": "중지 중…",
|
||||||
|
"Launch.Exited": "프로세스가 코드 {0} ({1})(으)로 종료되었습니다",
|
||||||
|
"Launch.ExeNotFound": "SharpEmu 실행 파일을 찾을 수 없습니다. 먼저 SharpEmu.CLI 프로젝트를 컴파일하세요 (dotnet build).",
|
||||||
|
"Launch.LogFile": "로그 파일: {0}",
|
||||||
|
"Launch.Command": "$ SharpEmu {0}",
|
||||||
|
"Launch.StartFailed": "에뮬레이터를 시작할 수 없습니다: {0}",
|
||||||
|
"Launch.ProcessExited": "프로세스가 코드 {0} ({1})(으)로 종료되었습니다.",
|
||||||
|
|
||||||
|
"Exit.Ok": "확인",
|
||||||
|
"Exit.InvalidArguments": "잘못된 인수",
|
||||||
|
"Exit.EbootNotFound": "eboot을 찾을 수 없음",
|
||||||
|
"Exit.RuntimeException": "런타임 예외",
|
||||||
|
"Exit.EmulationError": "에뮬레이션 오류",
|
||||||
|
"Exit.Unknown": "알 수 없음",
|
||||||
|
|
||||||
|
"Status.EmulatorLocating": "에뮬레이터: 위치 검색 중…",
|
||||||
|
"Status.EmulatorPath": "에뮬레이터: {0}",
|
||||||
|
"Status.EmulatorNotFound": "에뮬레이터: SharpEmu 실행 파일을 찾을 수 없습니다 — 먼저 SharpEmu.CLI를 컴파일하세요.",
|
||||||
|
"Status.ScanningLibrary": "라이브러리 스캔 중…",
|
||||||
|
"Status.AddFolderPrompt": "라이브러리를 채우려면 게임 폴더를 추가하세요.",
|
||||||
|
"Status.LibraryScanned": "라이브러리 스캔 완료: {1}개 폴더에서 {0}개 게임 발견.",
|
||||||
|
"Status.CouldNotOpenFolder": "폴더를 열 수 없습니다: {0}",
|
||||||
|
"Status.CopiedToClipboard": "{0}이(가) 클립보드에 복사되었습니다.",
|
||||||
|
"Status.RemovedFromLibrary": "'{0}'이(가) 라이브러리에서 제거되었습니다. 복구하려면 폴더를 다시 추가하세요.",
|
||||||
|
"Status.Running": "{0} 실행 중",
|
||||||
|
"Status.Stopping": "중지 중…",
|
||||||
|
"Status.Idle": "대기 중",
|
||||||
|
|
||||||
|
"Clipboard.Path": "경로",
|
||||||
|
"Clipboard.TitleId": "게임 ID",
|
||||||
|
|
||||||
|
"Discord.Playing": "{0} 플레이 중",
|
||||||
|
"Discord.Browsing": "라이브러리 둘러보는 중",
|
||||||
|
|
||||||
|
"Dialog.ChooseGameFolder": "게임이 포함된 폴더 선택",
|
||||||
|
"Dialog.OpenExecutable": "실행할 파일 열기",
|
||||||
|
"Dialog.PsExecutables": "PlayStation 실행 파일",
|
||||||
|
"Dialog.SaveLogFile": "로그 파일 저장 위치 선택",
|
||||||
|
"Dialog.PlainTextFiles": "일반 텍스트 파일",
|
||||||
|
"Dialog.LogFiles": "로그 파일"
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"_languageName": "Nederlands",
|
||||||
|
|
||||||
|
"Page.Library": "Bibliotheek",
|
||||||
|
"Page.Options": "Opties",
|
||||||
|
"Page.GameCount.One": "1 game",
|
||||||
|
"Page.GameCount.Other": "{0} games",
|
||||||
|
|
||||||
|
"Library.SearchWatermark": "Zoeken in bibliotheek…",
|
||||||
|
"Library.AddFolder": "+ Map toevoegen",
|
||||||
|
"Library.Rescan": "⟳ Opnieuw scannen",
|
||||||
|
"Library.OpenFile": "Bestand openen…",
|
||||||
|
|
||||||
|
"Library.Context.Launch": "Starten",
|
||||||
|
"Library.Context.OpenFolder": "Gamemap openen",
|
||||||
|
"Library.Context.CopyPath": "Pad kopiëren",
|
||||||
|
"Library.Context.CopyTitleId": "Titel-ID kopiëren",
|
||||||
|
"Library.Context.Remove": "Verwijderen uit bibliotheek",
|
||||||
|
|
||||||
|
"Library.Empty.Title": "Je bibliotheek is leeg",
|
||||||
|
"Library.Empty.Hint": "Voeg een map met je games toe om te beginnen.",
|
||||||
|
"Library.Empty.SearchTitle": "Geen games komen overeen met je zoekopdracht",
|
||||||
|
"Library.Empty.SearchHint": "Niets in de bibliotheek komt overeen met “{0}”.",
|
||||||
|
"Library.Empty.AddFolder": "+ Gamemap toevoegen",
|
||||||
|
|
||||||
|
"Library.Loading": "Bibliotheek laden…",
|
||||||
|
|
||||||
|
"Options.General": "Algemeen",
|
||||||
|
"Options.Section.Emulation": "EMULATIE",
|
||||||
|
"Options.Section.Logging": "LOGGING",
|
||||||
|
"Options.Section.Launcher": "LAUNCHER",
|
||||||
|
|
||||||
|
"Options.CpuEngine.Label": "CPU-engine",
|
||||||
|
"Options.CpuEngine.Desc": "Engine die wordt gebruikt om gamecode uit te voeren.",
|
||||||
|
"Options.CpuEngine.Native": "Native",
|
||||||
|
|
||||||
|
"Options.Strict.Label": "Strikte dynlib-resolutie",
|
||||||
|
"Options.Strict.Desc": "Laat het opstarten mislukken wanneer een geïmporteerd symbool niet kan worden opgelost.",
|
||||||
|
|
||||||
|
"Options.LogLevel.Label": "Logniveau",
|
||||||
|
"Options.LogLevel.Desc": "Uitgebreidheid van de console-uitvoer van de emulator.",
|
||||||
|
"Options.LogLevel.Trace": "Trace",
|
||||||
|
"Options.LogLevel.Debug": "Debug",
|
||||||
|
"Options.LogLevel.Info": "Info",
|
||||||
|
"Options.LogLevel.Warning": "Waarschuwing",
|
||||||
|
"Options.LogLevel.Error": "Fout",
|
||||||
|
"Options.LogLevel.Critical": "Kritiek",
|
||||||
|
|
||||||
|
"Options.TraceImports.Label": "Tracelimiet voor imports",
|
||||||
|
"Options.TraceImports.Desc": "Traceer de eerste N imports per module (0 = uit).",
|
||||||
|
|
||||||
|
"Options.LogToFile.Label": "Loggen naar bestand",
|
||||||
|
"Options.LogToFile.Desc": "Stuur de uitvoer van de emulator ook naar een logbestand.",
|
||||||
|
|
||||||
|
"Options.LogFilePath.Label": "Pad naar logbestand",
|
||||||
|
"Options.LogFilePath.Default": "Geen aangepast pad — logs komen terecht in user/logs naast de emulator.",
|
||||||
|
"Options.LogFilePath.Select": "Selecteren…",
|
||||||
|
|
||||||
|
"Options.OverrideLogFile.Label": "Logbestand overschrijven",
|
||||||
|
"Options.OverrideLogFile.Desc": "Gebruik het exacte bestandspad in plaats van de titel-ID en tijdstempel toe te voegen.",
|
||||||
|
|
||||||
|
"Options.TitleMusic.Label": "Titelmuziek",
|
||||||
|
"Options.TitleMusic.Desc": "Herhaal de voorbeeldmuziek van de geselecteerde game in de bibliotheek.",
|
||||||
|
|
||||||
|
"Options.Discord.Label": "Discord-aanwezigheid",
|
||||||
|
"Options.Discord.Desc": "Toon de actieve game op je Discord-profiel.",
|
||||||
|
|
||||||
|
"Options.Language.Label": "Taal van de emulator",
|
||||||
|
"Options.Language.Desc": "Taal die in de hele launcher wordt gebruikt. Wordt direct toegepast.",
|
||||||
|
|
||||||
|
"Common.On": "Aan",
|
||||||
|
"Common.Off": "Uit",
|
||||||
|
|
||||||
|
"Console.Title": "CONSOLE",
|
||||||
|
"Console.SearchWatermark": "Zoeken...",
|
||||||
|
"Console.AutoScroll": "Automatisch scrollen",
|
||||||
|
"Console.Split": "Splitsen",
|
||||||
|
"Console.Copy": "Kopiëren",
|
||||||
|
"Console.Clear": "Wissen",
|
||||||
|
"Console.WindowTitle": "SharpEmu-console",
|
||||||
|
|
||||||
|
"Launch.NoGameSelected": "Geen game geselecteerd",
|
||||||
|
"Launch.NoGameHint": "Kies een game uit de bibliotheek, of open direct een eboot.bin-bestand.",
|
||||||
|
"Launch.Idle": "Inactief",
|
||||||
|
"Launch.Console": "≡ Console",
|
||||||
|
"Launch.Launch": "▶ Starten",
|
||||||
|
"Launch.Stop": "■ Stoppen",
|
||||||
|
"Launch.Running": "Actief — {0}",
|
||||||
|
"Launch.Stopping": "Stoppen…",
|
||||||
|
"Launch.Exited": "Afgesloten met code {0} ({1})",
|
||||||
|
"Launch.ExeNotFound": "SharpEmu-uitvoerbaar bestand niet gevonden. Bouw eerst het SharpEmu.CLI-project (dotnet build).",
|
||||||
|
"Launch.LogFile": "Logbestand: {0}",
|
||||||
|
"Launch.Command": "$ SharpEmu {0}",
|
||||||
|
"Launch.StartFailed": "Starten van de emulator mislukt: {0}",
|
||||||
|
"Launch.ProcessExited": "Proces afgesloten met code {0} ({1}).",
|
||||||
|
|
||||||
|
"Exit.Ok": "OK",
|
||||||
|
"Exit.InvalidArguments": "ongeldige argumenten",
|
||||||
|
"Exit.EbootNotFound": "eboot niet gevonden",
|
||||||
|
"Exit.RuntimeException": "runtime-uitzondering",
|
||||||
|
"Exit.EmulationError": "emulatiefout",
|
||||||
|
"Exit.Unknown": "onbekend",
|
||||||
|
|
||||||
|
"Status.EmulatorLocating": "Emulator: zoeken…",
|
||||||
|
"Status.EmulatorPath": "Emulator: {0}",
|
||||||
|
"Status.EmulatorNotFound": "Emulator: SharpEmu-uitvoerbaar bestand niet gevonden — bouw eerst SharpEmu.CLI.",
|
||||||
|
"Status.ScanningLibrary": "Bibliotheek scannen…",
|
||||||
|
"Status.AddFolderPrompt": "Voeg een gamemap toe om de bibliotheek te vullen.",
|
||||||
|
"Status.LibraryScanned": "Bibliotheek gescand: {0} game(s) in {1} map(pen).",
|
||||||
|
"Status.CouldNotOpenFolder": "Kan map niet openen: {0}",
|
||||||
|
"Status.CopiedToClipboard": "{0} gekopieerd naar klembord.",
|
||||||
|
"Status.RemovedFromLibrary": "“{0}” verwijderd uit de bibliotheek. Voeg de map opnieuw toe om dit te herstellen.",
|
||||||
|
"Status.Running": "Actief {0}",
|
||||||
|
"Status.Stopping": "Stoppen…",
|
||||||
|
"Status.Idle": "Inactief",
|
||||||
|
|
||||||
|
"Clipboard.Path": "Pad",
|
||||||
|
"Clipboard.TitleId": "Titel-ID",
|
||||||
|
|
||||||
|
"Discord.Playing": "Speelt {0}",
|
||||||
|
"Discord.Browsing": "Bladert door de bibliotheek",
|
||||||
|
|
||||||
|
"Dialog.ChooseGameFolder": "Kies een map met games",
|
||||||
|
"Dialog.OpenExecutable": "Open een uitvoerbaar bestand om te starten",
|
||||||
|
"Dialog.PsExecutables": "PS-uitvoerbare bestanden",
|
||||||
|
"Dialog.SaveLogFile": "Selecteer waar het logbestand moet worden opgeslagen",
|
||||||
|
"Dialog.PlainTextFiles": "Platte tekstbestanden",
|
||||||
|
"Dialog.LogFiles": "Logbestanden"
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"_languageName": "Русский",
|
||||||
|
|
||||||
|
"Page.Library": "Библиотека",
|
||||||
|
"Page.Options": "Настройки",
|
||||||
|
"Page.GameCount.One": "1 игра",
|
||||||
|
"Page.GameCount.Other": "Игр: {0}",
|
||||||
|
|
||||||
|
"Library.SearchWatermark": "Поиск…",
|
||||||
|
"Library.AddFolder": "+ Добавить папку",
|
||||||
|
"Library.Rescan": "⟳ Сканировать",
|
||||||
|
"Library.OpenFile": "Открыть файл…",
|
||||||
|
|
||||||
|
"Library.Context.Launch": "Запустить",
|
||||||
|
"Library.Context.OpenFolder": "Открыть папку с игрой",
|
||||||
|
"Library.Context.CopyPath": "Скопировать путь",
|
||||||
|
"Library.Context.CopyTitleId": "Скопировать ID игры",
|
||||||
|
"Library.Context.Remove": "Удалить из библиотеки",
|
||||||
|
|
||||||
|
"Library.Empty.Title": "Ваша библиотека пуста",
|
||||||
|
"Library.Empty.Hint": "Добавьте папку с играми, чтобы начать.",
|
||||||
|
"Library.Empty.SearchTitle": "По вашему запросу ничего не найдено",
|
||||||
|
"Library.Empty.SearchHint": "В библиотеке нет игр, соответствующих запросу «{0}».",
|
||||||
|
"Library.Empty.AddFolder": "+ Добавить папку с играми",
|
||||||
|
|
||||||
|
"Library.Loading": "Загрузка библиотеки…",
|
||||||
|
|
||||||
|
"Options.General": "Основные",
|
||||||
|
"Options.Section.Emulation": "ЭМУЛЯЦИЯ",
|
||||||
|
"Options.Section.Logging": "ЛОГГИРОВАНИЕ",
|
||||||
|
"Options.Section.Launcher": "ЛАУНЧЕР",
|
||||||
|
|
||||||
|
"Options.CpuEngine.Label": "Движок ЦП",
|
||||||
|
"Options.CpuEngine.Desc": "Движок выполнения, используемый для запуска игрового кода.",
|
||||||
|
"Options.CpuEngine.Native": "Нативный",
|
||||||
|
|
||||||
|
"Options.Strict.Label": "Строгое разрешение динамических библиотек",
|
||||||
|
"Options.Strict.Desc": "Не запускать игру, если не удается найти или связать импортируемую функцию/переменную.",
|
||||||
|
|
||||||
|
"Options.LogLevel.Label": "Уровень логгирования",
|
||||||
|
"Options.LogLevel.Desc": "Подробность вывода в консоль эмулятора.",
|
||||||
|
"Options.LogLevel.Trace": "Trace",
|
||||||
|
"Options.LogLevel.Debug": "Debug",
|
||||||
|
"Options.LogLevel.Info": "Info",
|
||||||
|
"Options.LogLevel.Warning": "Warning",
|
||||||
|
"Options.LogLevel.Error": "Error",
|
||||||
|
"Options.LogLevel.Critical": "Critical",
|
||||||
|
|
||||||
|
"Options.TraceImports.Label": "Лимит трассировки импортов",
|
||||||
|
"Options.TraceImports.Desc": "Трассировать первые N импортов в каждом модуле (0 - выключено).",
|
||||||
|
|
||||||
|
"Options.LogToFile.Label": "Запись лога в файл",
|
||||||
|
"Options.LogToFile.Desc": "Дублировать вывод эмулятора в файл лога.",
|
||||||
|
|
||||||
|
"Options.LogFilePath.Label": "Путь к файлу лога",
|
||||||
|
"Options.LogFilePath.Default": "Пользовательский путь не задан: логи сохраняются в user/logs рядом с эмулятором.",
|
||||||
|
"Options.LogFilePath.Select": "Выбрать…",
|
||||||
|
|
||||||
|
"Options.OverrideLogFile.Label": "Переопределить файл лога",
|
||||||
|
"Options.OverrideLogFile.Desc": "Использовать указанный путь к файлу без добавления ID игры и метки времени.",
|
||||||
|
|
||||||
|
"Options.TitleMusic.Label": "Музыка игры",
|
||||||
|
"Options.TitleMusic.Desc": "Зацикленно воспроизводить музыку предпросмотра выбранной игры в библиотеке.",
|
||||||
|
|
||||||
|
"Options.Discord.Label": "Статус Discord",
|
||||||
|
"Options.Discord.Desc": "Показывать запущенную игру в профиле Discord.",
|
||||||
|
|
||||||
|
"Options.Language.Label": "Язык эмулятора",
|
||||||
|
"Options.Language.Desc": "Язык интерфейса лаунчера. Изменение применяется сразу.",
|
||||||
|
|
||||||
|
"Common.On": "Включено",
|
||||||
|
"Common.Off": "Выключено",
|
||||||
|
|
||||||
|
"Console.Title": "КОНСОЛЬ",
|
||||||
|
"Console.SearchWatermark": "Поиск...",
|
||||||
|
"Console.AutoScroll": "Авто-прокрутка",
|
||||||
|
"Console.Split": "Разделить",
|
||||||
|
"Console.Copy": "Скопировать",
|
||||||
|
"Console.Clear": "Очистить",
|
||||||
|
"Console.WindowTitle": "Консоль SharpEmu",
|
||||||
|
|
||||||
|
"Launch.NoGameSelected": "Ничего не выбрано",
|
||||||
|
"Launch.NoGameHint": "Выберите игру из библиотеки или откройте eboot.bin напрямую.",
|
||||||
|
"Launch.Idle": "Ожидание",
|
||||||
|
"Launch.Console": "≡ Консоль",
|
||||||
|
"Launch.Launch": "▶ Запустить",
|
||||||
|
"Launch.Stop": "■ Остановить",
|
||||||
|
"Launch.Running": "Запущено - {0}",
|
||||||
|
"Launch.Stopping": "Остановка…",
|
||||||
|
"Launch.Exited": "Завершено с кодом {0} ({1})",
|
||||||
|
"Launch.ExeNotFound": "Исполняемый файл SharpEmu не найден. Скомпилируйте сначала проект SharpEmu.CLI (dotnet build).",
|
||||||
|
"Launch.LogFile": "Лог: {0}",
|
||||||
|
"Launch.Command": "$ SharpEmu {0}",
|
||||||
|
"Launch.StartFailed": "Не удалось запустить эмулятор: {0}",
|
||||||
|
"Launch.ProcessExited": "Процесс завершился с кодом {0} ({1}).",
|
||||||
|
|
||||||
|
"Exit.Ok": "OK",
|
||||||
|
"Exit.InvalidArguments": "некорректные аргументы",
|
||||||
|
"Exit.EbootNotFound": "eboot не найден",
|
||||||
|
"Exit.RuntimeException": "ошибка выполнения",
|
||||||
|
"Exit.EmulationError": "ошибка эмуляции",
|
||||||
|
"Exit.Unknown": "неизвестная ошибка",
|
||||||
|
|
||||||
|
"Status.EmulatorLocating": "Эмулятор: поиск…",
|
||||||
|
"Status.EmulatorPath": "Эмулятор: {0}",
|
||||||
|
"Status.EmulatorNotFound": "Эмулятор: исполняемый файл SharpEmu не был найден - скомпилируйте сначала SharpEmu.CLI.",
|
||||||
|
"Status.ScanningLibrary": "Сканирование библиотеки…",
|
||||||
|
"Status.AddFolderPrompt": "Добавьте папку с играми, чтобы заполнить библиотеку.",
|
||||||
|
"Status.LibraryScanned": "Сканирование библиотеки завершено: игр: {0}, папок: {1}.",
|
||||||
|
"Status.CouldNotOpenFolder": "Не удалось открыть папку: {0}",
|
||||||
|
"Status.CopiedToClipboard": "{0}: скопировано в буфер обмена.",
|
||||||
|
"Status.RemovedFromLibrary": "Игра «{0}» удалена из библиотеки. Добавьте её папку заново, чтобы вернуть.",
|
||||||
|
"Status.Running": "Запущено: {0}",
|
||||||
|
"Status.Stopping": "Остановка…",
|
||||||
|
"Status.Idle": "Ожидание",
|
||||||
|
|
||||||
|
"Clipboard.Path": "Путь",
|
||||||
|
"Clipboard.TitleId": "ID игры",
|
||||||
|
|
||||||
|
"Discord.Playing": "Играет в {0}",
|
||||||
|
"Discord.Browsing": "Просматривает библиотеку",
|
||||||
|
|
||||||
|
"Dialog.ChooseGameFolder": "Выберите папку, содержащую игры",
|
||||||
|
"Dialog.OpenExecutable": "Открыть исполняемый файл для запуска",
|
||||||
|
"Dialog.PsExecutables": "Исполняемые файлы PS",
|
||||||
|
"Dialog.SaveLogFile": "Выберите, куда сохранить файл с логами",
|
||||||
|
"Dialog.PlainTextFiles": "Текстовые файлы",
|
||||||
|
"Dialog.LogFiles": "Логи"
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"_languageName": "Türkçe",
|
||||||
|
|
||||||
|
"Page.Library": "Kütüphane",
|
||||||
|
"Page.Options": "Ayarlar",
|
||||||
|
"Page.GameCount.One": "1 oyun",
|
||||||
|
"Page.GameCount.Other": "{0} oyun",
|
||||||
|
|
||||||
|
"Library.SearchWatermark": "Kütüphanede ara…",
|
||||||
|
"Library.AddFolder": "+ Klasör ekle",
|
||||||
|
"Library.Rescan": "⟳ Yeniden tara",
|
||||||
|
"Library.OpenFile": "Dosya aç…",
|
||||||
|
|
||||||
|
"Library.Context.Launch": "Başlat",
|
||||||
|
"Library.Context.OpenFolder": "Oyun klasörünü aç",
|
||||||
|
"Library.Context.CopyPath": "Yolu kopyala",
|
||||||
|
"Library.Context.CopyTitleId": "Title ID'yi kopyala",
|
||||||
|
"Library.Context.Remove": "Kütüphaneden kaldır",
|
||||||
|
|
||||||
|
"Library.Empty.Title": "Kütüphaneniz boş",
|
||||||
|
"Library.Empty.Hint": "Başlamak için oyunlarınızı içeren bir klasör ekleyin.",
|
||||||
|
"Library.Empty.SearchTitle": "Aramanızla eşleşen oyun yok",
|
||||||
|
"Library.Empty.SearchHint": "Kütüphanede “{0}” ile eşleşen bir şey yok.",
|
||||||
|
"Library.Empty.AddFolder": "+ Oyun klasörü ekle",
|
||||||
|
|
||||||
|
"Library.Loading": "Kütüphane yükleniyor…",
|
||||||
|
|
||||||
|
"Options.General": "Genel",
|
||||||
|
"Options.Section.Emulation": "EMÜLASYON",
|
||||||
|
"Options.Section.Logging": "GÜNLÜKLEME",
|
||||||
|
"Options.Section.Launcher": "BAŞLATICI",
|
||||||
|
|
||||||
|
"Options.CpuEngine.Label": "CPU motoru",
|
||||||
|
"Options.CpuEngine.Desc": "Oyun kodunu çalıştırmak için kullanılan yürütme motoru.",
|
||||||
|
"Options.CpuEngine.Native": "Native",
|
||||||
|
|
||||||
|
"Options.Strict.Label": "Katı dynlib çözümü",
|
||||||
|
"Options.Strict.Desc": "İçe aktarılan bir sembol çözümlenemediğinde başlatmayı başarısız kılar.",
|
||||||
|
|
||||||
|
"Options.LogLevel.Label": "Günlük seviyesi",
|
||||||
|
"Options.LogLevel.Desc": "Emülatör konsol çıktısının ayrıntı düzeyi.",
|
||||||
|
"Options.LogLevel.Trace": "Trace",
|
||||||
|
"Options.LogLevel.Debug": "Debug",
|
||||||
|
"Options.LogLevel.Info": "Info",
|
||||||
|
"Options.LogLevel.Warning": "Warning",
|
||||||
|
"Options.LogLevel.Error": "Error",
|
||||||
|
"Options.LogLevel.Critical": "Critical",
|
||||||
|
|
||||||
|
"Options.TraceImports.Label": "Import izleme sınırı",
|
||||||
|
"Options.TraceImports.Desc": "Modül başına ilk N import'u izle (0 = kapalı).",
|
||||||
|
|
||||||
|
"Options.LogToFile.Label": "Dosyaya günlükle",
|
||||||
|
"Options.LogToFile.Desc": "Emülatör çıktısını bir günlük dosyasına yansıt.",
|
||||||
|
|
||||||
|
"Options.LogFilePath.Label": "Günlük dosyası yolu",
|
||||||
|
"Options.LogFilePath.Default": "Özel yol yok — günlükler emülatörün yanındaki user/logs klasörüne yazılır.",
|
||||||
|
"Options.LogFilePath.Select": "Seç…",
|
||||||
|
|
||||||
|
"Options.OverrideLogFile.Label": "Günlük dosyasının üzerine yaz",
|
||||||
|
"Options.OverrideLogFile.Desc": "Title ID ve zaman damgası eklemek yerine tam dosya yolunu kullan.",
|
||||||
|
|
||||||
|
"Options.TitleMusic.Label": "Oyun müziği",
|
||||||
|
"Options.TitleMusic.Desc": "Kütüphanede seçili oyunun önizleme müziğini döngüye al.",
|
||||||
|
|
||||||
|
"Options.Discord.Label": "Discord durumu",
|
||||||
|
"Options.Discord.Desc": "Discord profilinde çalışan oyunu göster.",
|
||||||
|
|
||||||
|
"Options.Language.Label": "Emülatör dili",
|
||||||
|
"Options.Language.Desc": "Başlatıcı genelinde kullanılan dil. Hemen uygulanır.",
|
||||||
|
|
||||||
|
"Common.On": "Açık",
|
||||||
|
"Common.Off": "Kapalı",
|
||||||
|
|
||||||
|
"Console.Title": "KONSOL",
|
||||||
|
"Console.SearchWatermark": "Ara...",
|
||||||
|
"Console.AutoScroll": "Otomatik kaydır",
|
||||||
|
"Console.Split": "Ayır",
|
||||||
|
"Console.Copy": "Kopyala",
|
||||||
|
"Console.Clear": "Temizle",
|
||||||
|
"Console.WindowTitle": "SharpEmu Konsol",
|
||||||
|
|
||||||
|
"Launch.NoGameSelected": "Oyun seçilmedi",
|
||||||
|
"Launch.NoGameHint": "Kütüphaneden bir oyun seçin veya doğrudan bir eboot.bin açın.",
|
||||||
|
"Launch.Idle": "Boşta",
|
||||||
|
"Launch.Console": "≡ Konsol",
|
||||||
|
"Launch.Launch": "▶ Başlat",
|
||||||
|
"Launch.Stop": "■ Durdur",
|
||||||
|
"Launch.Running": "Çalışıyor — {0}",
|
||||||
|
"Launch.Stopping": "Durduruluyor…",
|
||||||
|
"Launch.Exited": "Çıkış kodu {0} ({1})",
|
||||||
|
"Launch.ExeNotFound": "SharpEmu çalıştırılabilir dosyası bulunamadı. Önce SharpEmu.CLI projesini derleyin (dotnet build).",
|
||||||
|
"Launch.LogFile": "Günlük dosyası: {0}",
|
||||||
|
"Launch.Command": "$ SharpEmu {0}",
|
||||||
|
"Launch.StartFailed": "Emülatör başlatılamadı: {0}",
|
||||||
|
"Launch.ProcessExited": "İşlem {0} kodla sonlandı ({1}).",
|
||||||
|
|
||||||
|
"Exit.Ok": "Tamam",
|
||||||
|
"Exit.InvalidArguments": "geçersiz argümanlar",
|
||||||
|
"Exit.EbootNotFound": "eboot bulunamadı",
|
||||||
|
"Exit.RuntimeException": "çalışma zamanı hatası",
|
||||||
|
"Exit.EmulationError": "emülasyon hatası",
|
||||||
|
"Exit.Unknown": "bilinmiyor",
|
||||||
|
|
||||||
|
"Status.EmulatorLocating": "Emülatör: bulunuyor…",
|
||||||
|
"Status.EmulatorPath": "Emülatör: {0}",
|
||||||
|
"Status.EmulatorNotFound": "Emülatör: SharpEmu çalıştırılabilir dosyası bulunamadı — önce SharpEmu.CLI'yi derleyin.",
|
||||||
|
"Status.ScanningLibrary": "Kütüphane taranıyor…",
|
||||||
|
"Status.AddFolderPrompt": "Kütüphaneyi doldurmak için bir oyun klasörü ekleyin.",
|
||||||
|
"Status.LibraryScanned": "Kütüphane tarandı: {1} klasörde {0} oyun.",
|
||||||
|
"Status.CouldNotOpenFolder": "Klasör açılamadı: {0}",
|
||||||
|
"Status.CopiedToClipboard": "{0} panoya kopyalandı.",
|
||||||
|
"Status.RemovedFromLibrary": "“{0}” kütüphaneden kaldırıldı. Geri getirmek için klasörünü yeniden ekleyin.",
|
||||||
|
"Status.Running": "{0} çalışıyor",
|
||||||
|
"Status.Stopping": "Durduruluyor…",
|
||||||
|
"Status.Idle": "Boşta",
|
||||||
|
|
||||||
|
"Clipboard.Path": "Yol",
|
||||||
|
"Clipboard.TitleId": "Title ID",
|
||||||
|
|
||||||
|
"Discord.Playing": "{0} oynuyor",
|
||||||
|
"Discord.Browsing": "Kütüphaneye göz atıyor",
|
||||||
|
|
||||||
|
"Dialog.ChooseGameFolder": "Oyunlarınızı içeren bir klasör seçin",
|
||||||
|
"Dialog.OpenExecutable": "Başlatılacak bir çalıştırılabilir dosya aç",
|
||||||
|
"Dialog.PsExecutables": "PS çalıştırılabilirleri",
|
||||||
|
"Dialog.SaveLogFile": "Günlük dosyasının kaydedileceği yeri seçin",
|
||||||
|
"Dialog.PlainTextFiles": "Düz Metin Dosyaları",
|
||||||
|
"Dialog.LogFiles": "Günlük Dosyaları"
|
||||||
|
}
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace SharpEmu.GUI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads UI strings for the launcher. Every language ships embedded in the
|
||||||
|
/// assembly (see SharpEmu.GUI.csproj) so a release build is fully
|
||||||
|
/// self-contained; an optional Languages/<code>.json file next to the
|
||||||
|
/// executable overrides the embedded copy for that code, so a translation
|
||||||
|
/// fix or a brand-new language never needs a rebuild.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class Localization
|
||||||
|
{
|
||||||
|
public static Localization Instance { get; } = new();
|
||||||
|
|
||||||
|
public sealed record LanguageInfo(string Code, string NativeName);
|
||||||
|
|
||||||
|
private const string EmbeddedResourcePrefix = "Languages.";
|
||||||
|
private const string EmbeddedResourceSuffix = ".json";
|
||||||
|
|
||||||
|
private Dictionary<string, string> _strings = new();
|
||||||
|
private Dictionary<string, string> _fallbackStrings = new();
|
||||||
|
|
||||||
|
|
||||||
|
private Localization()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Directory holding optional *.json language overrides, next to the executable.</summary>
|
||||||
|
public static string LanguagesDirectory => Path.Combine(AppContext.BaseDirectory, "Languages");
|
||||||
|
|
||||||
|
public string CurrentCode { get; private set; } = "en";
|
||||||
|
|
||||||
|
public string Get(string key)
|
||||||
|
{
|
||||||
|
if (_strings.TryGetValue(key, out var value))
|
||||||
|
return value;
|
||||||
|
|
||||||
|
if (_fallbackStrings.TryGetValue(key, out var fallbackValue))
|
||||||
|
return fallbackValue;
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Format(string key, params object?[] args) => string.Format(Get(key), args);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Languages available either embedded in the binary or as a loose
|
||||||
|
/// override file, sorted by code. A loose file's declared name wins when
|
||||||
|
/// the same code exists in both places.
|
||||||
|
/// </summary>
|
||||||
|
public List<LanguageInfo> DiscoverLanguages()
|
||||||
|
{
|
||||||
|
var languages = new Dictionary<string, LanguageInfo>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var code in EmbeddedLanguageCodes())
|
||||||
|
{
|
||||||
|
using var stream = OpenEmbeddedLanguageStream(code);
|
||||||
|
if (stream is not null)
|
||||||
|
{
|
||||||
|
languages[code] = new LanguageInfo(code, ReadLanguageName(stream) ?? code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var file in Directory.EnumerateFiles(LanguagesDirectory, "*.json"))
|
||||||
|
{
|
||||||
|
var code = Path.GetFileNameWithoutExtension(file);
|
||||||
|
using var stream = File.OpenRead(file);
|
||||||
|
languages[code] = new LanguageInfo(code, ReadLanguageName(stream) ?? code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// No loose Languages directory: the embedded languages still stand.
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = languages.Values.ToList();
|
||||||
|
result.Sort((a, b) => string.CompareOrdinal(a.Code, b.Code));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Loads a language by code (e.g. "en"): a loose override file first, then the embedded copy.</summary>
|
||||||
|
/// english is the fallback language
|
||||||
|
public void Load(string code)
|
||||||
|
{
|
||||||
|
if (_fallbackStrings.Count == 0 && !string.Equals(code, "en", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (!TryLoadLooseFile("en", out var fallback) && !TryLoadEmbedded("en", out fallback))
|
||||||
|
{
|
||||||
|
fallback = new Dictionary<string, string>();
|
||||||
|
}
|
||||||
|
_fallbackStrings = fallback;
|
||||||
|
}
|
||||||
|
else if (string.Equals(code, "en", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (TryLoadLooseFile("en", out var enDict) || TryLoadEmbedded("en", out enDict))
|
||||||
|
{
|
||||||
|
_strings = enDict;
|
||||||
|
_fallbackStrings = enDict;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_strings = new Dictionary<string, string>();
|
||||||
|
_fallbackStrings = new Dictionary<string, string>();
|
||||||
|
}
|
||||||
|
CurrentCode = "en";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the requested language
|
||||||
|
if (TryLoadLooseFile(code, out var loaded) || TryLoadEmbedded(code, out loaded))
|
||||||
|
{
|
||||||
|
_strings = loaded;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (_fallbackStrings.Count > 0)
|
||||||
|
_strings = new Dictionary<string, string>(_fallbackStrings);
|
||||||
|
else
|
||||||
|
_strings = new Dictionary<string, string>();
|
||||||
|
}
|
||||||
|
CurrentCode = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<string> EmbeddedLanguageCodes()
|
||||||
|
{
|
||||||
|
foreach (var name in typeof(Localization).Assembly.GetManifestResourceNames())
|
||||||
|
{
|
||||||
|
if (name.StartsWith(EmbeddedResourcePrefix, StringComparison.Ordinal) &&
|
||||||
|
name.EndsWith(EmbeddedResourceSuffix, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
yield return name[EmbeddedResourcePrefix.Length..^EmbeddedResourceSuffix.Length];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream? OpenEmbeddedLanguageStream(string code) =>
|
||||||
|
typeof(Localization).Assembly.GetManifestResourceStream($"{EmbeddedResourcePrefix}{code}{EmbeddedResourceSuffix}");
|
||||||
|
|
||||||
|
private static string? ReadLanguageName(Stream stream)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var document = JsonDocument.Parse(stream);
|
||||||
|
if (document.RootElement.TryGetProperty("_languageName", out var name) &&
|
||||||
|
name.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
return name.GetString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Malformed file: fall back to the code as its own display name.
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryLoadLooseFile(string code)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var path = Path.Combine(LanguagesDirectory, $"{code}.json");
|
||||||
|
return File.Exists(path) && TryLoad(code, File.ReadAllText(path));
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryLoadEmbedded(string code)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = OpenEmbeddedLanguageStream(code);
|
||||||
|
if (stream is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var reader = new StreamReader(stream);
|
||||||
|
return TryLoad(code, reader.ReadToEnd());
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryLoadLooseFile(string code, out Dictionary<string, string> result)
|
||||||
|
{
|
||||||
|
result = new Dictionary<string, string>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var path = Path.Combine(LanguagesDirectory, $"{code}.json");
|
||||||
|
if (!File.Exists(path))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var json = File.ReadAllText(path);
|
||||||
|
return TryLoad(json, out result);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryLoadEmbedded(string code, out Dictionary<string, string> result)
|
||||||
|
{
|
||||||
|
result = new Dictionary<string, string>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = OpenEmbeddedLanguageStream(code);
|
||||||
|
if (stream is null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
using var reader = new StreamReader(stream);
|
||||||
|
var json = reader.ReadToEnd();
|
||||||
|
return TryLoad(json, out result);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryLoad(string json, out Dictionary<string, string> result)
|
||||||
|
{
|
||||||
|
var loaded = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
|
||||||
|
if (loaded is null)
|
||||||
|
{
|
||||||
|
result = new Dictionary<string, string>();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = loaded;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryLoad(string code, string json)
|
||||||
|
{
|
||||||
|
if (TryLoad(json, out var dict))
|
||||||
|
{
|
||||||
|
_strings = dict;
|
||||||
|
CurrentCode = code;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
+364
-157
@@ -9,6 +9,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
Title="SharpEmu"
|
Title="SharpEmu"
|
||||||
Width="1280" Height="820"
|
Width="1280" Height="820"
|
||||||
MinWidth="980" MinHeight="640"
|
MinWidth="980" MinHeight="640"
|
||||||
|
WindowState="Maximized"
|
||||||
WindowStartupLocation="CenterScreen"
|
WindowStartupLocation="CenterScreen"
|
||||||
Background="{StaticResource BgBrush}"
|
Background="{StaticResource BgBrush}"
|
||||||
ExtendClientAreaToDecorationsHint="True"
|
ExtendClientAreaToDecorationsHint="True"
|
||||||
@@ -17,7 +18,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
Icon="avares://SharpEmu.GUI/Assets/SharpEmu.ico"
|
Icon="avares://SharpEmu.GUI/Assets/SharpEmu.ico"
|
||||||
KeyDown="OnKeyDown">
|
KeyDown="OnKeyDown">
|
||||||
|
|
||||||
<Grid RowDefinitions="44,*,32">
|
<Grid RowDefinitions="Auto,*,Auto">
|
||||||
|
|
||||||
<!-- Selected-game backdrop: key art behind the main content, dimmed by
|
<!-- Selected-game backdrop: key art behind the main content, dimmed by
|
||||||
a scrim so tiles and text stay readable. Fades on selection. -->
|
a scrim so tiles and text stay readable. Fades on selection. -->
|
||||||
@@ -41,8 +42,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</Border>
|
</Border>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<!-- Title bar -->
|
<!-- Title bar; hidden in fullscreen (F11) along with the status bar, so
|
||||||
<Grid x:Name="TitleBar" Grid.Row="0" Background="{StaticResource ChromeBrush}">
|
the game gets the whole screen. -->
|
||||||
|
<Grid x:Name="TitleBar" Grid.Row="0" Height="44" Background="{StaticResource ChromeBrush}">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="10" Margin="16,0" VerticalAlignment="Center">
|
<StackPanel Orientation="Horizontal" Spacing="10" Margin="16,0" VerticalAlignment="Center">
|
||||||
<Image Source="avares://SharpEmu.GUI/Assets/SharpEmu.ico" Width="20" Height="20"
|
<Image Source="avares://SharpEmu.GUI/Assets/SharpEmu.ico" Width="20" Height="20"
|
||||||
RenderOptions.BitmapInterpolationMode="HighQuality" />
|
RenderOptions.BitmapInterpolationMode="HighQuality" />
|
||||||
@@ -54,106 +56,339 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Main content -->
|
<!-- Main content -->
|
||||||
<Grid Grid.Row="1" Margin="18,14,18,14" RowDefinitions="Auto,*,Auto,Auto">
|
<Grid Grid.Row="1" Margin="32,24,32,20" RowDefinitions="Auto,*,Auto,Auto">
|
||||||
|
|
||||||
<!-- Library toolbar -->
|
<!-- Library / Options page switcher, with the library toolbar sharing
|
||||||
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,*,Auto,Auto,Auto,Auto" Margin="6,0,6,12">
|
the same row on the right. Plain buttons (not TabItem) so there is
|
||||||
<TextBlock Grid.Column="0" Text="Library" FontSize="22" FontWeight="Bold" VerticalAlignment="Center" />
|
no underline; LB/RB hint chips flank the pair and the gamepad's
|
||||||
<Border Grid.Column="1" Classes="pill" Margin="12,2,0,0" VerticalAlignment="Center">
|
shoulder buttons actually switch pages from anywhere. -->
|
||||||
<TextBlock x:Name="GameCountText" Text="0 games" FontSize="11" Foreground="{StaticResource MutedBrush}" />
|
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto" Margin="0,0,0,20">
|
||||||
</Border>
|
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="14" VerticalAlignment="Center">
|
||||||
<TextBox Grid.Column="3" x:Name="SearchBox" Watermark="Search library…" Width="280"
|
<Border Classes="padHint" VerticalAlignment="Center">
|
||||||
VerticalAlignment="Center" Margin="0,0,12,0" />
|
<TextBlock Text="LB" FontSize="11" FontWeight="Bold" Foreground="{StaticResource MutedBrush}" />
|
||||||
<Button Grid.Column="4" x:Name="AddFolderButton" Classes="ghost" Content="+ Add folder"
|
</Border>
|
||||||
VerticalAlignment="Center" Margin="0,0,8,0" />
|
<Button x:Name="LibraryTabButton" Classes="segment active" Content="Library" />
|
||||||
<Button Grid.Column="5" x:Name="RescanButton" Classes="ghost" Content="⟳ Rescan"
|
<Button x:Name="OptionsTabButton" Classes="segment" Content="Options" />
|
||||||
VerticalAlignment="Center" Margin="0,0,8,0" />
|
<Border Classes="padHint" VerticalAlignment="Center">
|
||||||
<Button Grid.Column="6" x:Name="OpenFileButton" Classes="ghost" Content="Open file…"
|
<TextBlock Text="RB" FontSize="11" FontWeight="Bold" Foreground="{StaticResource MutedBrush}" />
|
||||||
VerticalAlignment="Center" />
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="2" x:Name="LibraryToolbar" Orientation="Horizontal" Spacing="8"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
<TextBox x:Name="SearchBox" Watermark="Search library…" Width="240" VerticalAlignment="Center" />
|
||||||
|
<Button x:Name="AddFolderButton" Classes="ghost" Content="+ Add folder" VerticalAlignment="Center" />
|
||||||
|
<Button x:Name="RescanButton" Classes="ghost" Content="⟳ Rescan" VerticalAlignment="Center" />
|
||||||
|
<Button x:Name="OpenFileButton" Classes="ghost" Content="Open file…" VerticalAlignment="Center" />
|
||||||
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Cover-art grid -->
|
|
||||||
<Panel Grid.Row="1">
|
<Panel Grid.Row="1">
|
||||||
<ListBox x:Name="GameList" Classes="tileGrid" Background="Transparent"
|
|
||||||
SelectionMode="Single" Padding="0">
|
|
||||||
<ListBox.ContextMenu>
|
|
||||||
<ContextMenu x:Name="GameContextMenu" Placement="Pointer">
|
|
||||||
<MenuItem x:Name="CtxLaunch" Header="Launch" FontWeight="SemiBold">
|
|
||||||
<MenuItem.Icon>
|
|
||||||
<TextBlock Text="▶" FontSize="11" Foreground="{StaticResource AccentHoverBrush}"
|
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
|
||||||
</MenuItem.Icon>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem x:Name="CtxOpenFolder" Header="Open game folder">
|
|
||||||
<MenuItem.Icon>
|
|
||||||
<TextBlock Text="📂" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
|
||||||
</MenuItem.Icon>
|
|
||||||
</MenuItem>
|
|
||||||
<Separator />
|
|
||||||
<MenuItem x:Name="CtxCopyPath" Header="Copy path">
|
|
||||||
<MenuItem.Icon>
|
|
||||||
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
|
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
|
||||||
</MenuItem.Icon>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem x:Name="CtxCopyTitleId" Header="Copy title ID">
|
|
||||||
<MenuItem.Icon>
|
|
||||||
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
|
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
|
||||||
</MenuItem.Icon>
|
|
||||||
</MenuItem>
|
|
||||||
<Separator />
|
|
||||||
<MenuItem x:Name="CtxRemove" Header="Remove from library"
|
|
||||||
Foreground="{StaticResource DangerHoverBrush}">
|
|
||||||
<MenuItem.Icon>
|
|
||||||
<TextBlock Text="✕" FontSize="12" Foreground="{StaticResource DangerHoverBrush}"
|
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
|
||||||
</MenuItem.Icon>
|
|
||||||
</MenuItem>
|
|
||||||
</ContextMenu>
|
|
||||||
</ListBox.ContextMenu>
|
|
||||||
<ListBox.ItemsPanel>
|
|
||||||
<ItemsPanelTemplate>
|
|
||||||
<WrapPanel />
|
|
||||||
</ItemsPanelTemplate>
|
|
||||||
</ListBox.ItemsPanel>
|
|
||||||
<ListBox.ItemTemplate>
|
|
||||||
<DataTemplate>
|
|
||||||
<StackPanel Width="128" Height="186" Spacing="7">
|
|
||||||
<Border Classes="coverShadow" Width="128" Height="128">
|
|
||||||
<Border Classes="coverClip">
|
|
||||||
<Panel>
|
|
||||||
<Border Background="{Binding PlaceholderBrush}" IsVisible="{Binding !HasCover}">
|
|
||||||
<TextBlock Text="{Binding Initials}" FontSize="36" FontWeight="Bold"
|
|
||||||
Foreground="#E8ECF4" Opacity="0.85"
|
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
|
||||||
</Border>
|
|
||||||
<Image Source="{Binding Cover}" Stretch="UniformToFill" IsVisible="{Binding HasCover}" />
|
|
||||||
</Panel>
|
|
||||||
</Border>
|
|
||||||
</Border>
|
|
||||||
<StackPanel Spacing="2">
|
|
||||||
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="SemiBold"
|
|
||||||
TextWrapping="Wrap" MaxLines="2" TextTrimming="CharacterEllipsis" />
|
|
||||||
<TextBlock Text="{Binding Detail}" FontSize="11" Foreground="{StaticResource MutedBrush}"
|
|
||||||
TextTrimming="CharacterEllipsis" />
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
</DataTemplate>
|
|
||||||
</ListBox.ItemTemplate>
|
|
||||||
</ListBox>
|
|
||||||
|
|
||||||
<!-- Empty state -->
|
<!-- Library page. The tile row gets extra top margin so it sits
|
||||||
<StackPanel x:Name="EmptyState" Spacing="10" HorizontalAlignment="Center" VerticalAlignment="Center"
|
closer to eye level (PS5 home-screen style) instead of hugging
|
||||||
IsVisible="False">
|
the Library/Options switcher above it. -->
|
||||||
<TextBlock Text="🎮" FontSize="44" HorizontalAlignment="Center" Opacity="0.7" />
|
<Panel x:Name="LibraryPage" Margin="0,64,0,0">
|
||||||
<TextBlock x:Name="EmptyStateTitle" Text="Your library is empty" FontSize="18" FontWeight="SemiBold"
|
<ListBox x:Name="GameList" Classes="tileGrid" Background="Transparent"
|
||||||
HorizontalAlignment="Center" />
|
SelectionMode="Single" Padding="0">
|
||||||
<TextBlock x:Name="EmptyStateHint" Text="Add a folder containing your games to get started."
|
<ListBox.ContextMenu>
|
||||||
FontSize="13" Foreground="{StaticResource MutedBrush}" HorizontalAlignment="Center" />
|
<ContextMenu x:Name="GameContextMenu" Placement="Pointer">
|
||||||
<Button x:Name="EmptyAddFolderButton" Classes="accent" Content="+ Add game folder"
|
<MenuItem x:Name="CtxLaunch" Header="Launch" FontWeight="SemiBold">
|
||||||
HorizontalAlignment="Center" Margin="0,8,0,0" />
|
<MenuItem.Icon>
|
||||||
</StackPanel>
|
<TextBlock Text="▶" FontSize="11" Foreground="{StaticResource AccentHoverBrush}"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
|
</MenuItem.Icon>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem x:Name="CtxOpenFolder" Header="Open game folder">
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<TextBlock Text="📂" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
|
</MenuItem.Icon>
|
||||||
|
</MenuItem>
|
||||||
|
<Separator />
|
||||||
|
<MenuItem x:Name="CtxCopyPath" Header="Copy path">
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
|
</MenuItem.Icon>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem x:Name="CtxCopyTitleId" Header="Copy title ID">
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
|
</MenuItem.Icon>
|
||||||
|
</MenuItem>
|
||||||
|
<Separator />
|
||||||
|
<MenuItem x:Name="CtxRemove" Header="Remove from library"
|
||||||
|
Foreground="{StaticResource DangerHoverBrush}">
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<TextBlock Text="✕" FontSize="12" Foreground="{StaticResource DangerHoverBrush}"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
|
</MenuItem.Icon>
|
||||||
|
</MenuItem>
|
||||||
|
</ContextMenu>
|
||||||
|
</ListBox.ContextMenu>
|
||||||
|
<ListBox.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<WrapPanel />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ListBox.ItemsPanel>
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Width="128" Height="172" Spacing="7">
|
||||||
|
<Border Classes="coverShadow" Width="128" Height="128">
|
||||||
|
<Border Classes="coverClip">
|
||||||
|
<Panel>
|
||||||
|
<Border Background="{Binding PlaceholderBrush}" IsVisible="{Binding !HasCover}">
|
||||||
|
<TextBlock Text="{Binding Initials}" FontSize="36" FontWeight="Bold"
|
||||||
|
Foreground="#E8ECF4" Opacity="0.85"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
|
</Border>
|
||||||
|
<Image Source="{Binding Cover}" Stretch="UniformToFill" IsVisible="{Binding HasCover}" />
|
||||||
|
</Panel>
|
||||||
|
</Border>
|
||||||
|
</Border>
|
||||||
|
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="SemiBold"
|
||||||
|
TextWrapping="Wrap" MaxLines="2" TextTrimming="CharacterEllipsis"
|
||||||
|
TextAlignment="Center" HorizontalAlignment="Center" />
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
|
||||||
|
<!-- Empty state -->
|
||||||
|
<StackPanel x:Name="EmptyState" Spacing="10" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
|
IsVisible="False">
|
||||||
|
<TextBlock Text="🎮" FontSize="44" HorizontalAlignment="Center" Opacity="0.7" />
|
||||||
|
<TextBlock x:Name="EmptyStateTitle" Text="Your library is empty" FontSize="18" FontWeight="SemiBold"
|
||||||
|
HorizontalAlignment="Center" />
|
||||||
|
<TextBlock x:Name="EmptyStateHint" Text="Add a folder containing your games to get started."
|
||||||
|
FontSize="13" Foreground="{StaticResource MutedBrush}" HorizontalAlignment="Center" />
|
||||||
|
<Button x:Name="EmptyAddFolderButton" Classes="accent" Content="+ Add game folder"
|
||||||
|
HorizontalAlignment="Center" Margin="0,8,0,0" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Loading state: covers the grid while a scan (initial load,
|
||||||
|
rescan, or a just-added folder) is in flight, so the screen
|
||||||
|
never looks blank mid-scan. -->
|
||||||
|
<StackPanel x:Name="LoadingState" Spacing="14" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
|
IsVisible="False">
|
||||||
|
<ProgressBar IsIndeterminate="True" Width="180" Height="3" />
|
||||||
|
<TextBlock x:Name="LoadingStateText" Text="Loading library…" FontSize="13"
|
||||||
|
Foreground="{StaticResource MutedBrush}" HorizontalAlignment="Center" />
|
||||||
|
</StackPanel>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<!-- Options page: sub-tabs so future categories (Graphics, …) slot
|
||||||
|
in next to General. Card-grouped sections match the visual
|
||||||
|
language used by the console panel and launch bar below. -->
|
||||||
|
<Grid x:Name="OptionsPage" IsVisible="False">
|
||||||
|
<TabControl>
|
||||||
|
<TabItem x:Name="GeneralTabItem" Header="General" FontSize="15">
|
||||||
|
<ScrollViewer>
|
||||||
|
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
|
||||||
|
|
||||||
|
<Border Classes="card">
|
||||||
|
<StackPanel Spacing="14">
|
||||||
|
<TextBlock x:Name="EmulationSectionTitle" Classes="sectionTitle" Text="EMULATION" />
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||||
|
<TextBlock x:Name="CpuEngineLabel" Text="CPU engine" FontSize="13" />
|
||||||
|
<TextBlock x:Name="CpuEngineDesc" Text="Execution engine used to run game code."
|
||||||
|
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
<ComboBox Grid.Column="1" x:Name="CpuEngineBox" Width="160" SelectedIndex="0"
|
||||||
|
VerticalAlignment="Center" CornerRadius="8">
|
||||||
|
<ComboBoxItem x:Name="CpuEngineNativeItem" Content="Native" />
|
||||||
|
</ComboBox>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||||
|
<TextBlock x:Name="StrictLabel" Text="Strict dynlib resolution" FontSize="13" />
|
||||||
|
<TextBlock x:Name="StrictDesc" Text="Fail the launch when an imported symbol cannot be resolved."
|
||||||
|
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
<ToggleSwitch Grid.Column="1" x:Name="StrictToggle" OnContent="On" OffContent="Off"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Classes="card">
|
||||||
|
<StackPanel Spacing="14">
|
||||||
|
<TextBlock x:Name="LoggingSectionTitle" Classes="sectionTitle" Text="LOGGING" />
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||||
|
<TextBlock x:Name="LogLevelLabel" Text="Log level" FontSize="13" />
|
||||||
|
<TextBlock x:Name="LogLevelDesc" Text="Verbosity of the emulator console output."
|
||||||
|
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
<ComboBox Grid.Column="1" x:Name="LogLevelBox" Width="160" SelectedIndex="2"
|
||||||
|
VerticalAlignment="Center" CornerRadius="8">
|
||||||
|
<ComboBoxItem x:Name="LogLevelTraceItem" Content="Trace" />
|
||||||
|
<ComboBoxItem x:Name="LogLevelDebugItem" Content="Debug" />
|
||||||
|
<ComboBoxItem x:Name="LogLevelInfoItem" Content="Info" />
|
||||||
|
<ComboBoxItem x:Name="LogLevelWarningItem" Content="Warning" />
|
||||||
|
<ComboBoxItem x:Name="LogLevelErrorItem" Content="Error" />
|
||||||
|
<ComboBoxItem x:Name="LogLevelCriticalItem" Content="Critical" />
|
||||||
|
</ComboBox>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||||
|
<TextBlock x:Name="TraceImportsLabel" Text="Import trace limit" FontSize="13" />
|
||||||
|
<TextBlock x:Name="TraceImportsDesc" Text="Trace the first N imports per module (0 = off)."
|
||||||
|
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
<NumericUpDown Grid.Column="1" x:Name="TraceImportsBox" Width="160" Minimum="0"
|
||||||
|
Maximum="4096" Increment="16" Value="0" FormatString="0"
|
||||||
|
VerticalAlignment="Center" CornerRadius="8" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||||
|
<TextBlock x:Name="LogToFileLabel" Text="Log to file" FontSize="13" />
|
||||||
|
<TextBlock x:Name="LogToFileDesc" Text="Mirror emulator output to a log file."
|
||||||
|
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
<ToggleSwitch Grid.Column="1" x:Name="LogToFileToggle" OnContent="On" OffContent="Off"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||||
|
<TextBlock x:Name="LogFilePathLabel" Text="Log file path" FontSize="13" />
|
||||||
|
<TextBlock x:Name="LogFilePathText" Text="No custom path"
|
||||||
|
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
<Button Grid.Column="1" x:Name="SelectLogFilePathButton" Classes="ghost" Content="Select…"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||||
|
<TextBlock x:Name="OverrideLogFileLabel" Text="Override log file" FontSize="13" />
|
||||||
|
<TextBlock x:Name="OverrideLogFileDesc"
|
||||||
|
Text="Use the exact file path instead of appending title ID and timestamp."
|
||||||
|
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
<ToggleSwitch Grid.Column="1" x:Name="OverrideLogFileToggle" OnContent="On" OffContent="Off"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Classes="card">
|
||||||
|
<StackPanel Spacing="14">
|
||||||
|
<TextBlock x:Name="LauncherSectionTitle" Classes="sectionTitle" Text="LAUNCHER" />
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||||
|
<TextBlock x:Name="LanguageLabel" Text="Emulator language" FontSize="13" />
|
||||||
|
<TextBlock x:Name="LanguageDesc"
|
||||||
|
Text="Language used throughout the launcher. Applies immediately."
|
||||||
|
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
<ComboBox Grid.Column="1" x:Name="LanguageBox" Width="160"
|
||||||
|
VerticalAlignment="Center" CornerRadius="8"
|
||||||
|
DisplayMemberBinding="{Binding NativeName}" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||||
|
<TextBlock x:Name="TitleMusicLabel" Text="Title music" FontSize="13" />
|
||||||
|
<TextBlock x:Name="TitleMusicDesc" Text="Loop the selected game's preview music in the library."
|
||||||
|
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
<ToggleSwitch Grid.Column="1" x:Name="TitleMusicToggle" OnContent="On" OffContent="Off"
|
||||||
|
IsChecked="True" VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||||
|
<TextBlock x:Name="DiscordLabel" Text="Discord presence" FontSize="13" />
|
||||||
|
<TextBlock x:Name="DiscordDesc" Text="Show the running game on your Discord profile."
|
||||||
|
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
<ToggleSwitch Grid.Column="1" x:Name="DiscordToggle" OnContent="On" OffContent="Off"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<Border Classes="card">
|
||||||
|
<StackPanel Spacing="14">
|
||||||
|
<TextBlock x:Name="AboutSectionTitle"
|
||||||
|
Classes="sectionTitle"
|
||||||
|
Text="ABOUT" />
|
||||||
|
|
||||||
|
<!--Github-->
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel Grid.Column="0"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="8"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
<Image Source="avares://SharpEmu.GUI/Assets/github.png"
|
||||||
|
Width="20"
|
||||||
|
Height="20"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
<StackPanel VerticalAlignment="Center"
|
||||||
|
Spacing="2">
|
||||||
|
<TextBlock x:Name="GithubLabel"
|
||||||
|
Text="GitHub"
|
||||||
|
FontSize="13" />
|
||||||
|
<TextBlock x:Name="GithubDesc"
|
||||||
|
Text="Source code, issues and project development."
|
||||||
|
FontSize="11"
|
||||||
|
Foreground="{StaticResource MutedBrush}"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
x:Name="GithubButton"
|
||||||
|
Classes="ghost"
|
||||||
|
Content="Open"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!--Discord-->
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel Grid.Column="0"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="8"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
<Image Source="avares://SharpEmu.GUI/Assets/discord.png"
|
||||||
|
Width="20"
|
||||||
|
Height="20"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
<StackPanel VerticalAlignment="Center"
|
||||||
|
Spacing="2">
|
||||||
|
<TextBlock x:Name="DiscordServerLabel"
|
||||||
|
Text="Discord"
|
||||||
|
FontSize="13" />
|
||||||
|
<TextBlock x:Name="DiscordServerDesc"
|
||||||
|
Text="Join the community, get support and follow development."
|
||||||
|
FontSize="11"
|
||||||
|
Foreground="{StaticResource MutedBrush}"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
x:Name="DiscordButton"
|
||||||
|
Classes="ghost"
|
||||||
|
Content="Join"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</TabItem>
|
||||||
|
</TabControl>
|
||||||
|
</Grid>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<!-- Console (collapsible) -->
|
<!-- Console (collapsible) -->
|
||||||
@@ -161,7 +396,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
Margin="0,12,0,0" IsVisible="False">
|
Margin="0,12,0,0" IsVisible="False">
|
||||||
<Grid RowDefinitions="Auto,*">
|
<Grid RowDefinitions="Auto,*">
|
||||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto,Auto,Auto" Margin="16,12,16,8">
|
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto,Auto,Auto" Margin="16,12,16,8">
|
||||||
<TextBlock Classes="sectionTitle" Text="CONSOLE" VerticalAlignment="Center" />
|
<TextBlock x:Name="ConsoleSectionTitle" Classes="sectionTitle" Text="CONSOLE" VerticalAlignment="Center" />
|
||||||
<TextBox Grid.Column="1" FontSize="12" Margin="0,0,12,0" x:Name="ConsoleSearchBox"
|
<TextBox Grid.Column="1" FontSize="12" Margin="0,0,12,0" x:Name="ConsoleSearchBox"
|
||||||
Watermark="Search..." Width="320" />
|
Watermark="Search..." Width="320" />
|
||||||
<CheckBox Grid.Column="2" x:Name="AutoScrollCheck" Content="Auto-scroll" IsChecked="True"
|
<CheckBox Grid.Column="2" x:Name="AutoScrollCheck" Content="Auto-scroll" IsChecked="True"
|
||||||
@@ -184,8 +419,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Launch bar -->
|
<!-- Launch bar; capped so it does not sprawl on a maximized window. -->
|
||||||
<Border Grid.Row="3" Classes="card" Margin="0,12,0,0" Padding="14">
|
<Border Grid.Row="3" Classes="card" Margin="0,12,0,0" Padding="14" MaxWidth="1280">
|
||||||
<StackPanel Spacing="14">
|
<StackPanel Spacing="14">
|
||||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||||
|
|
||||||
@@ -204,9 +439,32 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</Panel>
|
</Panel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<StackPanel Grid.Column="1" Spacing="3" VerticalAlignment="Center" Margin="14,0,14,0">
|
<StackPanel Grid.Column="1" Spacing="4" VerticalAlignment="Center" Margin="14,0,14,0">
|
||||||
<TextBlock x:Name="SelectedGameTitle" Text="No game selected" FontSize="16" FontWeight="Bold"
|
<Grid ColumnDefinitions="Auto,Auto,*">
|
||||||
TextTrimming="CharacterEllipsis" />
|
<TextBlock Grid.Column="0" x:Name="SelectedGameTitle" Text="No game selected" FontSize="16"
|
||||||
|
FontWeight="Bold" TextTrimming="CharacterEllipsis" VerticalAlignment="Center"
|
||||||
|
MaxWidth="340" Margin="0,0,10,0" />
|
||||||
|
|
||||||
|
<!-- Title id / version / size badges, right next to the
|
||||||
|
title. The title's own MaxWidth (not a "*" column) is
|
||||||
|
what keeps them from drifting to the far right. -->
|
||||||
|
<StackPanel Grid.Column="1" x:Name="SelectedBadgesRow" Orientation="Horizontal" Spacing="6"
|
||||||
|
IsVisible="False" VerticalAlignment="Center">
|
||||||
|
<Border Classes="pill" IsVisible="{Binding HasTitleId, FallbackValue=False}">
|
||||||
|
<TextBlock Text="{Binding TitleId}" FontSize="10" FontWeight="SemiBold"
|
||||||
|
Foreground="{StaticResource MutedBrush}" />
|
||||||
|
</Border>
|
||||||
|
<Border Classes="pill" IsVisible="{Binding HasVersion, FallbackValue=False}">
|
||||||
|
<TextBlock Text="{Binding VersionText}" FontSize="10" FontWeight="SemiBold"
|
||||||
|
Foreground="{StaticResource MutedBrush}" />
|
||||||
|
</Border>
|
||||||
|
<Border Classes="pill">
|
||||||
|
<TextBlock Text="{Binding SizeText, FallbackValue=''}" FontSize="10" FontWeight="SemiBold"
|
||||||
|
Foreground="{StaticResource MutedBrush}" />
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<TextBlock x:Name="SelectedGamePath" Text="Pick a game from the library, or open an eboot.bin directly."
|
<TextBlock x:Name="SelectedGamePath" Text="Pick a game from the library, or open an eboot.bin directly."
|
||||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextTrimming="CharacterEllipsis" />
|
FontSize="11" Foreground="{StaticResource MutedBrush}" TextTrimming="CharacterEllipsis" />
|
||||||
<StackPanel Orientation="Horizontal" Spacing="7">
|
<StackPanel Orientation="Horizontal" Spacing="7">
|
||||||
@@ -218,69 +476,18 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||||
<ToggleButton x:Name="OptionsToggle" Classes="ghost" Content="⚙ Options" />
|
|
||||||
<ToggleButton x:Name="ConsoleToggle" Classes="ghost" Content="≡ Console" />
|
<ToggleButton x:Name="ConsoleToggle" Classes="ghost" Content="≡ Console" />
|
||||||
<Button x:Name="LaunchButton" Classes="accent" Content="▶ Launch" IsEnabled="False" />
|
<Button x:Name="LaunchButton" Classes="accent" Content="▶ Launch" IsEnabled="False" />
|
||||||
<Button x:Name="StopButton" Classes="danger" Content="■ Stop" IsEnabled="False" />
|
<Button x:Name="StopButton" Classes="danger" Content="■ Stop" IsEnabled="False" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Launch options (collapsible) -->
|
|
||||||
<WrapPanel x:Name="OptionsPanel" IsVisible="False">
|
|
||||||
<StackPanel Margin="0,0,24,0">
|
|
||||||
<TextBlock Classes="fieldLabel" Text="CPU engine" />
|
|
||||||
<ComboBox x:Name="CpuEngineBox" Width="150" SelectedIndex="0" CornerRadius="8">
|
|
||||||
<ComboBoxItem Content="Native" />
|
|
||||||
</ComboBox>
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Margin="0,0,24,0">
|
|
||||||
<TextBlock Classes="fieldLabel" Text="Log level" />
|
|
||||||
<ComboBox x:Name="LogLevelBox" Width="150" SelectedIndex="2" CornerRadius="8">
|
|
||||||
<ComboBoxItem Content="Trace" />
|
|
||||||
<ComboBoxItem Content="Debug" />
|
|
||||||
<ComboBoxItem Content="Info" />
|
|
||||||
<ComboBoxItem Content="Warning" />
|
|
||||||
<ComboBoxItem Content="Error" />
|
|
||||||
<ComboBoxItem Content="Critical" />
|
|
||||||
</ComboBox>
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Margin="0,0,24,0">
|
|
||||||
<TextBlock Classes="fieldLabel" Text="Import trace limit (0 = off)" />
|
|
||||||
<NumericUpDown x:Name="TraceImportsBox" Width="170" Minimum="0" Maximum="4096" Increment="16"
|
|
||||||
Value="0" FormatString="0" CornerRadius="8" />
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Margin="0,0,24,0">
|
|
||||||
<TextBlock Classes="fieldLabel" Text="Strict dynlib resolution" />
|
|
||||||
<ToggleSwitch x:Name="StrictToggle" OnContent="On" OffContent="Off" />
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Margin="0,0,24,0">
|
|
||||||
<TextBlock Classes="fieldLabel" Text="Log to file" />
|
|
||||||
<ToggleSwitch x:Name="LogToFileToggle" OnContent="On" OffContent="Off" />
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Margin="0,0,24,0">
|
|
||||||
<TextBlock Classes="fieldLabel" Text="Log file path" />
|
|
||||||
<Button x:Name="SelectLogFilePathButton" Classes="ghost" Content="Select…"
|
|
||||||
VerticalAlignment="Center" />
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Margin="0,0,24,0">
|
|
||||||
<TextBlock Classes="fieldLabel" Text="Override log file" />
|
|
||||||
<ToggleSwitch x:Name="OverrideLogFileToggle" OnContent="On" OffContent="Off" />
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Margin="0,0,24,0">
|
|
||||||
<TextBlock Classes="fieldLabel" Text="Title music" />
|
|
||||||
<ToggleSwitch x:Name="TitleMusicToggle" OnContent="On" OffContent="Off" IsChecked="True" />
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Classes="fieldLabel" Text="Discord presence" />
|
|
||||||
<ToggleSwitch x:Name="DiscordToggle" OnContent="On" OffContent="Off" />
|
|
||||||
</StackPanel>
|
|
||||||
</WrapPanel>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Status bar -->
|
<!-- Status bar -->
|
||||||
<Grid Grid.Row="2" Background="{StaticResource ChromeBrush}" ColumnDefinitions="*,Auto">
|
<Grid x:Name="StatusBar" Grid.Row="2" Height="32" Background="{StaticResource ChromeBrush}"
|
||||||
|
ColumnDefinitions="*,Auto">
|
||||||
<TextBlock x:Name="EmulatorPathText" Grid.Column="0" Text="Emulator: locating…" FontSize="11"
|
<TextBlock x:Name="EmulatorPathText" Grid.Column="0" Text="Emulator: locating…" FontSize="11"
|
||||||
Foreground="{StaticResource FaintBrush}" VerticalAlignment="Center" Margin="16,0"
|
Foreground="{StaticResource FaintBrush}" VerticalAlignment="Center" Margin="16,0"
|
||||||
TextTrimming="CharacterEllipsis" />
|
TextTrimming="CharacterEllipsis" />
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ namespace SharpEmu.GUI;
|
|||||||
public partial class MainWindow : Window
|
public partial class MainWindow : Window
|
||||||
{
|
{
|
||||||
private const int MaxConsoleLines = 4000;
|
private const int MaxConsoleLines = 4000;
|
||||||
|
private const int MaxConsoleLinesPerFlush = 500;
|
||||||
|
|
||||||
private static readonly IBrush DefaultLineBrush = new SolidColorBrush(Color.Parse("#C7CFDE"));
|
private static readonly IBrush DefaultLineBrush = new SolidColorBrush(Color.Parse("#C7CFDE"));
|
||||||
private static readonly IBrush DimLineBrush = new SolidColorBrush(Color.Parse("#6B7488"));
|
private static readonly IBrush DimLineBrush = new SolidColorBrush(Color.Parse("#6B7488"));
|
||||||
@@ -48,6 +49,7 @@ public partial class MainWindow : Window
|
|||||||
private string? _emulatorExePath;
|
private string? _emulatorExePath;
|
||||||
private bool _isRunning;
|
private bool _isRunning;
|
||||||
private int _autoScrollTicks;
|
private int _autoScrollTicks;
|
||||||
|
private int _activePageIndex;
|
||||||
|
|
||||||
// Discord Rich Presence state.
|
// Discord Rich Presence state.
|
||||||
private readonly long _launcherStartUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
private readonly long _launcherStartUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
@@ -100,23 +102,38 @@ public partial class MainWindow : Window
|
|||||||
ClearLogButton.Click += (_, _) => _consoleLines.Clear();
|
ClearLogButton.Click += (_, _) => _consoleLines.Clear();
|
||||||
CopyLogButton.Click += async (_, _) => await CopyConsoleAsync();
|
CopyLogButton.Click += async (_, _) => await CopyConsoleAsync();
|
||||||
DetachConsoleButton.Click += (_, _) => ShowConsoleWindow();
|
DetachConsoleButton.Click += (_, _) => ShowConsoleWindow();
|
||||||
OptionsToggle.IsCheckedChanged += (_, _) => OptionsPanel.IsVisible = OptionsToggle.IsChecked == true;
|
LibraryTabButton.Click += (_, _) => SetActivePage(0);
|
||||||
|
OptionsTabButton.Click += (_, _) => SetActivePage(1);
|
||||||
ConsoleToggle.IsCheckedChanged += (_, _) => ConsolePanel.IsVisible = ConsoleToggle.IsChecked == true && _consoleWindow is null;
|
ConsoleToggle.IsCheckedChanged += (_, _) => ConsolePanel.IsVisible = ConsoleToggle.IsChecked == true && _consoleWindow is null;
|
||||||
SelectLogFilePathButton.Click += async (_, _) => await SelectFilePathAsync();
|
|
||||||
TitleMusicToggle.IsCheckedChanged += (_, _) => OnTitleMusicToggled();
|
// The settings page edits _settings live, so a launch started while
|
||||||
|
// it is open already uses the new values.
|
||||||
|
LogLevelBox.SelectionChanged += (_, _) => _settings.LogLevel = SelectedLogLevel();
|
||||||
|
TraceImportsBox.ValueChanged += (_, _) => _settings.ImportTraceLimit = (int)(TraceImportsBox.Value ?? 0);
|
||||||
|
StrictToggle.IsCheckedChanged += (_, _) => _settings.StrictDynlibResolution = StrictToggle.IsChecked == true;
|
||||||
|
LogToFileToggle.IsCheckedChanged += (_, _) => _settings.LogToFile = LogToFileToggle.IsChecked == true;
|
||||||
|
OverrideLogFileToggle.IsCheckedChanged += (_, _) =>
|
||||||
|
_settings.OverrideLogFile = OverrideLogFileToggle.IsChecked == true;
|
||||||
|
TitleMusicToggle.IsCheckedChanged += (_, _) =>
|
||||||
|
{
|
||||||
|
_settings.PlayTitleMusic = TitleMusicToggle.IsChecked == true;
|
||||||
|
OnTitleMusicSettingChanged();
|
||||||
|
};
|
||||||
DiscordToggle.IsCheckedChanged += (_, _) =>
|
DiscordToggle.IsCheckedChanged += (_, _) =>
|
||||||
{
|
{
|
||||||
_settings.DiscordRichPresence = DiscordToggle.IsChecked == true;
|
_settings.DiscordRichPresence = DiscordToggle.IsChecked == true;
|
||||||
UpdateDiscordPresence();
|
UpdateDiscordPresence();
|
||||||
};
|
};
|
||||||
|
SelectLogFilePathButton.Click += async (_, _) => await SelectLogFilePathAsync();
|
||||||
|
LanguageBox.SelectionChanged += (_, _) => OnLanguageChanged();
|
||||||
|
|
||||||
GameList.AddHandler(ContextRequestedEvent, OnGameContextRequested, RoutingStrategies.Tunnel);
|
GameList.AddHandler(ContextRequestedEvent, OnGameContextRequested, RoutingStrategies.Tunnel);
|
||||||
CtxLaunch.Click += (_, _) => LaunchSelected();
|
CtxLaunch.Click += (_, _) => LaunchSelected();
|
||||||
CtxOpenFolder.Click += (_, _) => OpenSelectedGameFolder();
|
CtxOpenFolder.Click += (_, _) => OpenSelectedGameFolder();
|
||||||
CtxCopyPath.Click += async (_, _) =>
|
CtxCopyPath.Click += async (_, _) =>
|
||||||
await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.Path, "Path");
|
await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.Path, "Clipboard.Path");
|
||||||
CtxCopyTitleId.Click += async (_, _) =>
|
CtxCopyTitleId.Click += async (_, _) =>
|
||||||
await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.TitleId, "Title ID");
|
await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.TitleId, "Clipboard.TitleId");
|
||||||
CtxRemove.Click += (_, _) => RemoveSelectedFromLibrary();
|
CtxRemove.Click += (_, _) => RemoveSelectedFromLibrary();
|
||||||
|
|
||||||
Opened += async (_, _) => await OnOpenedAsync();
|
Opened += async (_, _) => await OnOpenedAsync();
|
||||||
@@ -130,6 +147,64 @@ 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>
|
||||||
|
/// Switches between the Library and Options pages. Also reachable via
|
||||||
|
/// the gamepad's shoulder buttons (LB/RB, L1/R1) from <see cref="PollGamepad"/>.
|
||||||
|
/// </summary>
|
||||||
|
private void SetActivePage(int index)
|
||||||
|
{
|
||||||
|
if (index == _activePageIndex)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_activePageIndex == 1)
|
||||||
|
{
|
||||||
|
_settings.Save(); // leaving the Options page
|
||||||
|
}
|
||||||
|
|
||||||
|
_activePageIndex = index;
|
||||||
|
SetActiveClass(LibraryTabButton, index == 0);
|
||||||
|
SetActiveClass(OptionsTabButton, index == 1);
|
||||||
|
LibraryPage.IsVisible = index == 0;
|
||||||
|
LibraryToolbar.IsVisible = index == 0;
|
||||||
|
OptionsPage.IsVisible = index == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetActiveClass(Button button, bool active)
|
||||||
|
{
|
||||||
|
if (active)
|
||||||
|
{
|
||||||
|
if (!button.Classes.Contains("active"))
|
||||||
|
{
|
||||||
|
button.Classes.Add("active");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
button.Classes.Remove("active");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Controller navigation ----
|
// ---- Controller navigation ----
|
||||||
@@ -145,8 +220,25 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
if (!IsActive)
|
if (!IsActive)
|
||||||
{
|
{
|
||||||
// Ignore input while the launcher is in the background (e.g. the
|
// Ignore input while the launcher is in the background, e.g. the
|
||||||
// game window is focused and using the same controller).
|
// game window is focused and using the same controller.
|
||||||
|
_previousPadButtons = pad.Buttons;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var shoulderPressed = pad.Buttons & ~_previousPadButtons;
|
||||||
|
if ((shoulderPressed & OrbisPadButton.L1) != 0)
|
||||||
|
{
|
||||||
|
SetActivePage(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((shoulderPressed & OrbisPadButton.R1) != 0)
|
||||||
|
{
|
||||||
|
SetActivePage(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_activePageIndex != 0)
|
||||||
|
{
|
||||||
_previousPadButtons = pad.Buttons;
|
_previousPadButtons = pad.Buttons;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -254,12 +346,134 @@ public partial class MainWindow : Window
|
|||||||
ToolTip.SetTip(VersionText, BuildInfo.Banner);
|
ToolTip.SetTip(VersionText, BuildInfo.Banner);
|
||||||
|
|
||||||
_settings = GuiSettings.Load();
|
_settings = GuiSettings.Load();
|
||||||
|
Localization.Instance.Load(_settings.Language);
|
||||||
|
PopulateLanguageBox();
|
||||||
|
ApplyLocalization();
|
||||||
ApplySettingsToControls();
|
ApplySettingsToControls();
|
||||||
LocateEmulator();
|
LocateEmulator();
|
||||||
UpdateDiscordPresence();
|
UpdateDiscordPresence();
|
||||||
await RescanLibraryAsync();
|
await RescanLibraryAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void PopulateLanguageBox()
|
||||||
|
{
|
||||||
|
var languages = Localization.Instance.DiscoverLanguages();
|
||||||
|
LanguageBox.ItemsSource = languages;
|
||||||
|
LanguageBox.SelectedItem = languages.FirstOrDefault(language =>
|
||||||
|
string.Equals(language.Code, _settings.Language, StringComparison.OrdinalIgnoreCase))
|
||||||
|
?? languages.FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLanguageChanged()
|
||||||
|
{
|
||||||
|
if (LanguageBox.SelectedItem is not Localization.LanguageInfo language)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_settings.Language = language.Code;
|
||||||
|
Localization.Instance.Load(language.Code);
|
||||||
|
ApplyLocalization();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Re-applies every UI string from the current language, so switching
|
||||||
|
/// languages in Options takes effect immediately without reopening the
|
||||||
|
/// window.
|
||||||
|
/// </summary>
|
||||||
|
private void ApplyLocalization()
|
||||||
|
{
|
||||||
|
var loc = Localization.Instance;
|
||||||
|
|
||||||
|
LibraryTabButton.Content = loc.Get("Page.Library");
|
||||||
|
OptionsTabButton.Content = loc.Get("Page.Options");
|
||||||
|
|
||||||
|
SearchBox.Watermark = loc.Get("Library.SearchWatermark");
|
||||||
|
AddFolderButton.Content = loc.Get("Library.AddFolder");
|
||||||
|
RescanButton.Content = loc.Get("Library.Rescan");
|
||||||
|
OpenFileButton.Content = loc.Get("Library.OpenFile");
|
||||||
|
|
||||||
|
CtxLaunch.Header = loc.Get("Library.Context.Launch");
|
||||||
|
CtxOpenFolder.Header = loc.Get("Library.Context.OpenFolder");
|
||||||
|
CtxCopyPath.Header = loc.Get("Library.Context.CopyPath");
|
||||||
|
CtxCopyTitleId.Header = loc.Get("Library.Context.CopyTitleId");
|
||||||
|
CtxRemove.Header = loc.Get("Library.Context.Remove");
|
||||||
|
|
||||||
|
EmptyAddFolderButton.Content = loc.Get("Library.Empty.AddFolder");
|
||||||
|
LoadingStateText.Text = loc.Get("Library.Loading");
|
||||||
|
|
||||||
|
GeneralTabItem.Header = loc.Get("Options.General");
|
||||||
|
EmulationSectionTitle.Text = loc.Get("Options.Section.Emulation");
|
||||||
|
LoggingSectionTitle.Text = loc.Get("Options.Section.Logging");
|
||||||
|
LauncherSectionTitle.Text = loc.Get("Options.Section.Launcher");
|
||||||
|
|
||||||
|
CpuEngineLabel.Text = loc.Get("Options.CpuEngine.Label");
|
||||||
|
CpuEngineDesc.Text = loc.Get("Options.CpuEngine.Desc");
|
||||||
|
CpuEngineNativeItem.Content = loc.Get("Options.CpuEngine.Native");
|
||||||
|
|
||||||
|
StrictLabel.Text = loc.Get("Options.Strict.Label");
|
||||||
|
StrictDesc.Text = loc.Get("Options.Strict.Desc");
|
||||||
|
|
||||||
|
LogLevelLabel.Text = loc.Get("Options.LogLevel.Label");
|
||||||
|
LogLevelDesc.Text = loc.Get("Options.LogLevel.Desc");
|
||||||
|
LogLevelTraceItem.Content = loc.Get("Options.LogLevel.Trace");
|
||||||
|
LogLevelDebugItem.Content = loc.Get("Options.LogLevel.Debug");
|
||||||
|
LogLevelInfoItem.Content = loc.Get("Options.LogLevel.Info");
|
||||||
|
LogLevelWarningItem.Content = loc.Get("Options.LogLevel.Warning");
|
||||||
|
LogLevelErrorItem.Content = loc.Get("Options.LogLevel.Error");
|
||||||
|
LogLevelCriticalItem.Content = loc.Get("Options.LogLevel.Critical");
|
||||||
|
|
||||||
|
TraceImportsLabel.Text = loc.Get("Options.TraceImports.Label");
|
||||||
|
TraceImportsDesc.Text = loc.Get("Options.TraceImports.Desc");
|
||||||
|
|
||||||
|
LogToFileLabel.Text = loc.Get("Options.LogToFile.Label");
|
||||||
|
LogToFileDesc.Text = loc.Get("Options.LogToFile.Desc");
|
||||||
|
|
||||||
|
LogFilePathLabel.Text = loc.Get("Options.LogFilePath.Label");
|
||||||
|
SelectLogFilePathButton.Content = loc.Get("Options.LogFilePath.Select");
|
||||||
|
UpdateLogFilePathText();
|
||||||
|
|
||||||
|
OverrideLogFileLabel.Text = loc.Get("Options.OverrideLogFile.Label");
|
||||||
|
OverrideLogFileDesc.Text = loc.Get("Options.OverrideLogFile.Desc");
|
||||||
|
|
||||||
|
LanguageLabel.Text = loc.Get("Options.Language.Label");
|
||||||
|
LanguageDesc.Text = loc.Get("Options.Language.Desc");
|
||||||
|
|
||||||
|
TitleMusicLabel.Text = loc.Get("Options.TitleMusic.Label");
|
||||||
|
TitleMusicDesc.Text = loc.Get("Options.TitleMusic.Desc");
|
||||||
|
|
||||||
|
DiscordLabel.Text = loc.Get("Options.Discord.Label");
|
||||||
|
DiscordDesc.Text = loc.Get("Options.Discord.Desc");
|
||||||
|
|
||||||
|
foreach (var toggle in new[] { StrictToggle, LogToFileToggle, OverrideLogFileToggle, TitleMusicToggle, DiscordToggle })
|
||||||
|
{
|
||||||
|
toggle.OnContent = loc.Get("Common.On");
|
||||||
|
toggle.OffContent = loc.Get("Common.Off");
|
||||||
|
}
|
||||||
|
|
||||||
|
ConsoleSectionTitle.Text = loc.Get("Console.Title");
|
||||||
|
ConsoleSearchBox.Watermark = loc.Get("Console.SearchWatermark");
|
||||||
|
AutoScrollCheck.Content = loc.Get("Console.AutoScroll");
|
||||||
|
DetachConsoleButton.Content = loc.Get("Console.Split");
|
||||||
|
CopyLogButton.Content = loc.Get("Console.Copy");
|
||||||
|
ClearLogButton.Content = loc.Get("Console.Clear");
|
||||||
|
|
||||||
|
ConsoleToggle.Content = loc.Get("Launch.Console");
|
||||||
|
LaunchButton.Content = loc.Get("Launch.Launch");
|
||||||
|
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();
|
||||||
|
UpdateSelectedGameTexts();
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Discord Rich Presence ----
|
// ---- Discord Rich Presence ----
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -280,7 +494,7 @@ public partial class MainWindow : Window
|
|||||||
if (_isRunning && _runningGameName is { } gameName)
|
if (_isRunning && _runningGameName is { } gameName)
|
||||||
{
|
{
|
||||||
_discord.SetPresence(
|
_discord.SetPresence(
|
||||||
$"Playing {gameName}",
|
Localization.Instance.Format("Discord.Playing", gameName),
|
||||||
_runningGameTitleId,
|
_runningGameTitleId,
|
||||||
_runningSinceUnixSeconds);
|
_runningSinceUnixSeconds);
|
||||||
}
|
}
|
||||||
@@ -288,9 +502,12 @@ public partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
// Discord does not render activities without timestamps, so the
|
// Discord does not render activities without timestamps, so the
|
||||||
// browsing state carries the launcher's start time.
|
// browsing state carries the launcher's start time.
|
||||||
|
var count = _allGames.Count == 1
|
||||||
|
? Localization.Instance.Get("Page.GameCount.One")
|
||||||
|
: Localization.Instance.Format("Page.GameCount.Other", _allGames.Count);
|
||||||
_discord.SetPresence(
|
_discord.SetPresence(
|
||||||
"Browsing the library",
|
Localization.Instance.Get("Discord.Browsing"),
|
||||||
$"{_allGames.Count} game(s)",
|
count,
|
||||||
_launcherStartUnixSeconds);
|
_launcherStartUnixSeconds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -315,17 +532,20 @@ public partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
WindowState = WindowState.Normal;
|
WindowState = WindowState.Normal;
|
||||||
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.PreferSystemChrome;
|
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.PreferSystemChrome;
|
||||||
|
TitleBar.IsVisible = true;
|
||||||
|
StatusBar.IsVisible = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
WindowState = WindowState.FullScreen;
|
WindowState = WindowState.FullScreen;
|
||||||
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.NoChrome;
|
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.NoChrome;
|
||||||
|
TitleBar.IsVisible = false;
|
||||||
|
StatusBar.IsVisible = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnWindowClosing()
|
private void OnWindowClosing()
|
||||||
{
|
{
|
||||||
ReadControlsIntoSettings();
|
|
||||||
_settings.Save();
|
_settings.Save();
|
||||||
_consoleFlushTimer.Stop();
|
_consoleFlushTimer.Stop();
|
||||||
_gamepadTimer.Stop();
|
_gamepadTimer.Stop();
|
||||||
@@ -344,7 +564,7 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Settings <-> controls ----
|
// ---- Settings ----
|
||||||
|
|
||||||
private void ApplySettingsToControls()
|
private void ApplySettingsToControls()
|
||||||
{
|
{
|
||||||
@@ -363,19 +583,8 @@ public partial class MainWindow : Window
|
|||||||
LogToFileToggle.IsChecked = _settings.LogToFile;
|
LogToFileToggle.IsChecked = _settings.LogToFile;
|
||||||
OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile;
|
OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile;
|
||||||
TitleMusicToggle.IsChecked = _settings.PlayTitleMusic;
|
TitleMusicToggle.IsChecked = _settings.PlayTitleMusic;
|
||||||
ToolTip.SetTip(SelectLogFilePathButton, string.IsNullOrWhiteSpace(_settings.LogFilePath) ? "No path selected" : _settings.LogFilePath);
|
|
||||||
DiscordToggle.IsChecked = _settings.DiscordRichPresence;
|
DiscordToggle.IsChecked = _settings.DiscordRichPresence;
|
||||||
}
|
UpdateLogFilePathText();
|
||||||
|
|
||||||
private void ReadControlsIntoSettings()
|
|
||||||
{
|
|
||||||
_settings.LogLevel = SelectedLogLevel();
|
|
||||||
_settings.ImportTraceLimit = (int)(TraceImportsBox.Value ?? 0);
|
|
||||||
_settings.StrictDynlibResolution = StrictToggle.IsChecked == true;
|
|
||||||
_settings.LogToFile = LogToFileToggle.IsChecked == true;
|
|
||||||
_settings.OverrideLogFile = OverrideLogFileToggle.IsChecked == true;
|
|
||||||
_settings.PlayTitleMusic = TitleMusicToggle.IsChecked == true;
|
|
||||||
_settings.DiscordRichPresence = DiscordToggle.IsChecked == true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string SelectedLogLevel()
|
private string SelectedLogLevel()
|
||||||
@@ -392,6 +601,35 @@ public partial class MainWindow : Window
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpdateLogFilePathText()
|
||||||
|
{
|
||||||
|
LogFilePathText.Text = string.IsNullOrWhiteSpace(_settings.LogFilePath)
|
||||||
|
? Localization.Instance.Get("Options.LogFilePath.Default")
|
||||||
|
: _settings.LogFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SelectLogFilePathAsync()
|
||||||
|
{
|
||||||
|
var loc = Localization.Instance;
|
||||||
|
SaveFilePickerResult result = await StorageProvider.SaveFilePickerWithResultAsync(new FilePickerSaveOptions
|
||||||
|
{
|
||||||
|
Title = loc.Get("Dialog.SaveLogFile"),
|
||||||
|
SuggestedFileName = "SharpEmuLog",
|
||||||
|
DefaultExtension = "log",
|
||||||
|
FileTypeChoices =
|
||||||
|
[
|
||||||
|
new FilePickerFileType(loc.Get("Dialog.PlainTextFiles")) { Patterns = ["*.txt"] },
|
||||||
|
new FilePickerFileType(loc.Get("Dialog.LogFiles")) { Patterns = ["*.log"] }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.File is not null)
|
||||||
|
{
|
||||||
|
_settings.LogFilePath = result.File.Path.LocalPath;
|
||||||
|
UpdateLogFilePathText();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Emulator discovery ----
|
// ---- Emulator discovery ----
|
||||||
|
|
||||||
private void LocateEmulator()
|
private void LocateEmulator()
|
||||||
@@ -421,8 +659,8 @@ public partial class MainWindow : Window
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
EmulatorPathText.Text = _emulatorExePath is not null
|
EmulatorPathText.Text = _emulatorExePath is not null
|
||||||
? $"Emulator: {_emulatorExePath}"
|
? Localization.Instance.Format("Status.EmulatorPath", _emulatorExePath)
|
||||||
: "Emulator: SharpEmu executable not found — build SharpEmu.CLI first.";
|
: Localization.Instance.Get("Status.EmulatorNotFound");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Game library ----
|
// ---- Game library ----
|
||||||
@@ -431,7 +669,7 @@ public partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
||||||
{
|
{
|
||||||
Title = "Choose a folder containing games",
|
Title = Localization.Instance.Get("Dialog.ChooseGameFolder"),
|
||||||
AllowMultiple = false,
|
AllowMultiple = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -466,18 +704,21 @@ public partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
var folders = _settings.GameFolders.ToArray();
|
var folders = _settings.GameFolders.ToArray();
|
||||||
var excluded = new HashSet<string>(_settings.ExcludedGames, StringComparer.OrdinalIgnoreCase);
|
var excluded = new HashSet<string>(_settings.ExcludedGames, StringComparer.OrdinalIgnoreCase);
|
||||||
StatusBarRight.Text = "Scanning library…";
|
StatusBarRight.Text = Localization.Instance.Get("Status.ScanningLibrary");
|
||||||
|
EmptyState.IsVisible = false;
|
||||||
|
LoadingState.IsVisible = true;
|
||||||
|
|
||||||
var games = await Task.Run(() => ScanFolders(folders, excluded));
|
var games = await Task.Run(() => ScanFolders(folders, excluded));
|
||||||
|
|
||||||
_allGames.Clear();
|
_allGames.Clear();
|
||||||
_allGames.AddRange(games);
|
_allGames.AddRange(games);
|
||||||
RefreshVisibleGames();
|
RefreshVisibleGames();
|
||||||
|
LoadingState.IsVisible = false;
|
||||||
LoadGameDetailsInBackground(games);
|
LoadGameDetailsInBackground(games);
|
||||||
UpdateDiscordPresence();
|
UpdateDiscordPresence();
|
||||||
StatusBarRight.Text = folders.Length == 0
|
StatusBarRight.Text = folders.Length == 0
|
||||||
? "Add a game folder to populate the library."
|
? Localization.Instance.Get("Status.AddFolderPrompt")
|
||||||
: $"Library scanned: {games.Count} game(s) in {folders.Length} folder(s).";
|
: Localization.Instance.Format("Status.LibraryScanned", games.Count, folders.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -614,9 +855,9 @@ public partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
var (title, titleId) = TryReadParamJson(fullPath);
|
var (title, titleId, version) = TryReadParamJson(fullPath);
|
||||||
games.Add(new GameEntry(
|
games.Add(new GameEntry(
|
||||||
title ?? GameNameFor(fullPath), titleId, fullPath, size,
|
title ?? GameNameFor(fullPath), titleId, version, fullPath, size,
|
||||||
FindCoverFor(fullPath), FindBackgroundFor(fullPath)));
|
FindCoverFor(fullPath), FindBackgroundFor(fullPath)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -631,23 +872,23 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads the game title and title id from sce_sys/param.json next to the
|
/// Reads the game title, title id and content version from
|
||||||
/// executable, when present.
|
/// sce_sys/param.json next to the executable, when present.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static (string? Title, string? TitleId) TryReadParamJson(string ebootPath)
|
private static (string? Title, string? TitleId, string? Version) TryReadParamJson(string ebootPath)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var directory = Path.GetDirectoryName(ebootPath);
|
var directory = Path.GetDirectoryName(ebootPath);
|
||||||
if (directory is null)
|
if (directory is null)
|
||||||
{
|
{
|
||||||
return (null, null);
|
return (null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
var paramPath = Path.Combine(directory, "sce_sys", "param.json");
|
var paramPath = Path.Combine(directory, "sce_sys", "param.json");
|
||||||
if (!File.Exists(paramPath))
|
if (!File.Exists(paramPath))
|
||||||
{
|
{
|
||||||
return (null, null);
|
return (null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadAllText handles a UTF-8 BOM, which JsonDocument rejects in
|
// ReadAllText handles a UTF-8 BOM, which JsonDocument rejects in
|
||||||
@@ -661,6 +902,20 @@ public partial class MainWindow : Window
|
|||||||
titleId = idElement.GetString();
|
titleId = idElement.GetString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// contentVersion carries the installed app version
|
||||||
|
// ("01.000.000"); masterVersion is the fallback on older dumps.
|
||||||
|
string? version = null;
|
||||||
|
if (root.TryGetProperty("contentVersion", out var versionElement) &&
|
||||||
|
versionElement.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
version = versionElement.GetString();
|
||||||
|
}
|
||||||
|
else if (root.TryGetProperty("masterVersion", out var masterElement) &&
|
||||||
|
masterElement.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
version = masterElement.GetString();
|
||||||
|
}
|
||||||
|
|
||||||
string? title = null;
|
string? title = null;
|
||||||
if (root.TryGetProperty("localizedParameters", out var localized) &&
|
if (root.TryGetProperty("localizedParameters", out var localized) &&
|
||||||
localized.ValueKind == JsonValueKind.Object)
|
localized.ValueKind == JsonValueKind.Object)
|
||||||
@@ -691,11 +946,12 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
string.IsNullOrWhiteSpace(title) ? null : title,
|
string.IsNullOrWhiteSpace(title) ? null : title,
|
||||||
string.IsNullOrWhiteSpace(titleId) ? null : titleId);
|
string.IsNullOrWhiteSpace(titleId) ? null : titleId,
|
||||||
|
string.IsNullOrWhiteSpace(version) ? null : version.Trim());
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
return (null, null);
|
return (null, null, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -806,11 +1062,12 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
StatusBarRight.Text = $"Could not open folder: {ex.Message}";
|
StatusBarRight.Text = Localization.Instance.Format("Status.CouldNotOpenFolder", ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CopyToClipboardAsync(string? text, string what)
|
/// <summary>Copies <paramref name="text"/> and reports it via <paramref name="whatKey"/>, e.g. "Clipboard.Path".</summary>
|
||||||
|
private async Task CopyToClipboardAsync(string? text, string whatKey)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(text) || Clipboard is null)
|
if (string.IsNullOrEmpty(text) || Clipboard is null)
|
||||||
{
|
{
|
||||||
@@ -818,7 +1075,7 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
await Clipboard.SetTextAsync(text);
|
await Clipboard.SetTextAsync(text);
|
||||||
StatusBarRight.Text = $"{what} copied to clipboard.";
|
StatusBarRight.Text = Localization.Instance.Format("Status.CopiedToClipboard", Localization.Instance.Get(whatKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveSelectedFromLibrary()
|
private void RemoveSelectedFromLibrary()
|
||||||
@@ -837,7 +1094,7 @@ public partial class MainWindow : Window
|
|||||||
_allGames.RemoveAll(g => string.Equals(g.Path, game.Path, StringComparison.OrdinalIgnoreCase));
|
_allGames.RemoveAll(g => string.Equals(g.Path, game.Path, StringComparison.OrdinalIgnoreCase));
|
||||||
GameList.SelectedItem = null;
|
GameList.SelectedItem = null;
|
||||||
RefreshVisibleGames();
|
RefreshVisibleGames();
|
||||||
StatusBarRight.Text = $"Removed “{game.Name}” from the library. Re-add its folder to restore it.";
|
StatusBarRight.Text = Localization.Instance.Format("Status.RemovedFromLibrary", game.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RefreshVisibleGames()
|
private void RefreshVisibleGames()
|
||||||
@@ -857,8 +1114,6 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GameCountText.Text = _visibleGames.Count == 1 ? "1 game" : $"{_visibleGames.Count} games";
|
|
||||||
|
|
||||||
if (selectedPath is not null &&
|
if (selectedPath is not null &&
|
||||||
_visibleGames.FirstOrDefault(g => g.Path.Equals(selectedPath, StringComparison.OrdinalIgnoreCase))
|
_visibleGames.FirstOrDefault(g => g.Path.Equals(selectedPath, StringComparison.OrdinalIgnoreCase))
|
||||||
is { } reselected)
|
is { } reselected)
|
||||||
@@ -867,34 +1122,50 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
EmptyState.IsVisible = _visibleGames.Count == 0;
|
EmptyState.IsVisible = _visibleGames.Count == 0;
|
||||||
if (_visibleGames.Count == 0)
|
UpdateEmptyStateTexts();
|
||||||
{
|
|
||||||
var hasFilter = query.Length > 0;
|
|
||||||
EmptyStateTitle.Text = hasFilter ? "No games match your search" : "Your library is empty";
|
|
||||||
EmptyStateHint.Text = hasFilter
|
|
||||||
? $"Nothing in the library matches “{query}”."
|
|
||||||
: "Add a folder containing your games to get started.";
|
|
||||||
EmptyAddFolderButton.IsVisible = !hasFilter;
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateSelectedGame();
|
UpdateSelectedGame();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Refreshes the empty-state title/hint from the current language and
|
||||||
|
/// search text; a no-op while the empty state is not showing.
|
||||||
|
/// </summary>
|
||||||
|
private void UpdateEmptyStateTexts()
|
||||||
|
{
|
||||||
|
if (_visibleGames.Count != 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var query = SearchBox.Text?.Trim() ?? string.Empty;
|
||||||
|
var hasFilter = query.Length > 0;
|
||||||
|
EmptyStateTitle.Text = hasFilter
|
||||||
|
? Localization.Instance.Get("Library.Empty.SearchTitle")
|
||||||
|
: Localization.Instance.Get("Library.Empty.Title");
|
||||||
|
EmptyStateHint.Text = hasFilter
|
||||||
|
? Localization.Instance.Format("Library.Empty.SearchHint", query)
|
||||||
|
: Localization.Instance.Get("Library.Empty.Hint");
|
||||||
|
EmptyAddFolderButton.IsVisible = !hasFilter;
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdateSelectedGame()
|
private void UpdateSelectedGame()
|
||||||
{
|
{
|
||||||
if (GameList.SelectedItem is GameEntry game)
|
if (GameList.SelectedItem is GameEntry game)
|
||||||
{
|
{
|
||||||
SelectedGameTitle.Text = game.Name;
|
UpdateSelectedGameTexts();
|
||||||
SelectedGamePath.Text = game.Path;
|
|
||||||
SelectedCoverPanel.DataContext = game;
|
SelectedCoverPanel.DataContext = game;
|
||||||
|
SelectedBadgesRow.DataContext = game;
|
||||||
|
SelectedBadgesRow.IsVisible = true;
|
||||||
_ = UpdateBackdropAsync(game);
|
_ = UpdateBackdropAsync(game);
|
||||||
PlaySelectedGamePreview(game);
|
PlaySelectedGamePreview(game);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
SelectedGameTitle.Text = "No game selected";
|
UpdateSelectedGameTexts();
|
||||||
SelectedGamePath.Text = "Pick a game from the library, or open an eboot.bin directly.";
|
|
||||||
SelectedCoverPanel.DataContext = null;
|
SelectedCoverPanel.DataContext = null;
|
||||||
|
SelectedBadgesRow.DataContext = null;
|
||||||
|
SelectedBadgesRow.IsVisible = false;
|
||||||
_ = UpdateBackdropAsync(null);
|
_ = UpdateBackdropAsync(null);
|
||||||
_sndPreview.Stop();
|
_sndPreview.Stop();
|
||||||
}
|
}
|
||||||
@@ -902,6 +1173,25 @@ public partial class MainWindow : Window
|
|||||||
UpdateRunButtons();
|
UpdateRunButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Text-only refresh of the launch bar's title/path, split out of
|
||||||
|
/// <see cref="UpdateSelectedGame"/> so a language change can re-apply it
|
||||||
|
/// without restarting the backdrop fade or preview music.
|
||||||
|
/// </summary>
|
||||||
|
private void UpdateSelectedGameTexts()
|
||||||
|
{
|
||||||
|
if (GameList.SelectedItem is GameEntry game)
|
||||||
|
{
|
||||||
|
SelectedGameTitle.Text = game.Name;
|
||||||
|
SelectedGamePath.Text = game.Path;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SelectedGameTitle.Text = Localization.Instance.Get("Launch.NoGameSelected");
|
||||||
|
SelectedGamePath.Text = Localization.Instance.Get("Launch.NoGameHint");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Loops the selected game's sce_sys/snd0.at9 preview music, console
|
/// Loops the selected game's sce_sys/snd0.at9 preview music, console
|
||||||
/// home screen style. Silent while a game is running or when disabled
|
/// home screen style. Silent while a game is running or when disabled
|
||||||
@@ -926,9 +1216,8 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTitleMusicToggled()
|
private void OnTitleMusicSettingChanged()
|
||||||
{
|
{
|
||||||
_settings.PlayTitleMusic = TitleMusicToggle.IsChecked == true;
|
|
||||||
if (!_settings.PlayTitleMusic)
|
if (!_settings.PlayTitleMusic)
|
||||||
{
|
{
|
||||||
_sndPreview.Stop();
|
_sndPreview.Stop();
|
||||||
@@ -1001,11 +1290,12 @@ public partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||||
{
|
{
|
||||||
Title = "Open an executable to launch",
|
Title = Localization.Instance.Get("Dialog.OpenExecutable"),
|
||||||
AllowMultiple = false,
|
AllowMultiple = false,
|
||||||
FileTypeFilter = new[]
|
FileTypeFilter = new[]
|
||||||
{
|
{
|
||||||
new FilePickerFileType("PS executables") { Patterns = new[] { "eboot.bin", "*.bin", "*.self", "*.elf" } },
|
new FilePickerFileType(Localization.Instance.Get("Dialog.PsExecutables"))
|
||||||
|
{ Patterns = new[] { "eboot.bin", "*.bin", "*.self", "*.elf" } },
|
||||||
FilePickerFileTypes.All,
|
FilePickerFileTypes.All,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1037,14 +1327,12 @@ public partial class MainWindow : Window
|
|||||||
LocateEmulator();
|
LocateEmulator();
|
||||||
if (_emulatorExePath is null)
|
if (_emulatorExePath is null)
|
||||||
{
|
{
|
||||||
AppendConsoleLine("SharpEmu executable not found. Build the SharpEmu.CLI project first (dotnet build).", ErrorLineBrush);
|
AppendConsoleLine(Localization.Instance.Get("Launch.ExeNotFound"), ErrorLineBrush);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_sndPreview.Stop();
|
_sndPreview.Stop();
|
||||||
ReadControlsIntoSettings();
|
|
||||||
_settings.Save();
|
|
||||||
|
|
||||||
var arguments = new List<string>
|
var arguments = new List<string>
|
||||||
{
|
{
|
||||||
@@ -1062,7 +1350,6 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
_consoleLines.Clear();
|
_consoleLines.Clear();
|
||||||
ConsoleToggle.IsChecked = true;
|
|
||||||
|
|
||||||
// Let the CLI mirror stdout/stderr itself; it sees loader/native
|
// Let the CLI mirror stdout/stderr itself; it sees loader/native
|
||||||
// diagnostics before the GUI pipe reader can filter or batch them.
|
// diagnostics before the GUI pipe reader can filter or batch them.
|
||||||
@@ -1105,13 +1392,15 @@ public partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
arguments.Add("--log-file");
|
arguments.Add("--log-file");
|
||||||
arguments.Add(filePath);
|
arguments.Add(filePath);
|
||||||
AppendConsoleLine($"Log file: {filePath}", DimLineBrush);
|
AppendConsoleLine(Localization.Instance.Format("Launch.LogFile", filePath), DimLineBrush);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
arguments.Add(ebootPath);
|
arguments.Add(ebootPath);
|
||||||
|
|
||||||
AppendConsoleLine($"$ SharpEmu {string.Join(' ', arguments)}", DimLineBrush);
|
AppendConsoleLine(
|
||||||
|
Localization.Instance.Format("Launch.Command", string.Join(' ', arguments)),
|
||||||
|
DimLineBrush);
|
||||||
|
|
||||||
var emulator = new EmulatorProcess();
|
var emulator = new EmulatorProcess();
|
||||||
emulator.OutputReceived += (line, isError) => _pendingLines.Enqueue((line, isError));
|
emulator.OutputReceived += (line, isError) => _pendingLines.Enqueue((line, isError));
|
||||||
@@ -1124,7 +1413,7 @@ public partial class MainWindow : Window
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
emulator.Dispose();
|
emulator.Dispose();
|
||||||
AppendConsoleLine($"Failed to start the emulator: {ex.Message}", ErrorLineBrush);
|
AppendConsoleLine(Localization.Instance.Format("Launch.StartFailed", ex.Message), ErrorLineBrush);
|
||||||
DropFileLog();
|
DropFileLog();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1137,8 +1426,8 @@ public partial class MainWindow : Window
|
|||||||
.TitleId;
|
.TitleId;
|
||||||
_runningSinceUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
_runningSinceUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
StatusDot.Fill = SuccessLineBrush;
|
StatusDot.Fill = SuccessLineBrush;
|
||||||
StatusText.Text = $"Running — {displayName}";
|
StatusText.Text = Localization.Instance.Format("Launch.Running", displayName);
|
||||||
StatusBarRight.Text = $"Running {displayName}";
|
StatusBarRight.Text = Localization.Instance.Format("Status.Running", displayName);
|
||||||
UpdateRunButtons();
|
UpdateRunButtons();
|
||||||
UpdateDiscordPresence();
|
UpdateDiscordPresence();
|
||||||
}
|
}
|
||||||
@@ -1160,8 +1449,8 @@ public partial class MainWindow : Window
|
|||||||
_emulator?.Stop();
|
_emulator?.Stop();
|
||||||
_runningGameName = null;
|
_runningGameName = null;
|
||||||
_runningGameTitleId = null;
|
_runningGameTitleId = null;
|
||||||
StatusText.Text = "Stopping…";
|
StatusText.Text = Localization.Instance.Get("Launch.Stopping");
|
||||||
StatusBarRight.Text = "Stopping…";
|
StatusBarRight.Text = Localization.Instance.Get("Status.Stopping");
|
||||||
UpdateDiscordPresence();
|
UpdateDiscordPresence();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1203,22 +1492,24 @@ public partial class MainWindow : Window
|
|||||||
_emulator?.Dispose();
|
_emulator?.Dispose();
|
||||||
_emulator = null;
|
_emulator = null;
|
||||||
|
|
||||||
var meaning = exitCode switch
|
var meaningKey = exitCode switch
|
||||||
{
|
{
|
||||||
0 => "OK",
|
0 => "Exit.Ok",
|
||||||
1 => "invalid arguments",
|
1 => "Exit.InvalidArguments",
|
||||||
2 => "eboot not found",
|
2 => "Exit.EbootNotFound",
|
||||||
3 => "runtime exception",
|
3 => "Exit.RuntimeException",
|
||||||
4 => "emulation error",
|
4 => "Exit.EmulationError",
|
||||||
_ => "unknown",
|
-1073741819 => "Exit.EmulationError",
|
||||||
|
_ => "Exit.Unknown",
|
||||||
};
|
};
|
||||||
|
var meaning = Localization.Instance.Get(meaningKey);
|
||||||
var brush = exitCode == 0 ? SuccessLineBrush : ErrorLineBrush;
|
var brush = exitCode == 0 ? SuccessLineBrush : ErrorLineBrush;
|
||||||
AppendConsoleLine($"Process exited with code {exitCode} ({meaning}).", brush);
|
AppendConsoleLine(Localization.Instance.Format("Launch.ProcessExited", exitCode, meaning), brush);
|
||||||
CloseFileLogSoon();
|
CloseFileLogSoon();
|
||||||
|
|
||||||
StatusDot.Fill = exitCode == 0 ? (IBrush)SuccessLineBrush : ErrorLineBrush;
|
StatusDot.Fill = exitCode == 0 ? (IBrush)SuccessLineBrush : ErrorLineBrush;
|
||||||
StatusText.Text = $"Exited with code {exitCode} ({meaning})";
|
StatusText.Text = Localization.Instance.Format("Launch.Exited", exitCode, meaning);
|
||||||
StatusBarRight.Text = "Idle";
|
StatusBarRight.Text = Localization.Instance.Get("Status.Idle");
|
||||||
_runningGameName = null;
|
_runningGameName = null;
|
||||||
_runningGameTitleId = null;
|
_runningGameTitleId = null;
|
||||||
UpdateRunButtons();
|
UpdateRunButtons();
|
||||||
@@ -1232,27 +1523,6 @@ public partial class MainWindow : Window
|
|||||||
OpenFileButton.IsEnabled = !_isRunning;
|
OpenFileButton.IsEnabled = !_isRunning;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SelectFilePathAsync()
|
|
||||||
{
|
|
||||||
SaveFilePickerResult result = await StorageProvider.SaveFilePickerWithResultAsync(new FilePickerSaveOptions
|
|
||||||
{
|
|
||||||
Title = "Select where to save the Log file",
|
|
||||||
SuggestedFileName = "SharpEmuLog",
|
|
||||||
DefaultExtension = "log",
|
|
||||||
FileTypeChoices =
|
|
||||||
[
|
|
||||||
new FilePickerFileType("Plain Text Files") { Patterns = ["*.txt"] },
|
|
||||||
new FilePickerFileType("Log Files") { Patterns = ["*.log"] }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.File is not null)
|
|
||||||
{
|
|
||||||
_settings.LogFilePath = result.File.Path.LocalPath;
|
|
||||||
ToolTip.SetTip(SelectLogFilePathButton, _settings.LogFilePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Console ----
|
// ---- Console ----
|
||||||
|
|
||||||
private void FlushPendingConsoleLines()
|
private void FlushPendingConsoleLines()
|
||||||
@@ -1263,7 +1533,8 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
var incoming = new List<LogLine>();
|
var incoming = new List<LogLine>();
|
||||||
while (_pendingLines.TryDequeue(out var pending))
|
while (incoming.Count < MaxConsoleLinesPerFlush &&
|
||||||
|
_pendingLines.TryDequeue(out var pending))
|
||||||
{
|
{
|
||||||
WriteFileLog(pending.Line);
|
WriteFileLog(pending.Line);
|
||||||
incoming.Add(new LogLine(pending.Line, BrushForLine(pending.Line)));
|
incoming.Add(new LogLine(pending.Line, BrushForLine(pending.Line)));
|
||||||
|
|||||||
@@ -28,8 +28,18 @@ 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>
|
||||||
|
<EmbeddedResource Include="Languages\*.json">
|
||||||
|
<LogicalName>Languages.%(Filename)%(Extension)</LogicalName>
|
||||||
|
</EmbeddedResource>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- The controller readers (DualSense raw HID + Xbox XInput) are shared
|
<!-- The controller readers (DualSense raw HID + Xbox XInput) are shared
|
||||||
with the emulator's pad HLE. They are dependency-free, so they are
|
with the emulator's pad HLE. They are dependency-free, so they are
|
||||||
compiled in directly rather than pulling a reference to all of
|
compiled in directly rather than pulling a reference to all of
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ internal sealed class SndPreviewPlayer
|
|||||||
{
|
{
|
||||||
// Debounce so skimming through the library does not decode (or
|
// Debounce so skimming through the library does not decode (or
|
||||||
// start) a preview per tile.
|
// start) a preview per tile.
|
||||||
await Task.Delay(300).ConfigureAwait(false);
|
await Task.Delay(120).ConfigureAwait(false);
|
||||||
|
|
||||||
byte[]? wav;
|
byte[]? wav;
|
||||||
lock (_sync)
|
lock (_sync)
|
||||||
|
|||||||
Binary file not shown.
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,18 +3,17 @@
|
|||||||
|
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using SharpEmu.Logging;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace SharpEmu.HLE;
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
public sealed class ModuleManager : IModuleManager
|
public sealed class ModuleManager : IModuleManager
|
||||||
{
|
{
|
||||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("HLE");
|
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<string, Delegate> _dispatchTable = new(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, Delegate> _dispatchTable = new(StringComparer.Ordinal);
|
||||||
private readonly ConcurrentDictionary<string, ExportedFunction> _exportTable = new(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, ExportedFunction> _exportTable = new(StringComparer.Ordinal);
|
||||||
private readonly ConcurrentDictionary<string, ExportedFunction> _exportNameTable = new(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, ExportedFunction> _exportNameTable = new(StringComparer.Ordinal);
|
||||||
private readonly object _registrationGate = new();
|
private readonly object _registrationGate = new();
|
||||||
|
private readonly HashSet<(Assembly Assembly, Generation Generation)> _scannedAssemblies = new();
|
||||||
private bool _isFrozen;
|
private bool _isFrozen;
|
||||||
|
|
||||||
public int RegisterFromAssembly(Assembly assembly, Generation generation, ISymbolCatalog? symbolCatalog = null)
|
public int RegisterFromAssembly(Assembly assembly, Generation generation, ISymbolCatalog? symbolCatalog = null)
|
||||||
@@ -28,6 +27,12 @@ public sealed class ModuleManager : IModuleManager
|
|||||||
throw new InvalidOperationException("Module registration is frozen.");
|
throw new InvalidOperationException("Module registration is frozen.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deduplicated: one assembly is reached through many types.
|
||||||
|
if (!_scannedAssemblies.Add((assembly, generation)))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
var registeredCount = 0;
|
var registeredCount = 0;
|
||||||
var instances = new Dictionary<Type, object>();
|
var instances = new Dictionary<Type, object>();
|
||||||
|
|
||||||
@@ -50,7 +55,7 @@ public sealed class ModuleManager : IModuleManager
|
|||||||
var handler = CreateHandler(type, method, instances);
|
var handler = CreateHandler(type, method, instances);
|
||||||
if (!_dispatchTable.TryAdd(exportInfo.Value.Nid, handler))
|
if (!_dispatchTable.TryAdd(exportInfo.Value.Nid, handler))
|
||||||
{
|
{
|
||||||
Log.Warning($"Duplicate NID '{exportInfo.Value.Nid}' ({exportInfo.Value.ExportName}) — already registered, skipping.");
|
Console.Error.WriteLine($"[HLE] Duplicate NID '{exportInfo.Value.Nid}' ({exportInfo.Value.ExportName}) — already registered, skipping.");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,8 +81,195 @@ public sealed class ModuleManager : IModuleManager
|
|||||||
{
|
{
|
||||||
_isFrozen = true;
|
_isFrozen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WarmHleTypeInitializers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A .cctor or first JIT running on a guest thread's hijacked stack fail-fasts the CLR.
|
||||||
|
// Run every HLE type's initializer and JIT its methods here first, on a host thread.
|
||||||
|
private void WarmHleTypeInitializers()
|
||||||
|
{
|
||||||
|
Assembly[] assemblies;
|
||||||
|
lock (_registrationGate)
|
||||||
|
{
|
||||||
|
assemblies = _scannedAssemblies.Select(entry => entry.Assembly).Distinct().ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
assemblies = WithGuestReachableDependencies(assemblies);
|
||||||
|
var bclWarmed = WarmFrameworkTypeInitializers();
|
||||||
|
|
||||||
|
var warmed = 0;
|
||||||
|
var failed = 0;
|
||||||
|
var jitted = 0;
|
||||||
|
var jitFailed = 0;
|
||||||
|
foreach (var assembly in assemblies)
|
||||||
|
{
|
||||||
|
Type[] types;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
types = assembly.GetTypes();
|
||||||
|
}
|
||||||
|
catch (ReflectionTypeLoadException ex)
|
||||||
|
{
|
||||||
|
types = ex.Types.Where(t => t is not null).ToArray()!;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BindingFlags allMembers = BindingFlags.Public | BindingFlags.NonPublic |
|
||||||
|
BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
|
||||||
|
|
||||||
|
foreach (var type in types)
|
||||||
|
{
|
||||||
|
if (type is null || type.ContainsGenericParameters)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RuntimeHelpers.RunClassConstructor(type.TypeHandle);
|
||||||
|
warmed++;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// A throw here beats a guest-thread fail-fast later; swallow and continue.
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force-JIT (not execute) every method so no guest thread compiles one first.
|
||||||
|
MethodBase[] members;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
members = type.GetConstructors(allMembers)
|
||||||
|
.Concat<MethodBase>(type.GetMethods(allMembers))
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var member in members)
|
||||||
|
{
|
||||||
|
if (member.ContainsGenericParameters || member.IsAbstract || member.MethodImplementationFlags.HasFlag(MethodImplAttributes.InternalCall))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RuntimeHelpers.PrepareMethod(member.MethodHandle);
|
||||||
|
jitted++;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
jitFailed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[HLE] Warmed {warmed} type initializers ({failed} threw) + JIT-compiled {jitted} methods ({jitFailed} skipped) across {assemblies.Length} HLE assemblies, plus {bclWarmed} framework type initializers.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Framework .cctors too (but not JIT — the BCL is too large).
|
||||||
|
private static int WarmFrameworkTypeInitializers()
|
||||||
|
{
|
||||||
|
var warmed = 0;
|
||||||
|
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||||
|
{
|
||||||
|
var name = assembly.GetName().Name;
|
||||||
|
if (name is null || !IsFrameworkAssembly(name))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Type[] types;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
types = assembly.GetTypes();
|
||||||
|
}
|
||||||
|
catch (ReflectionTypeLoadException ex)
|
||||||
|
{
|
||||||
|
types = ex.Types.Where(t => t is not null).ToArray()!;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var type in types)
|
||||||
|
{
|
||||||
|
if (type is null || type.ContainsGenericParameters)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RuntimeHelpers.RunClassConstructor(type.TypeHandle);
|
||||||
|
warmed++;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return warmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsFrameworkAssembly(string assemblyName) =>
|
||||||
|
assemblyName.StartsWith("System", StringComparison.Ordinal) ||
|
||||||
|
string.Equals(assemblyName, "netstandard", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
// Warm the interop assemblies guest threads reach (e.g. Silk.NET via the flip path).
|
||||||
|
private static Assembly[] WithGuestReachableDependencies(Assembly[] scanned)
|
||||||
|
{
|
||||||
|
var result = new Dictionary<string, Assembly>(StringComparer.Ordinal);
|
||||||
|
foreach (var assembly in scanned)
|
||||||
|
{
|
||||||
|
result[assembly.FullName ?? assembly.GetName().Name ?? string.Empty] = assembly;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var assembly in scanned)
|
||||||
|
{
|
||||||
|
AssemblyName[] references;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
references = assembly.GetReferencedAssemblies();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var reference in references)
|
||||||
|
{
|
||||||
|
var name = reference.Name;
|
||||||
|
if (name is null || !IsGuestReachableInterop(name))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var loaded = Assembly.Load(reference);
|
||||||
|
result[loaded.FullName ?? name] = loaded;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Values.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsGuestReachableInterop(string assemblyName) =>
|
||||||
|
assemblyName.StartsWith("Silk.NET", StringComparison.Ordinal) ||
|
||||||
|
assemblyName.StartsWith("SharpEmu", StringComparison.Ordinal);
|
||||||
|
|
||||||
public bool TryGetFunction(string nid, out Delegate function)
|
public bool TryGetFunction(string nid, out Delegate function)
|
||||||
{
|
{
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(nid);
|
ArgumentException.ThrowIfNullOrWhiteSpace(nid);
|
||||||
@@ -109,7 +301,7 @@ public sealed class ModuleManager : IModuleManager
|
|||||||
|
|
||||||
if (!_dispatchTable.TryGetValue(nid, out var function) || !_exportTable.TryGetValue(nid, out var export))
|
if (!_dispatchTable.TryGetValue(nid, out var function) || !_exportTable.TryGetValue(nid, out var export))
|
||||||
{
|
{
|
||||||
Log.Warning($"NID '{nid}' not found in dispatch table.");
|
Console.Error.WriteLine($"[HLE] NID '{nid}' not found in dispatch table.");
|
||||||
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
return false;
|
return false;
|
||||||
@@ -117,7 +309,7 @@ public sealed class ModuleManager : IModuleManager
|
|||||||
|
|
||||||
if ((export.Target & context.TargetGeneration) == 0)
|
if ((export.Target & context.TargetGeneration) == 0)
|
||||||
{
|
{
|
||||||
Log.Warning($"NID '{nid}' ({export.Name}) found but not implemented for generation {context.TargetGeneration} (targets: {export.Target}).");
|
Console.Error.WriteLine($"[HLE] NID '{nid}' ({export.Name}) found but not implemented for generation {context.TargetGeneration} (targets: {export.Target}).");
|
||||||
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
|
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
|
||||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED;
|
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED;
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -120,6 +120,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 +157,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 +391,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(
|
||||||
@@ -1999,8 +2019,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 +2058,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 +2134,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 +2164,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 +2263,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 +2364,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 +3329,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 +3443,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 +3463,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 +3512,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 +3582,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 +3594,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 +3603,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 +3619,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 +3632,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 +3647,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 +3684,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 +3708,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 +3850,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 +3893,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 +4654,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 +4724,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 +5231,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -621,6 +621,8 @@ internal static class Gen5ShaderTranslator
|
|||||||
0x10 => "SSendmsg",
|
0x10 => "SSendmsg",
|
||||||
0x16 => "STtraceData",
|
0x16 => "STtraceData",
|
||||||
0x20 => "SInstPrefetch",
|
0x20 => "SInstPrefetch",
|
||||||
|
0x21 => "SClause",
|
||||||
|
0x23 => "SWaitcntDepctr",
|
||||||
_ => string.Empty,
|
_ => string.Empty,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -800,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",
|
||||||
@@ -900,6 +909,7 @@ internal static class Gen5ShaderTranslator
|
|||||||
0x169 => "VMulLoU32",
|
0x169 => "VMulLoU32",
|
||||||
0x16A => "VMulHiU32",
|
0x16A => "VMulHiU32",
|
||||||
0x16B => "VMulLoI32",
|
0x16B => "VMulLoI32",
|
||||||
|
0x16C => "VMulHiI32",
|
||||||
0x360 => "VMadU32U16",
|
0x360 => "VMadU32U16",
|
||||||
0x361 => "VMulLoU32",
|
0x361 => "VMulLoU32",
|
||||||
0x362 => "VLdexpF32",
|
0x362 => "VLdexpF32",
|
||||||
|
|||||||
@@ -337,6 +337,7 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
result = EmitSubtractWithBorrow(instruction, reverse: true);
|
result = EmitSubtractWithBorrow(instruction, reverse: true);
|
||||||
break;
|
break;
|
||||||
case "VMulLoU32":
|
case "VMulLoU32":
|
||||||
|
case "VMulLoI32":
|
||||||
case "VMulU32U24":
|
case "VMulU32U24":
|
||||||
result = EmitIntegerBinary(instruction, SpirvOp.IMul);
|
result = EmitIntegerBinary(instruction, SpirvOp.IMul);
|
||||||
break;
|
break;
|
||||||
@@ -372,6 +373,37 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
_module.Constant64(_ulongType, 32)));
|
_module.Constant64(_ulongType, 32)));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "VMulHiI32":
|
||||||
|
{
|
||||||
|
var wideLeft = _module.AddInstruction(
|
||||||
|
SpirvOp.SConvert,
|
||||||
|
_longType,
|
||||||
|
Bitcast(_intType, GetRawSource(instruction, 0)));
|
||||||
|
var wideRight = _module.AddInstruction(
|
||||||
|
SpirvOp.SConvert,
|
||||||
|
_longType,
|
||||||
|
Bitcast(_intType, GetRawSource(instruction, 1)));
|
||||||
|
var product = _module.AddInstruction(
|
||||||
|
SpirvOp.IMul,
|
||||||
|
_longType,
|
||||||
|
wideLeft,
|
||||||
|
wideRight);
|
||||||
|
result = _module.AddInstruction(
|
||||||
|
SpirvOp.UConvert,
|
||||||
|
_uintType,
|
||||||
|
ShiftRightLogical64(
|
||||||
|
Bitcast(_ulongType, product),
|
||||||
|
_module.Constant64(_ulongType, 32)));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "VBcntU32B32":
|
||||||
|
result = IAdd(
|
||||||
|
_module.AddInstruction(
|
||||||
|
SpirvOp.BitCount,
|
||||||
|
_uintType,
|
||||||
|
GetRawSource(instruction, 0)),
|
||||||
|
GetRawSource(instruction, 1));
|
||||||
|
break;
|
||||||
case "VMadU32U24":
|
case "VMadU32U24":
|
||||||
{
|
{
|
||||||
var left = BitwiseAnd(
|
var left = BitwiseAnd(
|
||||||
@@ -569,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));
|
||||||
@@ -678,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;
|
||||||
@@ -809,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))
|
||||||
{
|
{
|
||||||
@@ -823,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,
|
||||||
@@ -873,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(
|
||||||
@@ -883,6 +956,10 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
condition);
|
condition);
|
||||||
StoreWaveMask(126, active);
|
StoreWaveMask(126, active);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StoreWaveMask(106, condition);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1112,6 +1189,52 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
left,
|
left,
|
||||||
right);
|
right);
|
||||||
break;
|
break;
|
||||||
|
case "SMulHiU32":
|
||||||
|
{
|
||||||
|
var wideLeft = _module.AddInstruction(
|
||||||
|
SpirvOp.UConvert,
|
||||||
|
_ulongType,
|
||||||
|
left);
|
||||||
|
var wideRight = _module.AddInstruction(
|
||||||
|
SpirvOp.UConvert,
|
||||||
|
_ulongType,
|
||||||
|
right);
|
||||||
|
var product = _module.AddInstruction(
|
||||||
|
SpirvOp.IMul,
|
||||||
|
_ulongType,
|
||||||
|
wideLeft,
|
||||||
|
wideRight);
|
||||||
|
result = _module.AddInstruction(
|
||||||
|
SpirvOp.UConvert,
|
||||||
|
_uintType,
|
||||||
|
ShiftRightLogical64(
|
||||||
|
product,
|
||||||
|
_module.Constant64(_ulongType, 32)));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "SMulHiI32":
|
||||||
|
{
|
||||||
|
var wideLeft = _module.AddInstruction(
|
||||||
|
SpirvOp.SConvert,
|
||||||
|
_longType,
|
||||||
|
Bitcast(_intType, left));
|
||||||
|
var wideRight = _module.AddInstruction(
|
||||||
|
SpirvOp.SConvert,
|
||||||
|
_longType,
|
||||||
|
Bitcast(_intType, right));
|
||||||
|
var product = _module.AddInstruction(
|
||||||
|
SpirvOp.IMul,
|
||||||
|
_longType,
|
||||||
|
wideLeft,
|
||||||
|
wideRight);
|
||||||
|
result = _module.AddInstruction(
|
||||||
|
SpirvOp.UConvert,
|
||||||
|
_uintType,
|
||||||
|
ShiftRightLogical64(
|
||||||
|
Bitcast(_ulongType, product),
|
||||||
|
_module.Constant64(_ulongType, 32)));
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "SAndB32":
|
case "SAndB32":
|
||||||
result = BitwiseAnd(left, right);
|
result = BitwiseAnd(left, right);
|
||||||
Store(_scc, IsNotZero(result));
|
Store(_scc, IsNotZero(result));
|
||||||
@@ -1233,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),
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Ajm;
|
||||||
|
|
||||||
|
public static class AjmExports
|
||||||
|
{
|
||||||
|
private const int OrbisAjmErrorInvalidParameter = unchecked((int)0x80930005);
|
||||||
|
private static int _nextContextId;
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "dl+4eHSzUu4",
|
||||||
|
ExportName = "sceAjmInitialize",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceAjm")]
|
||||||
|
public static int Initialize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var reserved = ctx[CpuRegister.Rdi];
|
||||||
|
var outContextAddress = ctx[CpuRegister.Rsi];
|
||||||
|
|
||||||
|
// AJM requires a zero reserved argument.
|
||||||
|
if (reserved != 0 || outContextAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisAjmErrorInvalidParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
var contextId = unchecked((uint)Interlocked.Increment(ref _nextContextId));
|
||||||
|
if (!ctx.TryWriteUInt32(outContextAddress, contextId))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisAjmErrorInvalidParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ public static class AudioOut2Exports
|
|||||||
{
|
{
|
||||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
var outUserAddress = ctx[CpuRegister.Rsi];
|
var outUserAddress = ctx[CpuRegister.Rsi];
|
||||||
if ((userId != 0 && userId != 1 && userId != 255) || outUserAddress == 0)
|
if ((userId != 0 && userId != 1 && userId != 1000 && userId != 255) || outUserAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,6 +213,17 @@ public static class AudioOutExports
|
|||||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
: (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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Runtime.Versioning;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs;
|
||||||
|
|
||||||
|
public static class HostTimerResolution
|
||||||
|
{
|
||||||
|
private const uint TargetPeriodMilliseconds = 1;
|
||||||
|
|
||||||
|
private static int _requested;
|
||||||
|
|
||||||
|
public static void Request()
|
||||||
|
{
|
||||||
|
if (Interlocked.Exchange(ref _requested, 1) != 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (TimeBeginPeriod(TargetPeriodMilliseconds) != 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][WARN] Host timer resolution request rejected; " +
|
||||||
|
"timed waits keep the default ~15.6 ms granularity.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (DllNotFoundException exception)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] Host timer resolution unavailable: {exception.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SupportedOSPlatform("windows")]
|
||||||
|
[DllImport("winmm.dll", EntryPoint = "timeBeginPeriod", ExactSpelling = true)]
|
||||||
|
private static extern uint TimeBeginPeriod(uint uPeriod);
|
||||||
|
}
|
||||||
@@ -78,6 +78,82 @@ 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(
|
||||||
|
Nid = "WSOuge5IsCg",
|
||||||
|
ExportName = "_ZN3sce4Json14InitParameter2C1Ev",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson2")]
|
||||||
|
public static int InitParameter2Constructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
TraceJson("InitParameter2.ctor", thisAddress, ctx[CpuRegister.Rsi]);
|
||||||
|
ctx[CpuRegister.Rax] = thisAddress;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "I2QC8PYhJWY",
|
||||||
|
ExportName = "_ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson2")]
|
||||||
|
public static int InitParameter2SetAllocator(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
TraceJson("InitParameter2.setAllocator", thisAddress, ctx[CpuRegister.Rsi]);
|
||||||
|
ctx[CpuRegister.Rax] = thisAddress;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Eu95jmqn5Rw",
|
||||||
|
ExportName = "_ZN3sce4Json14InitParameter217setFileBufferSizeEm",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson2")]
|
||||||
|
public static int InitParameter2SetFileBufferSize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
TraceJson("InitParameter2.setFileBufferSize", thisAddress, ctx[CpuRegister.Rsi]);
|
||||||
|
ctx[CpuRegister.Rax] = thisAddress;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "IXW-z8pggfg",
|
||||||
|
ExportName = "_ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson2")]
|
||||||
|
public static int Initializer2Constructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
TraceJson("Initializer2.ctor", thisAddress, ctx[CpuRegister.Rsi]);
|
||||||
|
ctx[CpuRegister.Rax] = thisAddress;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
private static void TraceJson(string operation, ulong thisAddress, ulong argument)
|
private static void TraceJson(string operation, ulong thisAddress, ulong argument)
|
||||||
{
|
{
|
||||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_JSON"), "1", StringComparison.Ordinal))
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_JSON"), "1", StringComparison.Ordinal))
|
||||||
|
|||||||
@@ -0,0 +1,234 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Json;
|
||||||
|
|
||||||
|
// sce::Json::Value and sce::Json::String constructors, setters and destructors. Prospero titles
|
||||||
|
// (Quake among them) build a Value tree and populate it through these before serializing it for a
|
||||||
|
// web request; without them the imports resolve to nothing and the guest faults on the call. The
|
||||||
|
// payload is modelled in JsonObjectHeap; here we only translate the C++ ABI (registers in, `this`
|
||||||
|
// back out) and never write the guest object, so a stack-allocated Value/String is left intact.
|
||||||
|
//
|
||||||
|
// Only the complete-object variants (C1/D1) are bound — those are what standalone locals emit and
|
||||||
|
// what the observed Prospero imports use. The base-object variants (C2/D2) are left for a title
|
||||||
|
// that actually imports them.
|
||||||
|
public static class JsonValueExports
|
||||||
|
{
|
||||||
|
private const int MaxStringLength = 0x10000;
|
||||||
|
|
||||||
|
private static int ReturnThis(CpuContext ctx, ulong thisAddress)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = thisAddress;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ReturnVoid(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double ReadDoubleArg(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx.GetXmmRegister(0, out var low, out _);
|
||||||
|
return BitConverter.Int64BitsToDouble(unchecked((long)low));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadCString(CpuContext ctx, ulong address) =>
|
||||||
|
ctx.TryReadNullTerminatedUtf8(address, MaxStringLength, out var text) ? text : string.Empty;
|
||||||
|
|
||||||
|
// ---- sce::Json::Value constructors ----
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "qBMjqyBn3OM", ExportName = "_ZN3sce4Json5ValueC1Ev",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueDefaultConstructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetValue(thisAddress, JsonValueState.Null);
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "UeuWT+yNdCQ", ExportName = "_ZN3sce4Json5ValueC1Eb",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueBooleanConstructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromBoolean((ctx[CpuRegister.Rsi] & 0xFF) != 0));
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "0lLK8+kDqmE", ExportName = "_ZN3sce4Json5ValueC1El",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueIntegerConstructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromInteger(unchecked((long)ctx[CpuRegister.Rsi])));
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "x4AUdbhpRB0", ExportName = "_ZN3sce4Json5ValueC1Em",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueUnsignedConstructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromUnsignedInteger(ctx[CpuRegister.Rsi]));
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "sOmU4vnx3s0", ExportName = "_ZN3sce4Json5ValueC1Ed",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueRealConstructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromReal(ReadDoubleArg(ctx)));
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "b9V6fmppLXY", ExportName = "_ZN3sce4Json5ValueC1EPKc",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueCStringConstructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromString(ReadCString(ctx, ctx[CpuRegister.Rsi])));
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "CbrT3dwDILo", ExportName = "_ZN3sce4Json5ValueC1ENS0_9ValueTypeE",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueTypeConstructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromExplicitType(unchecked((uint)ctx[CpuRegister.Rsi])));
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "sZIoMRGO+jk", ExportName = "_ZN3sce4Json5ValueC1ERKNS0_6StringE",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueStringConstructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromString(JsonObjectHeap.GetStringOrEmpty(ctx[CpuRegister.Rsi])));
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "WTtYf+cNnXI", ExportName = "_ZN3sce4Json5ValueD1Ev",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueDestructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
JsonObjectHeap.RemoveValue(ctx[CpuRegister.Rdi]);
|
||||||
|
return ReturnVoid(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- sce::Json::Value setters (return Value&, i.e. `this`) ----
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "5yHuiWXo2gg", ExportName = "_ZN3sce4Json5Value3setEb",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueSetBoolean(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromBoolean((ctx[CpuRegister.Rsi] & 0xFF) != 0));
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "QxVVYhP-mvg", ExportName = "_ZN3sce4Json5Value3setEl",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueSetInteger(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromInteger(unchecked((long)ctx[CpuRegister.Rsi])));
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "SIe1ZmW7e7s", ExportName = "_ZN3sce4Json5Value3setEm",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueSetUnsigned(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromUnsignedInteger(ctx[CpuRegister.Rsi]));
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "BSmWDIkV4w4", ExportName = "_ZN3sce4Json5Value3setEd",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueSetReal(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromReal(ReadDoubleArg(ctx)));
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "IKQimvG9Wqs", ExportName = "_ZN3sce4Json5Value3setENS0_9ValueTypeE",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueSetType(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromExplicitType(unchecked((uint)ctx[CpuRegister.Rsi])));
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "n6FC+l9DU70", ExportName = "_ZN3sce4Json5Value3setEPKc",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueSetCString(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromString(ReadCString(ctx, ctx[CpuRegister.Rsi])));
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "6l3Bv2gysNc", ExportName = "_ZN3sce4Json5Value3setERKNS0_6StringE",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueSetString(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromString(JsonObjectHeap.GetStringOrEmpty(ctx[CpuRegister.Rsi])));
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "FIjXN2TkuTs", ExportName = "_ZN3sce4Json5Value5clearEv",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueClear(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetValue(thisAddress, JsonValueState.Null);
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- sce::Json::String ----
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "9KUZFjI1IxA", ExportName = "_ZN3sce4Json6StringC1EPKc",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int StringCStringConstructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetString(thisAddress, ReadCString(ctx, ctx[CpuRegister.Rsi]));
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "qSmqLXXCPas", ExportName = "_ZN3sce4Json6StringC1Ev",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int StringDefaultConstructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetString(thisAddress, string.Empty);
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "0CAesfH963Q", ExportName = "_ZN3sce4Json6StringC1ERKS1_",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int StringCopyConstructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetString(thisAddress, JsonObjectHeap.GetStringOrEmpty(ctx[CpuRegister.Rsi]));
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "cG1VE2HMl6c", ExportName = "_ZN3sce4Json6StringD1Ev",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int StringDestructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
JsonObjectHeap.RemoveString(ctx[CpuRegister.Rdi]);
|
||||||
|
return ReturnVoid(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -80,14 +80,8 @@ public static class KernelAprCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
var resultAddress = ResolveWaitResultAddress(waitArg1, waitArg2, submission.ResultAddress);
|
// Completion output was written when the command was submitted.
|
||||||
if (resultAddress != 0 && !TryWriteAprResult(ctx, resultAddress))
|
TraceApr(ctx, "wait", submissionId, submission.CommandBuffer, waitArg1, waitArg2);
|
||||||
{
|
|
||||||
TraceAprWaitFailure(ctx, "wait_result_fault", submissionId, submission.CommandBuffer, waitArg1, waitArg2);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
TraceApr(ctx, "wait", submissionId, submission.CommandBuffer, waitArg1, resultAddress);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,27 +163,6 @@ public static class KernelAprCompatExports
|
|||||||
return ctx.Memory.TryWrite(resultAddress, result);
|
return ctx.Memory.TryWrite(resultAddress, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ulong ResolveWaitResultAddress(ulong waitArg1, ulong waitArg2, ulong submittedResultAddress)
|
|
||||||
{
|
|
||||||
if (waitArg2 == 0)
|
|
||||||
{
|
|
||||||
return submittedResultAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IsAmprCompletionToken(waitArg1) && waitArg2 <= 0xFFFF)
|
|
||||||
{
|
|
||||||
return submittedResultAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
return waitArg2;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsAmprCompletionToken(ulong value)
|
|
||||||
{
|
|
||||||
var tag = value >> 56;
|
|
||||||
return tag is 0x0C or 0x10;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void TraceApr(
|
private static void TraceApr(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
string operation,
|
string operation,
|
||||||
|
|||||||
@@ -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,11 @@ public static class KernelEventFlagCompatExports
|
|||||||
public object Gate { get; } = new();
|
public object Gate { get; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class EventFlagWaiter
|
||||||
|
{
|
||||||
|
public OrbisGen2Result? Result { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "BpFoboUJoZU",
|
Nid = "BpFoboUJoZU",
|
||||||
ExportName = "sceKernelCreateEventFlag",
|
ExportName = "sceKernelCreateEventFlag",
|
||||||
@@ -233,9 +239,47 @@ 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();
|
||||||
|
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
|
ctx,
|
||||||
|
"sceKernelWaitEventFlag",
|
||||||
|
GetEventFlagWakeKey(handle),
|
||||||
|
resumeHandler: () => CompleteBlockedTimedWait(
|
||||||
|
ctx,
|
||||||
|
state,
|
||||||
|
timedWaiter,
|
||||||
|
pattern,
|
||||||
|
waitMode,
|
||||||
|
resultAddress,
|
||||||
|
timeoutAddress,
|
||||||
|
deadline),
|
||||||
|
wakeHandler: () => TryCompleteBlockedTimedWait(
|
||||||
|
ctx,
|
||||||
|
state,
|
||||||
|
timedWaiter,
|
||||||
|
pattern,
|
||||||
|
waitMode,
|
||||||
|
resultAddress),
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,6 +490,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}";
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ public static class KernelEventQueueCompatExports
|
|||||||
{
|
{
|
||||||
private const int KernelEventSize = 0x20;
|
private const int KernelEventSize = 0x20;
|
||||||
public const short KernelEventFilterGraphics = -14;
|
public const short KernelEventFilterGraphics = -14;
|
||||||
|
public const short KernelEventFilterUser = -11;
|
||||||
public const short KernelEventFilterAmpr = -16;
|
public const short KernelEventFilterAmpr = -16;
|
||||||
public const short KernelEventFilterAmprSystem = -17;
|
public const short KernelEventFilterAmprSystem = -17;
|
||||||
|
|
||||||
@@ -89,8 +90,16 @@ public static class KernelEventQueueCompatExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int KernelAddUserEventEdge(CpuContext ctx)
|
public static int KernelAddUserEventEdge(CpuContext ctx)
|
||||||
{
|
{
|
||||||
TraceEventQueue(ctx, "add_user_edge", ctx[CpuRegister.Rdi]);
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
var registered = RegisterEvent(
|
||||||
|
handle,
|
||||||
|
ctx[CpuRegister.Rsi],
|
||||||
|
KernelEventFilterUser,
|
||||||
|
0);
|
||||||
|
TraceEventQueue(ctx, "add_user_edge", handle);
|
||||||
|
return registered
|
||||||
|
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -100,8 +109,16 @@ public static class KernelEventQueueCompatExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int KernelAddUserEvent(CpuContext ctx)
|
public static int KernelAddUserEvent(CpuContext ctx)
|
||||||
{
|
{
|
||||||
TraceEventQueue(ctx, "add_user", ctx[CpuRegister.Rdi]);
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
var registered = RegisterEvent(
|
||||||
|
handle,
|
||||||
|
ctx[CpuRegister.Rsi],
|
||||||
|
KernelEventFilterUser,
|
||||||
|
0);
|
||||||
|
TraceEventQueue(ctx, "add_user", handle);
|
||||||
|
return registered
|
||||||
|
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -111,8 +128,15 @@ public static class KernelEventQueueCompatExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int KernelDeleteUserEvent(CpuContext ctx)
|
public static int KernelDeleteUserEvent(CpuContext ctx)
|
||||||
{
|
{
|
||||||
TraceEventQueue(ctx, "delete_user", ctx[CpuRegister.Rdi]);
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
var deleted = DeleteRegisteredEvent(
|
||||||
|
handle,
|
||||||
|
ctx[CpuRegister.Rsi],
|
||||||
|
KernelEventFilterUser);
|
||||||
|
TraceEventQueue(ctx, "delete_user", handle);
|
||||||
|
return deleted
|
||||||
|
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -122,8 +146,18 @@ public static class KernelEventQueueCompatExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int KernelTriggerUserEvent(CpuContext ctx)
|
public static int KernelTriggerUserEvent(CpuContext ctx)
|
||||||
{
|
{
|
||||||
TraceEventQueue(ctx, "trigger_user", ctx[CpuRegister.Rdi]);
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
var triggered = TriggerRegisteredEvent(
|
||||||
|
handle,
|
||||||
|
ctx[CpuRegister.Rsi],
|
||||||
|
KernelEventFilterUser,
|
||||||
|
flags: 0x21,
|
||||||
|
fflags: 0,
|
||||||
|
data: ctx[CpuRegister.Rdx]);
|
||||||
|
TraceEventQueue(ctx, "trigger_user", handle);
|
||||||
|
return triggered
|
||||||
|
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -475,6 +509,43 @@ public static class KernelEventQueueCompatExports
|
|||||||
return triggeredCount;
|
return triggeredCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TriggerRegisteredEvent(
|
||||||
|
ulong handle,
|
||||||
|
ulong ident,
|
||||||
|
short filter,
|
||||||
|
ushort flags,
|
||||||
|
uint fflags,
|
||||||
|
ulong data)
|
||||||
|
{
|
||||||
|
lock (_eventQueueGate)
|
||||||
|
{
|
||||||
|
if (!_registeredEvents.TryGetValue(handle, out var registrations) ||
|
||||||
|
!registrations.TryGetValue((ident, filter), out var registration))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
||||||
|
{
|
||||||
|
queue = new LinkedList<KernelQueuedEvent>();
|
||||||
|
_pendingEvents[handle] = queue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QueueOrUpdateEvent(
|
||||||
|
queue,
|
||||||
|
new KernelQueuedEvent(
|
||||||
|
registration.Ident,
|
||||||
|
registration.Filter,
|
||||||
|
flags,
|
||||||
|
fflags,
|
||||||
|
data,
|
||||||
|
registration.UserData));
|
||||||
|
}
|
||||||
|
|
||||||
|
WakeEventQueue(handle);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public static bool TriggerDisplayEvent(
|
public static bool TriggerDisplayEvent(
|
||||||
ulong handle,
|
ulong handle,
|
||||||
ulong ident,
|
ulong ident,
|
||||||
@@ -656,6 +727,6 @@ public static class KernelEventQueueCompatExports
|
|||||||
|
|
||||||
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out ulong returnRip);
|
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out ulong returnRip);
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][TRACE] equeue.{operation}: handle=0x{handle:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} ret=0x{returnRip:X16}");
|
$"[LOADER][TRACE] equeue.{operation}: thread=0x{KernelPthreadState.GetCurrentThreadHandle():X16} handle=0x{handle:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} ret=0x{returnRip:X16}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -461,66 +461,10 @@ public static class KernelExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unidentified kernel NIDs observed at runtime; stubbed to return 0 until they are
|
|
||||||
// resolved against scripts/ps5_names.txt.
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "9T2pDF2Ryqg",
|
|
||||||
ExportName = "sceKernelUnknown9T2p",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int KernelUnknown9T2p(CpuContext ctx) => SetZeroReturn(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "TU-d9PfIHPM",
|
|
||||||
ExportName = "sceKernelUnknownTU",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int KernelUnknownTU(CpuContext ctx) => SetZeroReturn(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "iWQWrwiSt8A",
|
|
||||||
ExportName = "sceKernelUnknowniWQW",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int KernelUnknowniWQW(CpuContext ctx) => SetZeroReturn(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "KuOmgKoqCdY",
|
|
||||||
ExportName = "sceKernelUnknownKuOmg",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int KernelUnknownKuOmg(CpuContext ctx) => SetZeroReturn(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "RenI1lL1WFk",
|
|
||||||
ExportName = "sceKernelUnknownRenI",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int KernelUnknownRenI(CpuContext ctx) => SetZeroReturn(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "pQGpHYopAIY",
|
|
||||||
ExportName = "sceKernelUnknownpQGp",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int KernelUnknownpQGp(CpuContext ctx) => SetZeroReturn(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "Rbvt+5Y2iEw",
|
|
||||||
ExportName = "sceKernelUnknownRbvt",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int KernelUnknownRbvt(CpuContext ctx) => SetZeroReturn(ctx);
|
|
||||||
|
|
||||||
// NOTE: "SHtAad20YYM"/"DIxvoy7Ngvk" are sce::Json::Value::getType/getInteger, and
|
// NOTE: "SHtAad20YYM"/"DIxvoy7Ngvk" are sce::Json::Value::getType/getInteger, and
|
||||||
// "3GPpjQdAMTw"/"9rAeANT2tyE"/"tsvEmnenz48" are __cxa_guard_acquire/__cxa_guard_release/
|
// "3GPpjQdAMTw"/"9rAeANT2tyE"/"tsvEmnenz48" are __cxa_guard_acquire/__cxa_guard_release/
|
||||||
// __cxa_atexit (verified by hashing against scripts/ps5_names.txt). Do not register them
|
// __cxa_atexit (verified by hashing against scripts/ps5_names.txt). Do not register them
|
||||||
// here as kernel mutex functions: the cxa guards are implemented in CxxAbiExports.cs and
|
// here as kernel mutex functions: the cxa guards are implemented in CxxAbiExports.cs and
|
||||||
// shadowing them breaks every C++ static-init guard in the game.
|
// shadowing them breaks every C++ static-init guard in the game.
|
||||||
|
|
||||||
private static int SetZeroReturn(CpuContext ctx)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,6 +110,15 @@ public static class KernelMemoryCompatExports
|
|||||||
private static readonly HashSet<string> _negativeStatCache = new(StringComparer.OrdinalIgnoreCase);
|
private static readonly HashSet<string> _negativeStatCache = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private static readonly ConcurrentDictionary<string, ulong> _aprFileSizeCache = new(StringComparer.OrdinalIgnoreCase);
|
private static readonly ConcurrentDictionary<string, ulong> _aprFileSizeCache = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private static long _nextFileDescriptor = 2;
|
private static long _nextFileDescriptor = 2;
|
||||||
|
|
||||||
|
internal static int AllocateGuestFileDescriptor()
|
||||||
|
{
|
||||||
|
lock (_fdGate)
|
||||||
|
{
|
||||||
|
return (int)Interlocked.Increment(ref _nextFileDescriptor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static ulong _nextPhysicalAddress;
|
private static ulong _nextPhysicalAddress;
|
||||||
private static ulong _nextVirtualAddress;
|
private static ulong _nextVirtualAddress;
|
||||||
private static ulong _mainDirectMemoryPoolBase = UnsetMainDirectMemoryPoolBase;
|
private static ulong _mainDirectMemoryPoolBase = UnsetMainDirectMemoryPoolBase;
|
||||||
@@ -191,6 +200,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,
|
||||||
@@ -469,7 +512,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)
|
||||||
@@ -1479,6 +1522,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",
|
||||||
@@ -1555,9 +1806,10 @@ public static class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
var directoryFd = (int)Interlocked.Increment(ref _nextFileDescriptor);
|
int directoryFd;
|
||||||
lock (_fdGate)
|
lock (_fdGate)
|
||||||
{
|
{
|
||||||
|
directoryFd = AllocateGuestFileDescriptor();
|
||||||
_openDirectories[directoryFd] = new OpenDirectory
|
_openDirectories[directoryFd] = new OpenDirectory
|
||||||
{
|
{
|
||||||
Path = hostPath,
|
Path = hostPath,
|
||||||
@@ -1578,9 +1830,10 @@ public static class KernelMemoryCompatExports
|
|||||||
stream.Seek(0, SeekOrigin.End);
|
stream.Seek(0, SeekOrigin.End);
|
||||||
}
|
}
|
||||||
|
|
||||||
var fd = (int)Interlocked.Increment(ref _nextFileDescriptor);
|
int fd;
|
||||||
lock (_fdGate)
|
lock (_fdGate)
|
||||||
{
|
{
|
||||||
|
fd = AllocateGuestFileDescriptor();
|
||||||
_openFiles[fd] = stream;
|
_openFiles[fd] = stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1672,6 +1925,30 @@ public static class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "E6ao34wPw+U",
|
||||||
|
ExportName = "stat",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixStat(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var result = KernelStat(ctx);
|
||||||
|
if (result == (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var errno = result switch
|
||||||
|
{
|
||||||
|
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT => Einval,
|
||||||
|
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT => Efault,
|
||||||
|
_ => 2,
|
||||||
|
};
|
||||||
|
KernelRuntimeCompatExports.TrySetErrno(ctx, errno);
|
||||||
|
ctx[CpuRegister.Rax] = ulong.MaxValue;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "gEpBkcwxUjw",
|
Nid = "gEpBkcwxUjw",
|
||||||
ExportName = "sceKernelAprResolveFilepathsToIdsAndFileSizes",
|
ExportName = "sceKernelAprResolveFilepathsToIdsAndFileSizes",
|
||||||
@@ -1953,6 +2230,12 @@ public static class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (KernelSocketCompatExports.TryCloseSocketFd(fd))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
FileStream? stream;
|
FileStream? stream;
|
||||||
lock (_fdGate)
|
lock (_fdGate)
|
||||||
{
|
{
|
||||||
@@ -1996,6 +2279,17 @@ public static class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (KernelSocketCompatExports.TryReadSocketFd(ctx, fd, bufferAddress, requested, out var socketBytesRead, out var socketError))
|
||||||
|
{
|
||||||
|
if (socketError != OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
{
|
||||||
|
return (int)socketError;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = socketBytesRead;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
FileStream? stream;
|
FileStream? stream;
|
||||||
lock (_fdGate)
|
lock (_fdGate)
|
||||||
{
|
{
|
||||||
@@ -2225,6 +2519,17 @@ public static class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (KernelSocketCompatExports.TryWriteSocketFd(ctx, fd, payload, out var socketError))
|
||||||
|
{
|
||||||
|
if (socketError != OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
{
|
||||||
|
return (int)socketError;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)requested);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
FileStream? stream;
|
FileStream? stream;
|
||||||
lock (_fdGate)
|
lock (_fdGate)
|
||||||
{
|
{
|
||||||
@@ -2974,7 +3279,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)
|
||||||
@@ -3441,6 +3746,18 @@ public static class KernelMemoryCompatExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static bool TryFormatStringFromVaList(CpuContext ctx, string format, ulong vaListAddress, out string rendered)
|
||||||
|
{
|
||||||
|
if (!TryCreateVaListCursor(ctx, vaListAddress, out var vaCursor))
|
||||||
|
{
|
||||||
|
rendered = format;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
rendered = FormatString(ctx, format, ref vaCursor);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static int WriteSnprintfOutput(
|
private static int WriteSnprintfOutput(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
ulong destination,
|
ulong destination,
|
||||||
@@ -3914,7 +4231,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)
|
||||||
@@ -3938,7 +4255,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;
|
||||||
@@ -3959,8 +4276,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;
|
||||||
@@ -4068,7 +4385,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;
|
||||||
@@ -6661,15 +6981,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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.Fiber;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Kernel;
|
namespace SharpEmu.Libs.Kernel;
|
||||||
@@ -24,6 +25,8 @@ public static class KernelPthreadCompatExports
|
|||||||
|
|
||||||
private static readonly object _stateGate = new();
|
private static readonly object _stateGate = new();
|
||||||
private static readonly ConcurrentDictionary<ulong, PthreadMutexState> _mutexStates = new();
|
private static readonly ConcurrentDictionary<ulong, PthreadMutexState> _mutexStates = new();
|
||||||
|
private static readonly ConcurrentDictionary<ulong, string> _mutexWakeKeys = new();
|
||||||
|
private static readonly ConcurrentDictionary<ulong, string> _condWakeKeys = new();
|
||||||
private static readonly Dictionary<ulong, PthreadMutexAttrState> _mutexAttrStates = new();
|
private static readonly Dictionary<ulong, PthreadMutexAttrState> _mutexAttrStates = new();
|
||||||
private static readonly Dictionary<ulong, PthreadCondState> _condStates = new();
|
private static readonly Dictionary<ulong, PthreadCondState> _condStates = new();
|
||||||
private static readonly Dictionary<ulong, object> _onceGates = new();
|
private static readonly Dictionary<ulong, object> _onceGates = new();
|
||||||
@@ -35,6 +38,15 @@ public static class KernelPthreadCompatExports
|
|||||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREAD_CONDS"), "1", StringComparison.Ordinal);
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREAD_CONDS"), "1", StringComparison.Ordinal);
|
||||||
private static readonly HashSet<ulong>? _tracePthreadMutexFilter = ParseTraceAddressFilter(
|
private static readonly HashSet<ulong>? _tracePthreadMutexFilter = ParseTraceAddressFilter(
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREAD_MUTEX_FILTER"));
|
Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREAD_MUTEX_FILTER"));
|
||||||
|
private static readonly bool _enableMutexLockBlocking =
|
||||||
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_MUTEX_LOCK_BLOCKING"), "1", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
// On the outer class deliberately: a static on the nested state classes gives them a
|
||||||
|
// type initializer that first runs on a guest thread and fail-fasts the CLR.
|
||||||
|
private static long _nextMutexWakeId;
|
||||||
|
private static long _nextCondWakeId;
|
||||||
|
private static readonly ConcurrentDictionary<ulong, long> _posixTimedWaitCounts = new();
|
||||||
|
private static readonly ConcurrentDictionary<ulong, byte> _reportedContendedMutexes = new();
|
||||||
|
|
||||||
private sealed class PthreadMutexState
|
private sealed class PthreadMutexState
|
||||||
{
|
{
|
||||||
@@ -43,6 +55,10 @@ public static class KernelPthreadCompatExports
|
|||||||
public int RecursionCount { get; set; }
|
public int RecursionCount { get; set; }
|
||||||
public int Type { get; set; } = MutexTypeErrorCheck;
|
public int Type { get; set; } = MutexTypeErrorCheck;
|
||||||
public int Protocol { get; set; }
|
public int Protocol { get; set; }
|
||||||
|
|
||||||
|
// Keyed on state identity, not the resolved address (which can differ between a
|
||||||
|
// lock and its unlock for the same state, stranding the waiter).
|
||||||
|
public string WakeKey { get; } = "pthread_mutex#" + Interlocked.Increment(ref _nextMutexWakeId).ToString("X");
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class PthreadMutexWaiter
|
private sealed class PthreadMutexWaiter
|
||||||
@@ -56,6 +72,23 @@ public static class KernelPthreadCompatExports
|
|||||||
public object SyncRoot { get; } = new();
|
public object SyncRoot { get; } = new();
|
||||||
public ulong SignalEpoch { get; set; }
|
public ulong SignalEpoch { get; set; }
|
||||||
public int Waiters { get; set; }
|
public int Waiters { get; set; }
|
||||||
|
|
||||||
|
// See PthreadMutexState.WakeKey.
|
||||||
|
public string WakeKey { get; } = "pthread_cond#" + Interlocked.Increment(ref _nextCondWakeId).ToString("X");
|
||||||
|
|
||||||
|
// A signal with no waiter stays pending; the guest often signals before the wait.
|
||||||
|
public int PendingSignals { get; set; }
|
||||||
|
|
||||||
|
public bool TryConsumePendingSignal()
|
||||||
|
{
|
||||||
|
if (PendingSignals <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PendingSignals--;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly record struct PthreadMutexAttrState(int Type, int Protocol);
|
private readonly record struct PthreadMutexAttrState(int Type, int Protocol);
|
||||||
@@ -107,11 +140,32 @@ public static class KernelPthreadCompatExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PthreadYield(CpuContext ctx)
|
public static int PthreadYield(CpuContext ctx)
|
||||||
{
|
{
|
||||||
_ = ctx;
|
GuestThreadExecution.Scheduler?.Pump(ctx, "scePthreadYield");
|
||||||
Thread.Yield();
|
Thread.Yield();
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "GBUY7ywdULE",
|
||||||
|
ExportName = "scePthreadRename",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadRename(CpuContext ctx)
|
||||||
|
{
|
||||||
|
if (_tracePthreads)
|
||||||
|
{
|
||||||
|
var nameAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var name = nameAddress != 0 &&
|
||||||
|
KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, nameAddress, 64, out var value)
|
||||||
|
? value
|
||||||
|
: "<unreadable>";
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] pthread.rename thread=0x{ctx[CpuRegister.Rdi]:X16} name=\"{name}\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "EI-5-jlq2dE",
|
Nid = "EI-5-jlq2dE",
|
||||||
ExportName = "scePthreadGetthreadid",
|
ExportName = "scePthreadGetthreadid",
|
||||||
@@ -293,6 +347,104 @@ public static class KernelPthreadCompatExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PthreadCondTimedwait(CpuContext ctx) => PthreadCondWaitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], timed: true, timeoutUsec: unchecked((uint)ctx[CpuRegister.Rdx]));
|
public static int PthreadCondTimedwait(CpuContext ctx) => PthreadCondWaitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], timed: true, timeoutUsec: unchecked((uint)ctx[CpuRegister.Rdx]));
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "27bAgiJmOh0",
|
||||||
|
ExportName = "pthread_cond_timedwait",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixPthreadCondTimedwait(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var absoluteTimeAddress = ctx[CpuRegister.Rdx];
|
||||||
|
if (absoluteTimeAddress == 0 ||
|
||||||
|
!ctx.TryReadUInt64(absoluteTimeAddress, out var secondsRaw) ||
|
||||||
|
!ctx.TryReadUInt64(absoluteTimeAddress + sizeof(ulong), out var nanosecondsRaw))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var deadlineSeconds = unchecked((long)secondsRaw);
|
||||||
|
var deadlineNanoseconds = unchecked((long)nanosecondsRaw);
|
||||||
|
if (deadlineSeconds < 0 || deadlineNanoseconds is < 0 or >= 1_000_000_000)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
long nowSeconds;
|
||||||
|
long nowNanoseconds;
|
||||||
|
if (deadlineSeconds >= 1_000_000_000)
|
||||||
|
{
|
||||||
|
var now = DateTimeOffset.UtcNow;
|
||||||
|
nowSeconds = now.ToUnixTimeSeconds();
|
||||||
|
nowNanoseconds = (now.Ticks % TimeSpan.TicksPerSecond) * 100;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Engine condition variables use monotonic deadlines.
|
||||||
|
KernelRuntimeCompatExports.GetProcessMonotonicTime(out nowSeconds, out nowNanoseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
var remainingNanoseconds =
|
||||||
|
((Int128)deadlineSeconds * 1_000_000_000 + deadlineNanoseconds) -
|
||||||
|
((Int128)nowSeconds * 1_000_000_000 + nowNanoseconds);
|
||||||
|
var timeoutUsec = remainingNanoseconds <= 0
|
||||||
|
? 0u
|
||||||
|
: (uint)Int128.Min(uint.MaxValue, (remainingNanoseconds + 999) / 1_000);
|
||||||
|
|
||||||
|
TracePosixTimedWait(
|
||||||
|
ctx,
|
||||||
|
deadlineSeconds,
|
||||||
|
deadlineNanoseconds,
|
||||||
|
nowSeconds,
|
||||||
|
nowNanoseconds,
|
||||||
|
remainingNanoseconds,
|
||||||
|
timeoutUsec);
|
||||||
|
|
||||||
|
return PthreadCondWaitCore(
|
||||||
|
ctx,
|
||||||
|
ctx[CpuRegister.Rdi],
|
||||||
|
ctx[CpuRegister.Rsi],
|
||||||
|
timed: true,
|
||||||
|
timeoutUsec,
|
||||||
|
posixResult: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TracePosixTimedWait(
|
||||||
|
CpuContext ctx,
|
||||||
|
long deadlineSeconds,
|
||||||
|
long deadlineNanoseconds,
|
||||||
|
long nowSeconds,
|
||||||
|
long nowNanoseconds,
|
||||||
|
Int128 remainingNanoseconds,
|
||||||
|
uint timeoutUsec)
|
||||||
|
{
|
||||||
|
var condAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var count = _posixTimedWaitCounts.AddOrUpdate(condAddress, 1, static (_, current) => current + 1);
|
||||||
|
if (count != 1 && count % 1_000_000 != 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out var returnRip);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][DIAG] pthread_cond_timedwait#{count}: thread=0x{KernelPthreadState.GetCurrentThreadHandle():X16} " +
|
||||||
|
$"cond=0x{condAddress:X16} mutex=0x{ctx[CpuRegister.Rsi]:X16} abstime=0x{ctx[CpuRegister.Rdx]:X16} " +
|
||||||
|
$"deadline={deadlineSeconds}.{deadlineNanoseconds:D9} now={nowSeconds}.{nowNanoseconds:D9} " +
|
||||||
|
$"remaining_ns={remainingNanoseconds} timeout_us={timeoutUsec} ret=0x{returnRip:X16}");
|
||||||
|
|
||||||
|
if (GuestThreadExecution.Scheduler is not { } scheduler)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var snapshot in scheduler.SnapshotThreads())
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][DIAG] guest_thread handle=0x{snapshot.ThreadHandle:X16} name='{snapshot.Name}' " +
|
||||||
|
$"state={snapshot.State} imports={snapshot.ImportCount} nid={snapshot.LastImportNid ?? "none"} " +
|
||||||
|
$"ret=0x{snapshot.LastReturnRip:X16} block={snapshot.BlockReason ?? "none"}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "kDh-NfxgMtE",
|
Nid = "kDh-NfxgMtE",
|
||||||
ExportName = "scePthreadCondSignal",
|
ExportName = "scePthreadCondSignal",
|
||||||
@@ -327,12 +479,6 @@ public static class KernelPthreadCompatExports
|
|||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PosixPthreadCondSignal(CpuContext ctx) => PthreadCondSignalCore(ctx, ctx[CpuRegister.Rdi], broadcast: false);
|
public static int PosixPthreadCondSignal(CpuContext ctx) => PthreadCondSignalCore(ctx, ctx[CpuRegister.Rdi], broadcast: false);
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "27bAgiJmOh0",
|
|
||||||
ExportName = "pthread_cond_timedwait",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PosixPthreadCondTimedwait(CpuContext ctx) => PthreadCondWaitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], timed: true, timeoutUsec: unchecked((uint)ctx[CpuRegister.Rdx]));
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "m5-2bsNfv7s",
|
Nid = "m5-2bsNfv7s",
|
||||||
@@ -559,42 +705,43 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normal/adaptive mutexes do not report EDEADLK on self-lock.
|
|
||||||
// Under the current single-host-thread guest execution model,
|
|
||||||
// treating them as nested ownership keeps init paths moving
|
|
||||||
// without turning a would-block path into a hard error.
|
|
||||||
state.RecursionCount++;
|
state.RecursionCount++;
|
||||||
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
var ownedResult = tryOnly
|
{
|
||||||
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY
|
var ownedResult = tryOnly
|
||||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY
|
||||||
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, ownedResult);
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
||||||
return ownedResult;
|
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, ownedResult);
|
||||||
|
return ownedResult;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var acquired = state.Semaphore.Wait(0);
|
var acquired = state.Semaphore.Wait(0);
|
||||||
if (!acquired)
|
if (!acquired)
|
||||||
{
|
{
|
||||||
// Guest-thread blocking for pthread_mutex_lock is currently disabled.
|
TraceContendedMutex(ctx, mutexAddress, resolvedAddress, state, currentThreadId);
|
||||||
// Demon's Souls deadlocks during PS5SyncEvent initialization.
|
var waiter = new PthreadMutexWaiter { ThreadId = currentThreadId };
|
||||||
/* var waiter = new PthreadMutexWaiter { ThreadId = currentThreadId };
|
// Fibers retain the synchronous fallback to preserve switch state.
|
||||||
if (!tryOnly &&
|
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
|
||||||
|
var canCooperativelyBlock = _enableMutexLockBlocking || currentFiber == 0;
|
||||||
|
if (canCooperativelyBlock &&
|
||||||
|
!tryOnly &&
|
||||||
GuestThreadExecution.IsGuestThread &&
|
GuestThreadExecution.IsGuestThread &&
|
||||||
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
|
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
|
||||||
GuestThreadExecution.RequestCurrentThreadBlock(
|
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
ctx,
|
ctx,
|
||||||
"pthread_mutex_lock",
|
"pthread_mutex_lock",
|
||||||
GetMutexWakeKey(resolvedAddress),
|
state.WakeKey,
|
||||||
() => CompleteBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter),
|
() => CompleteBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter),
|
||||||
() => TryReserveBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter)))
|
() => TryReserveBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter)))
|
||||||
{
|
{
|
||||||
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;
|
||||||
} */
|
}
|
||||||
|
|
||||||
if (!tryOnly)
|
if (!tryOnly)
|
||||||
{
|
{
|
||||||
@@ -611,6 +758,7 @@ public static class KernelPthreadCompatExports
|
|||||||
|
|
||||||
lock (state)
|
lock (state)
|
||||||
{
|
{
|
||||||
|
DetectMutexDoubleOwner(resolvedAddress, state, currentThreadId, "lock");
|
||||||
state.OwnerThreadId = currentThreadId;
|
state.OwnerThreadId = currentThreadId;
|
||||||
state.RecursionCount = 1;
|
state.RecursionCount = 1;
|
||||||
}
|
}
|
||||||
@@ -619,6 +767,53 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void TraceContendedMutex(
|
||||||
|
CpuContext ctx,
|
||||||
|
ulong mutexAddress,
|
||||||
|
ulong resolvedAddress,
|
||||||
|
PthreadMutexState state,
|
||||||
|
ulong currentThreadId)
|
||||||
|
{
|
||||||
|
if (!_reportedContendedMutexes.TryAdd(resolvedAddress, 0))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong ownerThreadId;
|
||||||
|
int recursionCount;
|
||||||
|
int type;
|
||||||
|
lock (state)
|
||||||
|
{
|
||||||
|
ownerThreadId = state.OwnerThreadId;
|
||||||
|
recursionCount = state.RecursionCount;
|
||||||
|
type = state.Type;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out var returnRip);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][DIAG] pthread_mutex_contended: waiter=0x{currentThreadId:X16} owner=0x{ownerThreadId:X16} " +
|
||||||
|
$"mutex=0x{mutexAddress:X16} resolved=0x{resolvedAddress:X16} recursion={recursionCount} " +
|
||||||
|
$"type={type} semaphore_count={state.Semaphore.CurrentCount} ret=0x{returnRip:X16}");
|
||||||
|
|
||||||
|
if (GuestThreadExecution.Scheduler is not { } scheduler)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var snapshot in scheduler.SnapshotThreads())
|
||||||
|
{
|
||||||
|
if (snapshot.ThreadHandle != currentThreadId && snapshot.ThreadHandle != ownerThreadId)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][DIAG] pthread_mutex_party handle=0x{snapshot.ThreadHandle:X16} name='{snapshot.Name}' " +
|
||||||
|
$"state={snapshot.State} imports={snapshot.ImportCount} nid={snapshot.LastImportNid ?? "none"} " +
|
||||||
|
$"ret=0x{snapshot.LastReturnRip:X16} block={snapshot.BlockReason ?? "none"}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static int PthreadMutexUnlockCore(CpuContext ctx, ulong mutexAddress, bool requireOwner)
|
private static int PthreadMutexUnlockCore(CpuContext ctx, ulong mutexAddress, bool requireOwner)
|
||||||
{
|
{
|
||||||
if (mutexAddress == 0)
|
if (mutexAddress == 0)
|
||||||
@@ -633,16 +828,24 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
||||||
|
var lenientOwnership = state.Type is MutexTypeNormal or MutexTypeAdaptiveNp;
|
||||||
|
|
||||||
var shouldRelease = false;
|
var shouldRelease = false;
|
||||||
lock (state)
|
lock (state)
|
||||||
{
|
{
|
||||||
if (state.RecursionCount <= 0)
|
if (state.RecursionCount <= 0)
|
||||||
{
|
{
|
||||||
|
if (lenientOwnership)
|
||||||
|
{
|
||||||
|
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requireOwner && state.OwnerThreadId != currentThreadId)
|
if (requireOwner && !lenientOwnership && state.OwnerThreadId != currentThreadId)
|
||||||
{
|
{
|
||||||
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED);
|
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
||||||
@@ -661,12 +864,15 @@ public static class KernelPthreadCompatExports
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
state.Semaphore.Release();
|
state.Semaphore.Release();
|
||||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetMutexWakeKey(resolvedAddress), 1);
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(state.WakeKey, 1);
|
||||||
}
|
}
|
||||||
catch (SemaphoreFullException)
|
catch (SemaphoreFullException)
|
||||||
{
|
{
|
||||||
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
if (!lenientOwnership)
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
{
|
||||||
|
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -987,6 +1193,13 @@ public static class KernelPthreadCompatExports
|
|||||||
|
|
||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
|
if (_condStates.TryGetValue(condAddress, out var raced))
|
||||||
|
{
|
||||||
|
resolvedAddress = condAddress;
|
||||||
|
state = raced;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
_condStates[condAddress] = createdState;
|
_condStates[condAddress] = createdState;
|
||||||
_condStates[handle] = createdState;
|
_condStates[handle] = createdState;
|
||||||
}
|
}
|
||||||
@@ -1083,27 +1296,41 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int PthreadCondWaitCore(CpuContext ctx, ulong condAddress, ulong mutexAddress, bool timed, uint timeoutUsec = 0)
|
private static int PthreadCondWaitCore(
|
||||||
|
CpuContext ctx,
|
||||||
|
ulong condAddress,
|
||||||
|
ulong mutexAddress,
|
||||||
|
bool timed,
|
||||||
|
uint timeoutUsec = 0,
|
||||||
|
bool posixResult = false)
|
||||||
{
|
{
|
||||||
if (condAddress == 0 || mutexAddress == 0)
|
if (condAddress == 0 || mutexAddress == 0)
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryResolveCondState(ctx, condAddress, createIfZero: true, out _, out var state))
|
if (!TryResolveCondState(ctx, condAddress, createIfZero: true, out var resolvedCondAddress, out var state))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
var waitResult = (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
var waitResult = (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
var spuriousWake = false;
|
var spuriousWake = false;
|
||||||
|
var releasedRecursion = 0;
|
||||||
lock (state.SyncRoot)
|
lock (state.SyncRoot)
|
||||||
{
|
{
|
||||||
state.Waiters++;
|
state.Waiters++;
|
||||||
var observedEpoch = state.SignalEpoch;
|
var observedEpoch = state.SignalEpoch;
|
||||||
TracePthreadCond("wait-enter", condAddress, mutexAddress, state, timed, waitResult);
|
var consumedPendingSignal = state.TryConsumePendingSignal();
|
||||||
|
TracePthreadCond(
|
||||||
|
consumedPendingSignal ? "wait-enter-pending" : "wait-enter",
|
||||||
|
condAddress,
|
||||||
|
mutexAddress,
|
||||||
|
state,
|
||||||
|
timed,
|
||||||
|
waitResult);
|
||||||
|
|
||||||
var unlockResult = PthreadMutexUnlockCore(ctx, mutexAddress, requireOwner: true);
|
var unlockResult = PthreadMutexFullUnlockForCondWait(ctx, mutexAddress, out releasedRecursion);
|
||||||
if (unlockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
if (unlockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
{
|
{
|
||||||
state.Waiters--;
|
state.Waiters--;
|
||||||
@@ -1111,10 +1338,36 @@ public static class KernelPthreadCompatExports
|
|||||||
return unlockResult;
|
return unlockResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
var scheduler = GuestThreadExecution.Scheduler;
|
if (consumedPendingSignal)
|
||||||
if (!timed && GuestThreadExecution.RequestCurrentThreadBlock("pthread_cond_wait"))
|
|
||||||
{
|
{
|
||||||
TracePthreadCond("wait-block", condAddress, mutexAddress, state, timed, waitResult);
|
state.Waiters = Math.Max(0, state.Waiters - 1);
|
||||||
|
TracePthreadCond("wait-wake-pending", condAddress, mutexAddress, state, timed, waitResult);
|
||||||
|
|
||||||
|
// Relock outside SyncRoot to preserve lock ordering.
|
||||||
|
Monitor.Exit(state.SyncRoot);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var pendingLockResult = PthreadMutexRelockForCondWait(ctx, mutexAddress, releasedRecursion);
|
||||||
|
return pendingLockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK ? pendingLockResult : waitResult;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Balance the surrounding lock statement.
|
||||||
|
Monitor.Enter(state.SyncRoot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var scheduler = GuestThreadExecution.Scheduler;
|
||||||
|
if (GuestThreadExecution.IsGuestThread &&
|
||||||
|
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
|
ctx,
|
||||||
|
timed ? "pthread_cond_timedwait" : "pthread_cond_wait",
|
||||||
|
state.WakeKey,
|
||||||
|
() => ResumePthreadCondWait(ctx, condAddress, mutexAddress, state, observedEpoch, timed, releasedRecursion, posixResult),
|
||||||
|
() => state.SignalEpoch != observedEpoch,
|
||||||
|
timed ? GuestThreadExecution.ComputeDeadlineTimestamp(GetCondWaitTimeout(timeoutUsec)) : 0))
|
||||||
|
{
|
||||||
|
TracePthreadCond(timed ? "wait-block-timed" : "wait-block", condAddress, mutexAddress, state, timed, waitResult);
|
||||||
return waitResult;
|
return waitResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1137,6 +1390,21 @@ public static class KernelPthreadCompatExports
|
|||||||
{
|
{
|
||||||
if (!Monitor.Wait(state.SyncRoot, GetCondSpuriousWakeTimeout()))
|
if (!Monitor.Wait(state.SyncRoot, GetCondSpuriousWakeTimeout()))
|
||||||
{
|
{
|
||||||
|
if (scheduler is not null && !GuestThreadExecution.IsGuestThread)
|
||||||
|
{
|
||||||
|
Monitor.Exit(state.SyncRoot);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
scheduler.Pump(ctx, "pthread_cond_wait");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Monitor.Enter(state.SyncRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
spuriousWake = true;
|
spuriousWake = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1163,7 +1431,7 @@ public static class KernelPthreadCompatExports
|
|||||||
waitResult);
|
waitResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
var lockResult = PthreadMutexLockCore(ctx, mutexAddress, tryOnly: false);
|
var lockResult = PthreadMutexRelockForCondWait(ctx, mutexAddress, releasedRecursion);
|
||||||
if (lockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
if (lockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
{
|
{
|
||||||
TracePthreadCond("wait-relock-fail", condAddress, mutexAddress, state, timed, lockResult);
|
TracePthreadCond("wait-relock-fail", condAddress, mutexAddress, state, timed, lockResult);
|
||||||
@@ -1179,7 +1447,7 @@ public static class KernelPthreadCompatExports
|
|||||||
state,
|
state,
|
||||||
timed,
|
timed,
|
||||||
waitResult);
|
waitResult);
|
||||||
return waitResult;
|
return TranslatePthreadResult(waitResult, posixResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int PthreadCondSignalCore(CpuContext ctx, ulong condAddress, bool broadcast)
|
private static int PthreadCondSignalCore(CpuContext ctx, ulong condAddress, bool broadcast)
|
||||||
@@ -1189,16 +1457,19 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryResolveCondState(ctx, condAddress, createIfZero: true, out _, out var state))
|
if (!TryResolveCondState(ctx, condAddress, createIfZero: true, out var resolvedCondAddress, out var state))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var shouldWakeScheduler = false;
|
||||||
lock (state.SyncRoot)
|
lock (state.SyncRoot)
|
||||||
{
|
{
|
||||||
|
// Advance the epoch even with no waiter registered — it's the wait predicate.
|
||||||
|
state.SignalEpoch++;
|
||||||
if (state.Waiters > 0)
|
if (state.Waiters > 0)
|
||||||
{
|
{
|
||||||
state.SignalEpoch++;
|
shouldWakeScheduler = true;
|
||||||
if (broadcast)
|
if (broadcast)
|
||||||
{
|
{
|
||||||
Monitor.PulseAll(state.SyncRoot);
|
Monitor.PulseAll(state.SyncRoot);
|
||||||
@@ -1208,15 +1479,161 @@ public static class KernelPthreadCompatExports
|
|||||||
Monitor.Pulse(state.SyncRoot);
|
Monitor.Pulse(state.SyncRoot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
state.PendingSignals++;
|
||||||
|
}
|
||||||
|
|
||||||
TracePthreadCond(broadcast ? "broadcast" : "signal", condAddress, mutexAddress: 0, state, timed: false, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
TracePthreadCond(broadcast ? "broadcast" : "signal", condAddress, mutexAddress: 0, state, timed: false, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shouldWakeScheduler)
|
||||||
|
{
|
||||||
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(
|
||||||
|
state.WakeKey,
|
||||||
|
broadcast ? int.MaxValue : 1);
|
||||||
|
}
|
||||||
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int ResumePthreadCondWait(
|
||||||
|
CpuContext ctx,
|
||||||
|
ulong condAddress,
|
||||||
|
ulong mutexAddress,
|
||||||
|
PthreadCondState state,
|
||||||
|
ulong observedEpoch,
|
||||||
|
bool timed,
|
||||||
|
int releasedRecursion,
|
||||||
|
bool posixResult)
|
||||||
|
{
|
||||||
|
var waitResult = (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
lock (state.SyncRoot)
|
||||||
|
{
|
||||||
|
state.Waiters = Math.Max(0, state.Waiters - 1);
|
||||||
|
if (timed && state.SignalEpoch == observedEpoch)
|
||||||
|
{
|
||||||
|
waitResult = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
TracePthreadCond(
|
||||||
|
waitResult == (int)OrbisGen2Result.ORBIS_GEN2_OK ? "wait-resume" : "wait-resume-timeout",
|
||||||
|
condAddress,
|
||||||
|
mutexAddress,
|
||||||
|
state,
|
||||||
|
timed,
|
||||||
|
waitResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
var lockResult = PthreadMutexRelockForCondWait(ctx, mutexAddress, releasedRecursion);
|
||||||
|
return TranslatePthreadResult(
|
||||||
|
lockResult == (int)OrbisGen2Result.ORBIS_GEN2_OK ? waitResult : lockResult,
|
||||||
|
posixResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int TranslatePthreadResult(int result, bool posixResult)
|
||||||
|
{
|
||||||
|
if (!posixResult || result == (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result switch
|
||||||
|
{
|
||||||
|
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED => 1, // EPERM
|
||||||
|
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND => 2, // ENOENT
|
||||||
|
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT => 22, // EINVAL
|
||||||
|
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK => 11, // EDEADLK
|
||||||
|
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED => 13, // EACCES
|
||||||
|
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY => 16, // EBUSY
|
||||||
|
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN => 35, // EAGAIN
|
||||||
|
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT => 60, // ETIMEDOUT
|
||||||
|
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_CANCELED => 85, // ECANCELED
|
||||||
|
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT => 14, // EFAULT
|
||||||
|
_ => result
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetCondWakeKey(ulong resolvedCondAddress) =>
|
||||||
|
_condWakeKeys.GetOrAdd(resolvedCondAddress, static address => string.Create(
|
||||||
|
31,
|
||||||
|
address,
|
||||||
|
static (destination, value) =>
|
||||||
|
{
|
||||||
|
"pthread_cond:0x".AsSpan().CopyTo(destination);
|
||||||
|
_ = value.TryFormat(destination[15..], out _, "X16");
|
||||||
|
}));
|
||||||
|
|
||||||
|
private static int PthreadMutexFullUnlockForCondWait(CpuContext ctx, ulong mutexAddress, out int releasedRecursion)
|
||||||
|
{
|
||||||
|
releasedRecursion = 0;
|
||||||
|
if (!TryResolveMutexState(ctx, mutexAddress, createIfZero: false, out _, out var state))
|
||||||
|
{
|
||||||
|
return PthreadMutexUnlockCore(ctx, mutexAddress, requireOwner: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
||||||
|
lock (state)
|
||||||
|
{
|
||||||
|
if (state.RecursionCount <= 0 || state.OwnerThreadId != currentThreadId)
|
||||||
|
{
|
||||||
|
return PthreadMutexUnlockCore(ctx, mutexAddress, requireOwner: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
releasedRecursion = state.RecursionCount;
|
||||||
|
state.RecursionCount = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PthreadMutexUnlockCore(ctx, mutexAddress, requireOwner: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int PthreadMutexRelockForCondWait(CpuContext ctx, ulong mutexAddress, int releasedRecursion)
|
||||||
|
{
|
||||||
|
var result = PthreadMutexLockCore(ctx, mutexAddress, tryOnly: false);
|
||||||
|
if (result != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (releasedRecursion > 1 &&
|
||||||
|
TryResolveMutexState(ctx, mutexAddress, createIfZero: false, out _, out var state))
|
||||||
|
{
|
||||||
|
lock (state)
|
||||||
|
{
|
||||||
|
if (state.OwnerThreadId == KernelPthreadState.GetCurrentThreadHandle())
|
||||||
|
{
|
||||||
|
state.RecursionCount = releasedRecursion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? DumpMutexStateForStall(ulong mutexAddress)
|
||||||
|
{
|
||||||
|
if (!_mutexStates.TryGetValue(mutexAddress, out var state))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var waiterThreads = string.Join(",", _mutexStates
|
||||||
|
.Where(pair => ReferenceEquals(pair.Value, state))
|
||||||
|
.Select(pair => $"0x{pair.Key:X}"));
|
||||||
|
|
||||||
|
return $"mutex=0x{mutexAddress:X16} owner=0x{state.OwnerThreadId:X} recursion={state.RecursionCount} " +
|
||||||
|
$"type={state.Type} semaphore_count={state.Semaphore.CurrentCount} aliases=[{waiterThreads}]";
|
||||||
|
}
|
||||||
|
|
||||||
private static string GetMutexWakeKey(ulong resolvedMutexAddress) =>
|
private static string GetMutexWakeKey(ulong resolvedMutexAddress) =>
|
||||||
$"pthread_mutex:0x{resolvedMutexAddress:X16}";
|
_mutexWakeKeys.GetOrAdd(resolvedMutexAddress, static address => string.Create(
|
||||||
|
32,
|
||||||
|
address,
|
||||||
|
static (destination, value) =>
|
||||||
|
{
|
||||||
|
"pthread_mutex:0x".AsSpan().CopyTo(destination);
|
||||||
|
_ = value.TryFormat(destination[16..], out _, "X16");
|
||||||
|
}));
|
||||||
|
|
||||||
private static bool TryReserveBlockedMutexLock(
|
private static bool TryReserveBlockedMutexLock(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
@@ -1233,6 +1650,13 @@ public static class KernelPthreadCompatExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!state.Semaphore.Wait(0))
|
||||||
|
{
|
||||||
|
TracePthreadMutex(ctx, "lock-reserve-busy", mutexAddress, resolvedAddress, state, waiter.ThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DetectMutexDoubleOwner(resolvedAddress, state, waiter.ThreadId, "reserve");
|
||||||
state.OwnerThreadId = waiter.ThreadId;
|
state.OwnerThreadId = waiter.ThreadId;
|
||||||
state.RecursionCount = 1;
|
state.RecursionCount = 1;
|
||||||
Interlocked.Exchange(ref waiter.Reserved, 1);
|
Interlocked.Exchange(ref waiter.Reserved, 1);
|
||||||
@@ -1264,6 +1688,7 @@ public static class KernelPthreadCompatExports
|
|||||||
|
|
||||||
lock (state)
|
lock (state)
|
||||||
{
|
{
|
||||||
|
DetectMutexDoubleOwner(resolvedAddress, state, currentThreadId, "resume");
|
||||||
state.OwnerThreadId = currentThreadId;
|
state.OwnerThreadId = currentThreadId;
|
||||||
state.RecursionCount = 1;
|
state.RecursionCount = 1;
|
||||||
}
|
}
|
||||||
@@ -1272,6 +1697,19 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A nonzero foreign prior owner means two guest threads are in the same critical
|
||||||
|
// section. Call while holding lock(state).
|
||||||
|
private static void DetectMutexDoubleOwner(ulong resolvedAddress, PthreadMutexState state, ulong currentThreadId, string site)
|
||||||
|
{
|
||||||
|
var priorOwner = state.OwnerThreadId;
|
||||||
|
if (priorOwner != 0 && priorOwner != currentThreadId)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][ERROR] MUTEX DOUBLE-OWNER at {site}: resolved=0x{resolvedAddress:X} acquirer=0x{currentThreadId:X} " +
|
||||||
|
$"prior_owner=0x{priorOwner:X} recursion={state.RecursionCount} type={state.Type} sem_count={state.Semaphore.CurrentCount}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static TimeSpan GetCondWaitTimeout(uint timeoutUsec)
|
private static TimeSpan GetCondWaitTimeout(uint timeoutUsec)
|
||||||
{
|
{
|
||||||
if (timeoutUsec == 0)
|
if (timeoutUsec == 0)
|
||||||
@@ -1279,7 +1717,10 @@ public static class KernelPthreadCompatExports
|
|||||||
return TimeSpan.Zero;
|
return TimeSpan.Zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
return TimeSpan.FromTicks((long)timeoutUsec * 10L);
|
// Round positive sub-millisecond waits to the host sleep quantum.
|
||||||
|
return timeoutUsec < 1_000
|
||||||
|
? TimeSpan.FromMilliseconds(1)
|
||||||
|
: TimeSpan.FromTicks((long)timeoutUsec * 10L);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TimeSpan GetCondSpuriousWakeTimeout()
|
private static TimeSpan GetCondSpuriousWakeTimeout()
|
||||||
@@ -1409,7 +1850,8 @@ public static class KernelPthreadCompatExports
|
|||||||
$"[LOADER][TRACE] pthread_{operation}: mutex=0x{mutexAddress:X16} resolved=0x{resolvedAddress:X16} " +
|
$"[LOADER][TRACE] pthread_{operation}: mutex=0x{mutexAddress:X16} resolved=0x{resolvedAddress:X16} " +
|
||||||
$"guest[0]=0x{guestWord0:X16} guest[8]=0x{guestWord1:X16} " +
|
$"guest[0]=0x{guestWord0:X16} guest[8]=0x{guestWord1:X16} " +
|
||||||
$"current=0x{currentThreadId:X16} owner=0x{(state?.OwnerThreadId ?? 0):X16} " +
|
$"current=0x{currentThreadId:X16} owner=0x{(state?.OwnerThreadId ?? 0):X16} " +
|
||||||
$"recursion={(state?.RecursionCount ?? 0)} type={(state?.Type ?? 0)} result=0x{unchecked((uint)result):X8}");
|
$"recursion={(state?.RecursionCount ?? 0)} type={(state?.Type ?? 0)} result=0x{unchecked((uint)result):X8} " +
|
||||||
|
$"guest_thread=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} managed={Environment.CurrentManagedThreadId}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void TracePthreadCond(string operation, ulong condAddress, ulong mutexAddress, PthreadCondState? state, bool timed, int result)
|
private static void TracePthreadCond(string operation, ulong condAddress, ulong mutexAddress, PthreadCondState? state, bool timed, int result)
|
||||||
@@ -1420,7 +1862,8 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][TRACE] pthread_cond_{operation}: cond=0x{condAddress:X16} mutex=0x{mutexAddress:X16} " +
|
$"[LOADER][TRACE] pthread_cond_{operation}: thread=0x{KernelPthreadState.GetCurrentThreadHandle():X16} " +
|
||||||
|
$"cond=0x{condAddress:X16} key={state?.WakeKey ?? "none"} mutex=0x{mutexAddress:X16} " +
|
||||||
$"waiters={(state?.Waiters ?? 0)} epoch=0x{(state?.SignalEpoch ?? 0):X} timed={timed} result=0x{unchecked((uint)result):X8}");
|
$"waiters={(state?.Waiters ?? 0)} epoch=0x{(state?.SignalEpoch ?? 0):X} timed={timed} result=0x{unchecked((uint)result):X8}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
private static long _nextSyntheticRwlockHandleId = 1;
|
private static long _nextSyntheticRwlockHandleId = 1;
|
||||||
private static long _nextSyntheticPthreadAttrHandleId = 1;
|
private static long _nextSyntheticPthreadAttrHandleId = 1;
|
||||||
private static long _nextSyntheticRwlockAttrHandleId = 1;
|
private static long _nextSyntheticRwlockAttrHandleId = 1;
|
||||||
|
private static readonly bool _strictRwlockWriterPreference =
|
||||||
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_STRICT_RWLOCK_WRITER_PREFERENCE"), "1", StringComparison.Ordinal);
|
||||||
|
|
||||||
private static readonly ConcurrentDictionary<ulong, ConcurrentDictionary<int, ulong>> _threadLocalSpecific = new();
|
private static readonly ConcurrentDictionary<ulong, ConcurrentDictionary<int, ulong>> _threadLocalSpecific = new();
|
||||||
|
|
||||||
@@ -85,14 +87,23 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
public PthreadAttrState Attributes { get; set; } = PthreadAttrState.Default;
|
public PthreadAttrState Attributes { get; set; } = PthreadAttrState.Default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// On the outer class deliberately: a static on the nested state class gives it a type
|
||||||
|
// initializer that first runs on a guest thread and fail-fasts the CLR.
|
||||||
|
private static long _nextRwlockWakeId;
|
||||||
|
|
||||||
private sealed class PthreadRwlockState
|
private sealed class PthreadRwlockState
|
||||||
{
|
{
|
||||||
public object SyncRoot { get; } = new();
|
public object SyncRoot { get; } = new();
|
||||||
public Dictionary<ulong, int> ReaderCounts { get; } = new();
|
public Dictionary<ulong, int> ReaderCounts { get; } = new();
|
||||||
|
public Dictionary<ulong, int> CompatWriterCounts { get; } = new();
|
||||||
public int ReaderTotalCount { get; set; }
|
public int ReaderTotalCount { get; set; }
|
||||||
|
public int CompatWriterTotalCount { get; set; }
|
||||||
public ulong WriterThreadId { get; set; }
|
public ulong WriterThreadId { get; set; }
|
||||||
public int WaitingWriters { get; set; }
|
public int WaitingWriters { get; set; }
|
||||||
|
|
||||||
|
// See PthreadMutexState.WakeKey.
|
||||||
|
public string WakeKey { get; } = "pthread_rwlock#" + Interlocked.Increment(ref _nextRwlockWakeId).ToString("X");
|
||||||
|
|
||||||
public int GetReaderCount(ulong threadId)
|
public int GetReaderCount(ulong threadId)
|
||||||
{
|
{
|
||||||
return ReaderCounts.TryGetValue(threadId, out var count) ? count : 0;
|
return ReaderCounts.TryGetValue(threadId, out var count) ? count : 0;
|
||||||
@@ -105,6 +116,33 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
ReaderTotalCount++;
|
ReaderTotalCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void AddCompatWriter(ulong threadId)
|
||||||
|
{
|
||||||
|
CompatWriterCounts.TryGetValue(threadId, out var currentCount);
|
||||||
|
CompatWriterCounts[threadId] = currentCount + 1;
|
||||||
|
CompatWriterTotalCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool RemoveCompatWriter(ulong threadId)
|
||||||
|
{
|
||||||
|
if (!CompatWriterCounts.TryGetValue(threadId, out var currentCount) || currentCount <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentCount == 1)
|
||||||
|
{
|
||||||
|
CompatWriterCounts.Remove(threadId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CompatWriterCounts[threadId] = currentCount - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
CompatWriterTotalCount = Math.Max(0, CompatWriterTotalCount - 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public bool RemoveReader(ulong threadId)
|
public bool RemoveReader(ulong threadId)
|
||||||
{
|
{
|
||||||
if (!ReaderCounts.TryGetValue(threadId, out var currentCount) || currentCount <= 0)
|
if (!ReaderCounts.TryGetValue(threadId, out var currentCount) || currentCount <= 0)
|
||||||
@@ -935,7 +973,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
|
|
||||||
lock (state.SyncRoot)
|
lock (state.SyncRoot)
|
||||||
{
|
{
|
||||||
if (state.WriterThreadId != 0 || state.ReaderTotalCount != 0 || state.WaitingWriters != 0)
|
if (state.WriterThreadId != 0 || state.ReaderTotalCount != 0 || state.WaitingWriters != 0 || state.CompatWriterTotalCount != 0)
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||||
}
|
}
|
||||||
@@ -1003,7 +1041,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: false, out _, out var rwlock))
|
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: false, out var resolvedAddress, out var rwlock))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
@@ -1014,7 +1052,11 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
{
|
{
|
||||||
lock (rwlock.SyncRoot)
|
lock (rwlock.SyncRoot)
|
||||||
{
|
{
|
||||||
if (rwlock.WriterThreadId == currentThreadId)
|
if (rwlock.RemoveCompatWriter(currentThreadId))
|
||||||
|
{
|
||||||
|
Monitor.PulseAll(rwlock.SyncRoot);
|
||||||
|
}
|
||||||
|
else if (rwlock.WriterThreadId == currentThreadId)
|
||||||
{
|
{
|
||||||
rwlock.WriterThreadId = 0;
|
rwlock.WriterThreadId = 0;
|
||||||
Monitor.PulseAll(rwlock.SyncRoot);
|
Monitor.PulseAll(rwlock.SyncRoot);
|
||||||
@@ -1037,6 +1079,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(rwlock.WakeKey);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1227,7 +1270,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: true, out _, out var rwlock))
|
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: true, out var resolvedAddress, out var rwlock))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
@@ -1242,20 +1285,60 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (rwlock.CompatWriterCounts.GetValueOrDefault(currentThreadId) > 0)
|
||||||
|
{
|
||||||
|
rwlock.AddCompatWriter(currentThreadId);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GuestThreadExecution.IsGuestThread &&
|
||||||
|
!_strictRwlockWriterPreference &&
|
||||||
|
rwlock.WriterThreadId == 0 &&
|
||||||
|
rwlock.ReaderTotalCount == 0 &&
|
||||||
|
rwlock.CompatWriterTotalCount == 0)
|
||||||
|
{
|
||||||
|
rwlock.AddCompatWriter(currentThreadId);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rwlock.WriterThreadId == 0 && rwlock.ReaderTotalCount == 0 && rwlock.CompatWriterTotalCount == 0)
|
||||||
|
{
|
||||||
|
DetectRwlockWriterConflict(resolvedAddress, rwlock, currentThreadId, "wrlock");
|
||||||
|
rwlock.WriterThreadId = currentThreadId;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
rwlock.WaitingWriters++;
|
rwlock.WaitingWriters++;
|
||||||
|
var transferredToScheduler = false;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
while (rwlock.WriterThreadId != 0 || rwlock.ReaderTotalCount != 0)
|
if (GuestThreadExecution.IsGuestThread &&
|
||||||
|
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
|
||||||
|
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
|
ctx,
|
||||||
|
"pthread_rwlock_wrlock",
|
||||||
|
rwlock.WakeKey,
|
||||||
|
static () => (int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||||
|
() => TryAcquireBlockedRwlock(rwlock, currentThreadId, write: true)))
|
||||||
|
{
|
||||||
|
transferredToScheduler = true;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (rwlock.WriterThreadId != 0 || rwlock.ReaderTotalCount != 0 || rwlock.CompatWriterTotalCount != 0)
|
||||||
{
|
{
|
||||||
Monitor.Wait(rwlock.SyncRoot);
|
Monitor.Wait(rwlock.SyncRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rwlock.WriterThreadId = currentThreadId;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
rwlock.WaitingWriters--;
|
if (!transferredToScheduler)
|
||||||
|
{
|
||||||
|
rwlock.WaitingWriters = Math.Max(0, rwlock.WaitingWriters - 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rwlock.WriterThreadId = currentThreadId;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -1264,12 +1347,30 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (rwlock.WriterThreadId != 0 ||
|
while (ReaderMustWaitForRwlock(rwlock, currentThreadId))
|
||||||
(rwlock.WaitingWriters > 0 && rwlock.GetReaderCount(currentThreadId) == 0))
|
|
||||||
{
|
{
|
||||||
|
if (GuestThreadExecution.IsGuestThread &&
|
||||||
|
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
|
||||||
|
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
|
ctx,
|
||||||
|
"pthread_rwlock_rdlock",
|
||||||
|
rwlock.WakeKey,
|
||||||
|
static () => (int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||||
|
() => TryAcquireBlockedRwlock(rwlock, currentThreadId, write: false)))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
Monitor.Wait(rwlock.SyncRoot);
|
Monitor.Wait(rwlock.SyncRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (rwlock.WriterThreadId != 0 ||
|
||||||
|
rwlock.CompatWriterTotalCount > rwlock.CompatWriterCounts.GetValueOrDefault(currentThreadId))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][ERROR] RWLOCK READER/WRITER COEXIST: resolved=0x{resolvedAddress:X} reader=0x{currentThreadId:X} " +
|
||||||
|
$"writer=0x{rwlock.WriterThreadId:X} compat_total={rwlock.CompatWriterTotalCount} readers_total={rwlock.ReaderTotalCount}");
|
||||||
|
}
|
||||||
rwlock.AddReader(currentThreadId);
|
rwlock.AddReader(currentThreadId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1277,6 +1378,86 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryAcquireBlockedRwlock(PthreadRwlockState rwlock, ulong currentThreadId, bool write)
|
||||||
|
{
|
||||||
|
lock (rwlock.SyncRoot)
|
||||||
|
{
|
||||||
|
if (write)
|
||||||
|
{
|
||||||
|
if (rwlock.WriterThreadId != 0 || rwlock.ReaderTotalCount != 0 || rwlock.CompatWriterTotalCount != 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DetectRwlockWriterConflict(0, rwlock, currentThreadId, "wrlock-resume");
|
||||||
|
rwlock.WriterThreadId = currentThreadId;
|
||||||
|
rwlock.WaitingWriters = Math.Max(0, rwlock.WaitingWriters - 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ReaderMustWaitForRwlock(rwlock, currentThreadId))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
rwlock.AddReader(currentThreadId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call while holding lock(rwlock.SyncRoot): an existing reader/writer here means a
|
||||||
|
// writer would share the rwlock with another holder — a data race.
|
||||||
|
private static void DetectRwlockWriterConflict(ulong resolvedAddress, PthreadRwlockState rwlock, ulong currentThreadId, string site)
|
||||||
|
{
|
||||||
|
if (rwlock.WriterThreadId != 0 ||
|
||||||
|
rwlock.ReaderTotalCount != 0 ||
|
||||||
|
rwlock.CompatWriterTotalCount > rwlock.CompatWriterCounts.GetValueOrDefault(currentThreadId))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][ERROR] RWLOCK WRITER CONFLICT at {site}: resolved=0x{resolvedAddress:X} writer=0x{currentThreadId:X} " +
|
||||||
|
$"existing_writer=0x{rwlock.WriterThreadId:X} readers_total={rwlock.ReaderTotalCount} compat_total={rwlock.CompatWriterTotalCount}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ReaderMustWaitForRwlock(PthreadRwlockState rwlock, ulong currentThreadId)
|
||||||
|
{
|
||||||
|
if (rwlock.WriterThreadId != 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rwlock.CompatWriterTotalCount > rwlock.CompatWriterCounts.GetValueOrDefault(currentThreadId))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rwlock.WaitingWriters > 0 &&
|
||||||
|
rwlock.GetReaderCount(currentThreadId) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetRwlockWakeKey(ulong rwlockAddress) => $"pthread_rwlock:0x{rwlockAddress:X16}";
|
||||||
|
|
||||||
|
public static string? DumpRwlockStateForStall(ulong rwlockAddress)
|
||||||
|
{
|
||||||
|
PthreadRwlockState? rwlock;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
if (!_rwlockStates.TryGetValue(rwlockAddress, out rwlock))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (rwlock.SyncRoot)
|
||||||
|
{
|
||||||
|
var readers = string.Join(",", rwlock.ReaderCounts.Select(pair => $"0x{pair.Key:X}x{pair.Value}"));
|
||||||
|
var compatWriters = string.Join(",", rwlock.CompatWriterCounts.Select(pair => $"0x{pair.Key:X}x{pair.Value}"));
|
||||||
|
return $"rwlock=0x{rwlockAddress:X16} writer=0x{rwlock.WriterThreadId:X} waiting_writers={rwlock.WaitingWriters} " +
|
||||||
|
$"readers_total={rwlock.ReaderTotalCount} readers=[{readers}] " +
|
||||||
|
$"compat_writers_total={rwlock.CompatWriterTotalCount} compat_writers=[{compatWriters}]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static ulong ResolveRwlockHandle(CpuContext ctx, ulong rwlockAddress)
|
private static ulong ResolveRwlockHandle(CpuContext ctx, ulong rwlockAddress)
|
||||||
{
|
{
|
||||||
if (rwlockAddress == 0)
|
if (rwlockAddress == 0)
|
||||||
|
|||||||
@@ -85,22 +85,29 @@ public static class KernelRuntimeCompatExports
|
|||||||
ExportName = "sceKernelNanosleep",
|
ExportName = "sceKernelNanosleep",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int KernelNanosleep(CpuContext ctx)
|
public static int KernelNanosleep(CpuContext ctx) => NanosleepCore(ctx, posix: false);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "yS8U2TGCe1A",
|
||||||
|
ExportName = "nanosleep",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixNanosleep(CpuContext ctx) => NanosleepCore(ctx, posix: true);
|
||||||
|
|
||||||
|
private static int NanosleepCore(CpuContext ctx, bool posix)
|
||||||
{
|
{
|
||||||
var requestAddress = ctx[CpuRegister.Rdi];
|
var requestAddress = ctx[CpuRegister.Rdi];
|
||||||
var remainAddress = ctx[CpuRegister.Rsi];
|
var remainAddress = ctx[CpuRegister.Rsi];
|
||||||
|
|
||||||
if (requestAddress == 0)
|
if (requestAddress == 0)
|
||||||
{
|
{
|
||||||
ctx[CpuRegister.Rax] = Einval;
|
return NanosleepFailure(ctx, posix, Einval, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> timespecBuffer = stackalloc byte[16];
|
Span<byte> timespecBuffer = stackalloc byte[16];
|
||||||
if (!ctx.Memory.TryRead(requestAddress, timespecBuffer))
|
if (!ctx.Memory.TryRead(requestAddress, timespecBuffer))
|
||||||
{
|
{
|
||||||
ctx[CpuRegister.Rax] = Efault;
|
return NanosleepFailure(ctx, posix, Efault, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var tvSec = BinaryPrimitives.ReadInt64LittleEndian(timespecBuffer);
|
var tvSec = BinaryPrimitives.ReadInt64LittleEndian(timespecBuffer);
|
||||||
@@ -108,8 +115,7 @@ public static class KernelRuntimeCompatExports
|
|||||||
|
|
||||||
if (tvSec < 0 || tvNsec < 0 || tvNsec >= 1_000_000_000L)
|
if (tvSec < 0 || tvNsec < 0 || tvNsec >= 1_000_000_000L)
|
||||||
{
|
{
|
||||||
ctx[CpuRegister.Rax] = Einval;
|
return NanosleepFailure(ctx, posix, Einval, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tvSec == 0 && tvNsec == 0)
|
if (tvSec == 0 && tvNsec == 0)
|
||||||
@@ -119,7 +125,7 @@ public static class KernelRuntimeCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
GuestThreadExecution.Scheduler?.Pump(ctx, "sceKernelNanosleep");
|
GuestThreadExecution.Scheduler?.Pump(ctx, posix ? "nanosleep" : "sceKernelNanosleep");
|
||||||
|
|
||||||
// TimeSpan resolution is 100 ns ticks, so sub-100 ns requests round up to
|
// TimeSpan resolution is 100 ns ticks, so sub-100 ns requests round up to
|
||||||
// a single tick rather than collapsing to a zero-length (no-op) sleep.
|
// a single tick rather than collapsing to a zero-length (no-op) sleep.
|
||||||
@@ -138,6 +144,21 @@ public static class KernelRuntimeCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int NanosleepFailure(CpuContext ctx, bool posix, int errnoValue, OrbisGen2Result sceResult)
|
||||||
|
{
|
||||||
|
if (posix)
|
||||||
|
{
|
||||||
|
TrySetErrno(ctx, errnoValue);
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)errnoValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)sceResult;
|
||||||
|
}
|
||||||
|
|
||||||
private static void WriteRemainingTime(CpuContext ctx, ulong remainAddress, long seconds, long nanoseconds)
|
private static void WriteRemainingTime(CpuContext ctx, ulong remainAddress, long seconds, long nanoseconds)
|
||||||
{
|
{
|
||||||
if (remainAddress == 0)
|
if (remainAddress == 0)
|
||||||
@@ -192,6 +213,13 @@ public static class KernelRuntimeCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "QcteRwbsnV0",
|
||||||
|
ExportName = "usleep",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixUsleep(CpuContext ctx) => KernelUsleep(ctx);
|
||||||
|
|
||||||
private static void TraceUsleepSpin(CpuContext ctx, ulong micros)
|
private static void TraceUsleepSpin(CpuContext ctx, ulong micros)
|
||||||
{
|
{
|
||||||
if (!_traceUsleep)
|
if (!_traceUsleep)
|
||||||
@@ -403,6 +431,18 @@ public static class KernelRuntimeCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "HoLVWNanBBc",
|
||||||
|
ExportName = "getpid",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int GetProcessId(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var processId = Environment.ProcessId;
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((uint)processId);
|
||||||
|
return processId;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "fgxnMeTNUtY",
|
Nid = "fgxnMeTNUtY",
|
||||||
ExportName = "sceKernelGetProcessTimeCounter",
|
ExportName = "sceKernelGetProcessTimeCounter",
|
||||||
|
|||||||
@@ -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 SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Kernel;
|
namespace SharpEmu.Libs.Kernel;
|
||||||
@@ -12,6 +13,9 @@ public static class KernelSemaphoreCompatExports
|
|||||||
private static readonly ConcurrentDictionary<uint, KernelSemaphoreState> _semaphores = new();
|
private static readonly ConcurrentDictionary<uint, KernelSemaphoreState> _semaphores = new();
|
||||||
private static int _nextSemaphoreHandle = 1;
|
private static int _nextSemaphoreHandle = 1;
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static int _semaPollBackoffCount;
|
||||||
|
|
||||||
private sealed class KernelSemaphoreState
|
private sealed class KernelSemaphoreState
|
||||||
{
|
{
|
||||||
public required string Name { get; init; }
|
public required string Name { get; init; }
|
||||||
@@ -28,6 +32,7 @@ public static class KernelSemaphoreCompatExports
|
|||||||
{
|
{
|
||||||
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; }
|
||||||
|
|
||||||
// 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; }
|
||||||
@@ -120,6 +125,7 @@ public static class KernelSemaphoreCompatExports
|
|||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var pollTimedOut = false;
|
||||||
lock (semaphore.Gate)
|
lock (semaphore.Gate)
|
||||||
{
|
{
|
||||||
if (semaphore.Count >= needCount)
|
if (semaphore.Count >= needCount)
|
||||||
@@ -131,36 +137,82 @@ public static class KernelSemaphoreCompatExports
|
|||||||
|
|
||||||
if (timeoutAddress != 0)
|
if (timeoutAddress != 0)
|
||||||
{
|
{
|
||||||
if (!ctx.TryReadUInt32(timeoutAddress, out _))
|
if (!ctx.TryReadUInt32(timeoutAddress, out var timeoutMicros))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
if (timeoutMicros == 0)
|
||||||
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||||
|
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||||
|
pollTimedOut = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromTicks((long)timeoutMicros * 10L));
|
||||||
|
var timedWaiter = new SemaphoreWaiter
|
||||||
|
{
|
||||||
|
NeedCount = needCount,
|
||||||
|
CancelEpochAtBlock = semaphore.CancelEpoch,
|
||||||
|
Timed = true,
|
||||||
|
};
|
||||||
|
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
|
ctx,
|
||||||
|
"sceKernelWaitSema",
|
||||||
|
GetSemaphoreWakeKey(handle),
|
||||||
|
resumeHandler: () => CompleteBlockedTimedSemaWait(ctx, semaphore, timedWaiter, timeoutAddress, deadline),
|
||||||
|
wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, timedWaiter),
|
||||||
|
blockDeadlineTimestamp: deadline))
|
||||||
|
{
|
||||||
|
semaphore.WaitingThreads++;
|
||||||
|
TraceSemaphore($"wait-block-timed handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout_us={timeoutMicros} waiters={semaphore.WaitingThreads}");
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Host-owned threads cannot park in the guest scheduler; degrade to the
|
||||||
|
// immediate-timeout poll the callers already tolerate.
|
||||||
|
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||||
|
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||||
|
pollTimedOut = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var waiter = new SemaphoreWaiter
|
if (!pollTimedOut)
|
||||||
{
|
{
|
||||||
NeedCount = needCount,
|
var waiter = new SemaphoreWaiter
|
||||||
CancelEpochAtBlock = semaphore.CancelEpoch,
|
{
|
||||||
};
|
NeedCount = needCount,
|
||||||
if (!GuestThreadExecution.RequestCurrentThreadBlock(
|
CancelEpochAtBlock = semaphore.CancelEpoch,
|
||||||
ctx,
|
};
|
||||||
"sceKernelWaitSema",
|
if (!GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
GetSemaphoreWakeKey(handle),
|
ctx,
|
||||||
resumeHandler: () => CompleteBlockedSemaWait(semaphore, waiter),
|
"sceKernelWaitSema",
|
||||||
wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, waiter)))
|
GetSemaphoreWakeKey(handle),
|
||||||
{
|
resumeHandler: () => CompleteBlockedSemaWait(semaphore, waiter),
|
||||||
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, waiter)))
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
{
|
||||||
}
|
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||||
|
}
|
||||||
|
|
||||||
semaphore.WaitingThreads++;
|
semaphore.WaitingThreads++;
|
||||||
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GuestThreadExecution.Scheduler?.Pump(ctx, "sceKernelWaitSema");
|
||||||
|
if ((++_semaPollBackoffCount & 255) == 0)
|
||||||
|
{
|
||||||
|
Thread.Sleep(0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Thread.Yield();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -368,6 +420,42 @@ public static class KernelSemaphoreCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int CompleteBlockedTimedSemaWait(
|
||||||
|
CpuContext ctx,
|
||||||
|
KernelSemaphoreState semaphore,
|
||||||
|
SemaphoreWaiter waiter,
|
||||||
|
ulong timeoutAddress,
|
||||||
|
long deadlineTimestamp)
|
||||||
|
{
|
||||||
|
int result;
|
||||||
|
lock (semaphore.Gate)
|
||||||
|
{
|
||||||
|
if (waiter.Result is null && !TryConsumeBlockedSemaWaitLocked(semaphore, waiter))
|
||||||
|
{
|
||||||
|
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||||
|
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||||
|
TraceSemaphore($"wake-timeout name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||||
|
}
|
||||||
|
|
||||||
|
result = waiter.Result!.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
{
|
||||||
|
var remainingTicks = deadlineTimestamp - Stopwatch.GetTimestamp();
|
||||||
|
var remainingMicros = remainingTicks <= 0
|
||||||
|
? 0u
|
||||||
|
: (uint)Math.Min(uint.MaxValue, remainingTicks / (double)Stopwatch.Frequency * 1_000_000d);
|
||||||
|
_ = ctx.TryWriteUInt32(timeoutAddress, remainingMicros);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private static void TraceSemaphore(string message)
|
private static void TraceSemaphore(string message)
|
||||||
{
|
{
|
||||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal))
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal))
|
||||||
|
|||||||
@@ -0,0 +1,558 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Text;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Kernel;
|
||||||
|
|
||||||
|
internal static class KernelSocketCompatExports
|
||||||
|
{
|
||||||
|
private sealed class EmulatedSocketState
|
||||||
|
{
|
||||||
|
public TcpClient? Client;
|
||||||
|
public NetworkStream? Stream;
|
||||||
|
public IPAddress BoundAddress = IPAddress.Any;
|
||||||
|
public int BoundPort;
|
||||||
|
public bool Bound;
|
||||||
|
public bool Connected;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly object Gate = new();
|
||||||
|
private static readonly Dictionary<int, EmulatedSocketState> Sockets = new();
|
||||||
|
|
||||||
|
internal static bool IsEmulatedSocketFd(int fd)
|
||||||
|
{
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
return Sockets.ContainsKey(fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool TryCloseSocketFd(int fd)
|
||||||
|
{
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
if (!Sockets.Remove(fd, out var state))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DisposeEmulatedSocket(state);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool TryReadSocketFd(
|
||||||
|
CpuContext ctx,
|
||||||
|
int fd,
|
||||||
|
ulong bufferAddress,
|
||||||
|
int requested,
|
||||||
|
out ulong bytesRead,
|
||||||
|
out OrbisGen2Result error)
|
||||||
|
{
|
||||||
|
bytesRead = 0;
|
||||||
|
error = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
|
||||||
|
if (!TryGetEmulatedSocketState(fd, out var state) ||
|
||||||
|
state is null ||
|
||||||
|
!state.Connected ||
|
||||||
|
state.Stream is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var socketBuffer = GC.AllocateUninitializedArray<byte>(requested);
|
||||||
|
int socketRead;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
socketRead = state.Stream.Read(socketBuffer, 0, requested);
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (socketRead > 0 && !ctx.Memory.TryWrite(bufferAddress, socketBuffer.AsSpan(0, socketRead)))
|
||||||
|
{
|
||||||
|
error = OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bytesRead = unchecked((ulong)socketRead);
|
||||||
|
error = OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool TryWriteSocketFd(
|
||||||
|
CpuContext ctx,
|
||||||
|
int fd,
|
||||||
|
byte[] payload,
|
||||||
|
out OrbisGen2Result error)
|
||||||
|
{
|
||||||
|
error = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
|
||||||
|
if (!TryGetEmulatedSocketState(fd, out var state) ||
|
||||||
|
state is null ||
|
||||||
|
!state.Connected ||
|
||||||
|
state.Stream is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
state.Stream.Write(payload, 0, payload.Length);
|
||||||
|
state.Stream.Flush();
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NET"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
Console.Out.Write(Encoding.UTF8.GetString(payload));
|
||||||
|
Console.Out.Flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
error = OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "TU-d9PfIHPM",
|
||||||
|
ExportName = "socket",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int Socket(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var fd = KernelMemoryCompatExports.AllocateGuestFileDescriptor();
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
Sockets[fd] = new EmulatedSocketState();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)fd);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "XVL8So3QJUk",
|
||||||
|
ExportName = "connect",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int Connect(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var sockaddrAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var addrlen = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
|
|
||||||
|
if (!TryGetEmulatedSocketState(fd, out _))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryParseGuestSockaddrIn(sockaddrAddress, addrlen, ctx, out var ipAddress, out var port))
|
||||||
|
{
|
||||||
|
LogNet($"connect sockaddr parse failed: fd={fd} addr=0x{sockaddrAddress:X} len={addrlen}");
|
||||||
|
RemoveEmulatedSocketFd(fd);
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
var redirectApplied = TryApplyNetRedirect(ref ipAddress);
|
||||||
|
if (redirectApplied)
|
||||||
|
{
|
||||||
|
LogNet($"connect redirect: fd={fd} ip={ipAddress} port={port}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsGuestTcpOutboundAllowed(ipAddress, redirectApplied))
|
||||||
|
{
|
||||||
|
LogNet($"connect denied by outbound policy: fd={fd} ip={ipAddress} port={port}");
|
||||||
|
RemoveEmulatedSocketFd(fd);
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryEstablishHostTcpConnection(ipAddress, port, out var client, out var stream))
|
||||||
|
{
|
||||||
|
LogNet($"connect failed: fd={fd} ip={ipAddress} port={port}");
|
||||||
|
RemoveEmulatedSocketFd(fd);
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogNet($"connect ok: fd={fd} ip={ipAddress} port={port}");
|
||||||
|
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
if (!Sockets.TryGetValue(fd, out var state) || state is null)
|
||||||
|
{
|
||||||
|
try { stream.Dispose(); } catch (IOException) { }
|
||||||
|
try { client.Dispose(); } catch (IOException) { }
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
DisposeEmulatedSocket(state);
|
||||||
|
state.Client = client;
|
||||||
|
state.Stream = stream;
|
||||||
|
state.Connected = true;
|
||||||
|
state.BoundAddress = ipAddress;
|
||||||
|
state.BoundPort = port;
|
||||||
|
state.Bound = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "KuOmgKoqCdY",
|
||||||
|
ExportName = "bind",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int Bind(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var sockaddrAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var addrlen = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
|
|
||||||
|
if (!TryGetEmulatedSocketState(fd, out var state) || state is null)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryParseGuestSockaddrIn(sockaddrAddress, addrlen, ctx, out var ipAddress, out var port))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.BoundAddress = ipAddress;
|
||||||
|
state.BoundPort = port;
|
||||||
|
state.Bound = true;
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "RenI1lL1WFk",
|
||||||
|
ExportName = "getsockname",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int Getsockname(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var sockaddrAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var addrlenAddress = ctx[CpuRegister.Rdx];
|
||||||
|
|
||||||
|
if (!TryGetEmulatedSocketState(fd, out var state) || state is null || !state.Bound)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> addrlenBuffer = stackalloc byte[4];
|
||||||
|
if (!ctx.Memory.TryRead(addrlenAddress, addrlenBuffer))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var addrlen = BinaryPrimitives.ReadInt32LittleEndian(addrlenBuffer);
|
||||||
|
if (addrlen < 8)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> sockaddr = stackalloc byte[16];
|
||||||
|
sockaddr[0] = 16;
|
||||||
|
sockaddr[1] = 2;
|
||||||
|
BinaryPrimitives.WriteUInt16BigEndian(sockaddr.Slice(2, 2), (ushort)state.BoundPort);
|
||||||
|
var addressBytes = state.BoundAddress.GetAddressBytes();
|
||||||
|
if (addressBytes.Length == 4)
|
||||||
|
{
|
||||||
|
addressBytes.CopyTo(sockaddr.Slice(4, 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
var writeLength = Math.Min(addrlen, 16);
|
||||||
|
if (!ctx.Memory.TryWrite(sockaddrAddress, sockaddr.Slice(0, writeLength)))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(addrlenBuffer, writeLength);
|
||||||
|
if (!ctx.Memory.TryWrite(addrlenAddress, addrlenBuffer))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "9oiX1kyeedA",
|
||||||
|
ExportName = "bzero",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int Bzero(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var address = ctx[CpuRegister.Rdi];
|
||||||
|
var length = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
if (length > 0 && address != 0)
|
||||||
|
{
|
||||||
|
var zeros = new byte[length];
|
||||||
|
if (!ctx.Memory.TryWrite(address, zeros))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "4n51s0zEf0c",
|
||||||
|
ExportName = "inet_pton",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int InetPton(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var af = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var srcAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var dstAddress = ctx[CpuRegister.Rdx];
|
||||||
|
if (af != 2 || srcAddress == 0 || dstAddress == 0)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadCString(srcAddress, ctx, out var text) ||
|
||||||
|
!TryParseIpv4Address(text, out var octets))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> packed = stackalloc byte[4];
|
||||||
|
packed[0] = octets[0];
|
||||||
|
packed[1] = octets[1];
|
||||||
|
packed[2] = octets[2];
|
||||||
|
packed[3] = octets[3];
|
||||||
|
if (!ctx.Memory.TryWrite(dstAddress, packed))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 1;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "jogUIsOV3-U",
|
||||||
|
ExportName = "htons",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int Htons(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var value = unchecked((ushort)ctx[CpuRegister.Rdi]);
|
||||||
|
var swapped = (ushort)(((value & 0x00FF) << 8) | ((value >> 8) & 0x00FF));
|
||||||
|
ctx[CpuRegister.Rax] = swapped;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetEmulatedSocketState(int fd, out EmulatedSocketState? state)
|
||||||
|
{
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
return Sockets.TryGetValue(fd, out state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseGuestSockaddrIn(
|
||||||
|
ulong address,
|
||||||
|
int addrlen,
|
||||||
|
CpuContext ctx,
|
||||||
|
out IPAddress ipAddress,
|
||||||
|
out int port)
|
||||||
|
{
|
||||||
|
ipAddress = IPAddress.None;
|
||||||
|
port = 0;
|
||||||
|
if (address == 0 || addrlen < 8)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> buffer = stackalloc byte[16];
|
||||||
|
var readLength = Math.Min(addrlen, buffer.Length);
|
||||||
|
if (!ctx.Memory.TryRead(address, buffer.Slice(0, readLength)))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buffer[1] != 2)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
port = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(2, 2));
|
||||||
|
ipAddress = new IPAddress(buffer.Slice(4, 4).ToArray());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DisposeEmulatedSocket(EmulatedSocketState state)
|
||||||
|
{
|
||||||
|
try { state.Stream?.Dispose(); } catch (IOException) { }
|
||||||
|
try { state.Client?.Dispose(); } catch (IOException) { }
|
||||||
|
state.Stream = null;
|
||||||
|
state.Client = null;
|
||||||
|
state.Connected = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RemoveEmulatedSocketFd(int fd)
|
||||||
|
{
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
if (Sockets.Remove(fd, out var socketState))
|
||||||
|
{
|
||||||
|
DisposeEmulatedSocket(socketState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void LogNet(string message)
|
||||||
|
{
|
||||||
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NET"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][DEBUG] {message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryApplyNetRedirect(ref IPAddress ipAddress)
|
||||||
|
{
|
||||||
|
var redirect = Environment.GetEnvironmentVariable("SHARPEMU_NET_REDIRECT");
|
||||||
|
if (string.IsNullOrWhiteSpace(redirect))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IPAddress.TryParse(redirect.Trim(), out var redirectAddress))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ipAddress = redirectAddress;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsNetRedirectConfigured()
|
||||||
|
{
|
||||||
|
var redirect = Environment.GetEnvironmentVariable("SHARPEMU_NET_REDIRECT");
|
||||||
|
return !string.IsNullOrWhiteSpace(redirect);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsGuestTcpOutboundAllowed(IPAddress ipAddress, bool redirectApplied)
|
||||||
|
{
|
||||||
|
return redirectApplied || IsNetRedirectConfigured() || IPAddress.IsLoopback(ipAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryEstablishHostTcpConnection(
|
||||||
|
IPAddress ipAddress,
|
||||||
|
int port,
|
||||||
|
out TcpClient client,
|
||||||
|
out NetworkStream stream)
|
||||||
|
{
|
||||||
|
client = null!;
|
||||||
|
stream = null!;
|
||||||
|
if (!TryConnectTcpClient(ipAddress, port, out client))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
stream = client.GetStream();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryConnectTcpClient(IPAddress ipAddress, int port, out TcpClient client)
|
||||||
|
{
|
||||||
|
client = new TcpClient();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var connectTask = client.ConnectAsync(ipAddress, port);
|
||||||
|
if (!connectTask.Wait(TimeSpan.FromMilliseconds(500)))
|
||||||
|
{
|
||||||
|
client.Dispose();
|
||||||
|
client = null!;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (SocketException)
|
||||||
|
{
|
||||||
|
client.Dispose();
|
||||||
|
client = null!;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
client.Dispose();
|
||||||
|
client = null!;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadCString(ulong address, CpuContext ctx, out string text)
|
||||||
|
{
|
||||||
|
const int maxLength = 64;
|
||||||
|
var buffer = new byte[maxLength];
|
||||||
|
var length = 0;
|
||||||
|
for (; length < maxLength; length++)
|
||||||
|
{
|
||||||
|
if (!ctx.Memory.TryRead(address + (ulong)length, buffer.AsSpan(length, 1)))
|
||||||
|
{
|
||||||
|
text = string.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buffer[length] == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
text = Encoding.ASCII.GetString(buffer, 0, length);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseIpv4Address(string text, out byte[] octets)
|
||||||
|
{
|
||||||
|
octets = Array.Empty<byte>();
|
||||||
|
var parts = text.Split('.', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
if (parts.Length != 4)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsed = new byte[4];
|
||||||
|
for (var i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
if (!byte.TryParse(parts[i], out parsed[i]))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
octets = parsed;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -338,6 +338,359 @@ public static class LibcStdioExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "8Q60JLJ6Rv4",
|
||||||
|
ExportName = "getc",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Getc(CpuContext ctx) => FgetcCore(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "AEuF3F2f8TA",
|
||||||
|
ExportName = "fgetc",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Fgetc(CpuContext ctx) => FgetcCore(ctx);
|
||||||
|
|
||||||
|
private static int FgetcCore(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
|
if (!_fileHandles.TryGetValue(handle, out var stream))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var value = stream.ReadByte();
|
||||||
|
ctx[CpuRegister.Rax] = value < 0 ? unchecked((ulong)(-1L)) : (ulong)value;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "LxcEU+ICu8U",
|
||||||
|
ExportName = "feof",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Feof(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
|
var atEnd = _fileHandles.TryGetValue(handle, out var stream) &&
|
||||||
|
stream.CanRead && stream.Position >= stream.Length;
|
||||||
|
ctx[CpuRegister.Rax] = atEnd ? 1UL : 0UL;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "AHxyhN96dy4",
|
||||||
|
ExportName = "ferror",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Ferror(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "3QIPIh-GDjw",
|
||||||
|
ExportName = "rewind",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Rewind(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
|
if (_fileHandles.TryGetValue(handle, out var stream))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
stream.Seek(0, SeekOrigin.Begin);
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "aZK8lNei-Qw",
|
||||||
|
ExportName = "fputc",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Fputc(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var character = unchecked((byte)ctx[CpuRegister.Rdi]);
|
||||||
|
var handle = ctx[CpuRegister.Rsi];
|
||||||
|
|
||||||
|
if (_fileHandles.TryGetValue(handle, out var stream))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
stream.WriteByte(character);
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Unknown streams are the bundled libc's real stdout/stderr FILE objects;
|
||||||
|
// mirror the fputs HLE and forward them to the host console.
|
||||||
|
Console.Out.Write((char)character);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = character;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "MpxhMh8QFro",
|
||||||
|
ExportName = "fwrite",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Fwrite(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var source = ctx[CpuRegister.Rdi];
|
||||||
|
var elementSize = ctx[CpuRegister.Rsi];
|
||||||
|
var elementCount = ctx[CpuRegister.Rdx];
|
||||||
|
var handle = ctx[CpuRegister.Rcx];
|
||||||
|
|
||||||
|
if (elementSize == 0 || elementCount == 0)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
var knownHandle = _fileHandles.TryGetValue(handle, out var stream);
|
||||||
|
if (source == 0)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalRequested = elementSize * elementCount;
|
||||||
|
var buffer = ArrayPool<byte>.Shared.Rent((int)Math.Min((ulong)ReadChunkSize, totalRequested));
|
||||||
|
ulong totalWritten = 0;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (totalWritten < totalRequested)
|
||||||
|
{
|
||||||
|
var request = (int)Math.Min((ulong)buffer.Length, totalRequested - totalWritten);
|
||||||
|
if (!ctx.Memory.TryRead(source + totalWritten, buffer.AsSpan(0, request)))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = totalWritten / elementSize;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (knownHandle)
|
||||||
|
{
|
||||||
|
stream!.Write(buffer, 0, request);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.Out.Write(System.Text.Encoding.UTF8.GetString(buffer, 0, request));
|
||||||
|
}
|
||||||
|
|
||||||
|
totalWritten += (ulong)request;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = totalWritten / elementSize;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = totalWritten / elementSize;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "MUjC4lbHrK4",
|
||||||
|
ExportName = "fflush",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Fflush(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (handle == 0)
|
||||||
|
{
|
||||||
|
foreach (var stream in _fileHandles.Values)
|
||||||
|
{
|
||||||
|
stream.Flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (_fileHandles.TryGetValue(handle, out var stream))
|
||||||
|
{
|
||||||
|
stream.Flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "fffwELXNVFA",
|
||||||
|
ExportName = "fprintf",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Fprintf(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
|
var formatAddress = ctx[CpuRegister.Rsi];
|
||||||
|
|
||||||
|
if (!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, formatAddress, MaxPathLength, out var format))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rendered = KernelMemoryCompatExports.FormatStringFromVarArgs(ctx, format, firstGpArgIndex: 2);
|
||||||
|
return WriteRenderedText(ctx, handle, rendered);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "pDBDcY6uLSA",
|
||||||
|
ExportName = "vfprintf",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Vfprintf(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
|
var formatAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var vaListAddress = ctx[CpuRegister.Rdx];
|
||||||
|
|
||||||
|
if (!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, formatAddress, MaxPathLength, out var format))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
KernelMemoryCompatExports.TryFormatStringFromVaList(ctx, format, vaListAddress, out var rendered);
|
||||||
|
return WriteRenderedText(ctx, handle, rendered);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int WriteRenderedText(CpuContext ctx, ulong handle, string rendered)
|
||||||
|
{
|
||||||
|
var payload = System.Text.Encoding.UTF8.GetBytes(rendered);
|
||||||
|
|
||||||
|
if (_fileHandles.TryGetValue(handle, out var stream))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
stream.Write(payload, 0, payload.Length);
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.Out.Write(rendered);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = (ulong)payload.Length;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "gkWgn0p1AfU",
|
||||||
|
ExportName = "freopen",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Freopen(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var pathAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var modeAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var handle = ctx[CpuRegister.Rdx];
|
||||||
|
|
||||||
|
if (pathAddress == 0 || modeAddress == 0 || handle == 0 ||
|
||||||
|
!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, pathAddress, MaxPathLength, out var guestPath) ||
|
||||||
|
!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, modeAddress, MaxModeLength, out var mode) ||
|
||||||
|
!TryParseFopenMode(mode, out var fileMode, out var fileAccess))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_fileHandles.TryRemove(handle, out var previousStream))
|
||||||
|
{
|
||||||
|
previousStream.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
var hostPath = KernelMemoryCompatExports.ResolveGuestPath(guestPath);
|
||||||
|
if (fileAccess != FileAccess.Read && KernelMemoryCompatExports.IsReadOnlyGuestMutationPath(guestPath))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (fileAccess != FileAccess.Read)
|
||||||
|
{
|
||||||
|
var parentDirectory = Path.GetDirectoryName(hostPath);
|
||||||
|
if (!string.IsNullOrWhiteSpace(parentDirectory))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(parentDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var stream = new FileStream(hostPath, fileMode, fileAccess, FileShare.ReadWrite);
|
||||||
|
// freopen keeps the caller's FILE* identity, so rebind the same handle value.
|
||||||
|
_fileHandles[handle] = stream;
|
||||||
|
|
||||||
|
if (_traceStdio)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] freopen: guest='{guestPath}' host='{hostPath}' mode='{mode}' -> OK handle=0x{handle:X}");
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = handle;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
if (_traceStdio)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] freopen: guest='{guestPath}' host='{hostPath}' mode='{mode}' -> FAILED {ex.GetType().Name}: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return ex is UnauthorizedAccessException
|
||||||
|
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "sUP1hBaouOw",
|
Nid = "sUP1hBaouOw",
|
||||||
ExportName = "_Getpctype",
|
ExportName = "_Getpctype",
|
||||||
|
|||||||
@@ -95,6 +95,54 @@ public static class NetExports
|
|||||||
return ctx.SetReturn(0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "9T2pDF2Ryqg",
|
||||||
|
ExportName = "sceNetHtonl",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNet")]
|
||||||
|
public static int NetHtonl(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var value = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
|
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "iWQWrwiSt8A",
|
||||||
|
ExportName = "sceNetHtons",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNet")]
|
||||||
|
public static int NetHtons(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var value = unchecked((ushort)ctx[CpuRegister.Rdi]);
|
||||||
|
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "pQGpHYopAIY",
|
||||||
|
ExportName = "sceNetNtohl",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNet")]
|
||||||
|
public static int NetNtohl(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var value = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
|
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Rbvt+5Y2iEw",
|
||||||
|
ExportName = "sceNetNtohs",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNet")]
|
||||||
|
public static int NetNtohs(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var value = unchecked((ushort)ctx[CpuRegister.Rdi]);
|
||||||
|
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "C4UgDHHPvdw",
|
Nid = "C4UgDHHPvdw",
|
||||||
ExportName = "sceNetResolverCreate",
|
ExportName = "sceNetResolverCreate",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -22,6 +22,17 @@ public static class NpManagerExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "S7QTn72PrDw",
|
||||||
|
ExportName = "sceNpDeleteRequest",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpManager")]
|
||||||
|
public static int NpDeleteRequest(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "JELHf4xPufo",
|
Nid = "JELHf4xPufo",
|
||||||
ExportName = "sceNpCheckCallbackForLib",
|
ExportName = "sceNpCheckCallbackForLib",
|
||||||
@@ -42,8 +53,19 @@ public static class NpManagerExports
|
|||||||
LibraryName = "libSceNpManager")]
|
LibraryName = "libSceNpManager")]
|
||||||
public static int NpGetOnlineId(CpuContext ctx)
|
public static int NpGetOnlineId(CpuContext ctx)
|
||||||
{
|
{
|
||||||
ctx[CpuRegister.Rax] = 0;
|
// Gen5 ABI: user ID, then output structure.
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return WriteOfflineOnlineId(ctx, ctx[CpuRegister.Rsi]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "rbknaUjpqWo",
|
||||||
|
ExportName = "sceNpGetAccountIdA",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpManager")]
|
||||||
|
public static int NpGetOnlineIdA(CpuContext ctx)
|
||||||
|
{
|
||||||
|
// The A variant takes a user ID before OnlineId.
|
||||||
|
return WriteOfflineOnlineId(ctx, ctx[CpuRegister.Rsi]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -158,4 +180,19 @@ public static class NpManagerExports
|
|||||||
|
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] np.{message}");
|
Console.Error.WriteLine($"[LOADER][TRACE] np.{message}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int WriteOfflineOnlineId(CpuContext ctx, ulong address)
|
||||||
|
{
|
||||||
|
if (address == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// SceNpOnlineId is a 16-byte handle plus four trailing bytes.
|
||||||
|
Span<byte> onlineId = stackalloc byte[20];
|
||||||
|
"Player"u8.CopyTo(onlineId);
|
||||||
|
return ctx.Memory.TryWrite(address, onlineId)
|
||||||
|
? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -31,6 +31,18 @@ public static class NpWebApi2Exports
|
|||||||
return ctx.SetReturn(0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "WV1GwM32NgY",
|
||||||
|
ExportName = "sceNpWebApi2PushEventCreateHandle",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpWebApi2")]
|
||||||
|
public static int NpWebApi2InitializeAlt(CpuContext ctx)
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref _initialized, 1);
|
||||||
|
TraceNpWebApi2("init-alt", unchecked((int)ctx[CpuRegister.Rdi]), ctx[CpuRegister.Rsi]);
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "bEvXpcEk200",
|
Nid = "bEvXpcEk200",
|
||||||
ExportName = "sceNpWebApi2Terminate",
|
ExportName = "sceNpWebApi2Terminate",
|
||||||
|
|||||||
@@ -14,13 +14,21 @@ public static class PadExports
|
|||||||
private const int OrbisPadErrorNotInitialized = unchecked((int)0x80920005);
|
private const int OrbisPadErrorNotInitialized = unchecked((int)0x80920005);
|
||||||
private const int OrbisPadErrorDeviceNotConnected = unchecked((int)0x80920007);
|
private const int OrbisPadErrorDeviceNotConnected = unchecked((int)0x80920007);
|
||||||
private const int OrbisPadErrorDeviceNoHandle = unchecked((int)0x80920008);
|
private const int OrbisPadErrorDeviceNoHandle = unchecked((int)0x80920008);
|
||||||
private const int PrimaryUserId = 1;
|
private const int PrimaryUserId = 1000;
|
||||||
private const int StandardPortType = 0;
|
private const int StandardPortType = 0;
|
||||||
private const int PrimaryPadHandle = 1;
|
private const int PrimaryPadHandle = 1;
|
||||||
private const int ControllerInformationSize = 0x1C;
|
private const int ControllerInformationSize = 0x1C;
|
||||||
private const int PadDataSize = 0x78;
|
private const int PadDataSize = 0x78;
|
||||||
|
private static readonly long InputSampleIntervalTicks = Math.Max(1, Stopwatch.Frequency / 1000);
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static long _lastInputSampleTicks;
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static PadState _cachedInputState;
|
||||||
|
|
||||||
private static bool _initialized;
|
private static bool _initialized;
|
||||||
|
private static int _controlsAnnouncementLogged;
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "hv1luiJrqQM",
|
Nid = "hv1luiJrqQM",
|
||||||
@@ -63,11 +71,15 @@ public static class PadExports
|
|||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,6 +190,50 @@ public static class PadExports
|
|||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "2JgFB2n9oUM",
|
||||||
|
ExportName = "scePadSetTriggerEffect",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePad")]
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
@@ -260,6 +316,44 @@ public static class PadExports
|
|||||||
{
|
{
|
||||||
Span<byte> data = stackalloc byte[PadDataSize];
|
Span<byte> data = stackalloc byte[PadDataSize];
|
||||||
data.Clear();
|
data.Clear();
|
||||||
|
var input = ReadHostInputState();
|
||||||
|
var buttons = input.Buttons;
|
||||||
|
var leftX = input.LeftX;
|
||||||
|
var leftY = input.LeftY;
|
||||||
|
var rightX = input.RightX;
|
||||||
|
var rightY = input.RightY;
|
||||||
|
var l2 = input.L2;
|
||||||
|
var r2 = input.R2;
|
||||||
|
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(data[0x00..], buttons);
|
||||||
|
data[0x04] = leftX;
|
||||||
|
data[0x05] = leftY;
|
||||||
|
data[0x06] = rightX;
|
||||||
|
data[0x07] = rightY;
|
||||||
|
data[0x08] = l2;
|
||||||
|
data[0x09] = r2;
|
||||||
|
BinaryPrimitives.WriteSingleLittleEndian(data[0x18..], 1.0f);
|
||||||
|
data[0x4C] = 1;
|
||||||
|
var timestampTicks = Stopwatch.GetTimestamp();
|
||||||
|
var timestampMicroseconds =
|
||||||
|
((ulong)(timestampTicks / Stopwatch.Frequency) * 1_000_000UL) +
|
||||||
|
((ulong)(timestampTicks % Stopwatch.Frequency) * 1_000_000UL / (ulong)Stopwatch.Frequency);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(
|
||||||
|
data[0x50..],
|
||||||
|
timestampMicroseconds);
|
||||||
|
data[0x68] = 1;
|
||||||
|
|
||||||
|
return ctx.Memory.TryWrite(dataAddress, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PadState ReadHostInputState()
|
||||||
|
{
|
||||||
|
var now = Stopwatch.GetTimestamp();
|
||||||
|
if (_lastInputSampleTicks != 0 && now - _lastInputSampleTicks < InputSampleIntervalTicks)
|
||||||
|
{
|
||||||
|
return _cachedInputState;
|
||||||
|
}
|
||||||
|
|
||||||
var acceptsKeyboardInput = IsEmulatorWindowFocused();
|
var acceptsKeyboardInput = IsEmulatorWindowFocused();
|
||||||
var buttons = acceptsKeyboardInput ? ReadKeyboardButtons() : 0;
|
var buttons = acceptsKeyboardInput ? ReadKeyboardButtons() : 0;
|
||||||
var leftX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x41), IsKeyDown(0x44)) : (byte)128;
|
var leftX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x41), IsKeyDown(0x44)) : (byte)128;
|
||||||
@@ -293,25 +387,17 @@ public static class PadExports
|
|||||||
r2 = Math.Max(r2, xpad.R2);
|
r2 = Math.Max(r2, xpad.R2);
|
||||||
}
|
}
|
||||||
|
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(data[0x00..], buttons);
|
_cachedInputState = new PadState(
|
||||||
data[0x04] = leftX;
|
Connected: true,
|
||||||
data[0x05] = leftY;
|
Buttons: buttons,
|
||||||
data[0x06] = rightX;
|
LeftX: leftX,
|
||||||
data[0x07] = rightY;
|
LeftY: leftY,
|
||||||
data[0x08] = l2;
|
RightX: rightX,
|
||||||
data[0x09] = r2;
|
RightY: rightY,
|
||||||
BinaryPrimitives.WriteSingleLittleEndian(data[0x18..], 1.0f);
|
L2: l2,
|
||||||
data[0x4C] = 1;
|
R2: r2);
|
||||||
var timestampTicks = Stopwatch.GetTimestamp();
|
_lastInputSampleTicks = now;
|
||||||
var timestampMicroseconds =
|
return _cachedInputState;
|
||||||
((ulong)(timestampTicks / Stopwatch.Frequency) * 1_000_000UL) +
|
|
||||||
((ulong)(timestampTicks % Stopwatch.Frequency) * 1_000_000UL / (ulong)Stopwatch.Frequency);
|
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(
|
|
||||||
data[0x50..],
|
|
||||||
timestampMicroseconds);
|
|
||||||
data[0x68] = 1;
|
|
||||||
|
|
||||||
return ctx.Memory.TryWrite(dataAddress, data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[DllImport("user32.dll")]
|
[DllImport("user32.dll")]
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -682,7 +682,9 @@ public static class PlayGoExports
|
|||||||
var hasMetadata = File.Exists(playGoDat) || File.Exists(scenarioJson) || File.Exists(chunkDefsXml);
|
var hasMetadata = File.Exists(playGoDat) || File.Exists(scenarioJson) || File.Exists(chunkDefsXml);
|
||||||
if (!hasMetadata)
|
if (!hasMetadata)
|
||||||
{
|
{
|
||||||
return PlayGoMetadata.Empty;
|
// Full installs may omit PlayGo sidecar metadata.
|
||||||
|
TracePlayGo("metadata_missing; using fully-installed default chunk");
|
||||||
|
return new PlayGoMetadata(true, [(ushort)0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
var chunkIds = LoadChunkIds(chunkDefsXml);
|
var chunkIds = LoadChunkIds(chunkDefsXml);
|
||||||
|
|||||||
@@ -9,6 +9,189 @@ namespace SharpEmu.Libs.Rtc;
|
|||||||
public static class RtcExports
|
public static class RtcExports
|
||||||
{
|
{
|
||||||
private const long DateTimeTicksPerMicrosecond = 10;
|
private const long DateTimeTicksPerMicrosecond = 10;
|
||||||
|
private const ulong MicrosecondsPerSecond = 1_000_000UL;
|
||||||
|
private const ulong MicrosecondsPerMinute = 60_000_000UL;
|
||||||
|
private const ulong MicrosecondsPerHour = 3_600_000_000UL;
|
||||||
|
private const ulong MicrosecondsPerDay = 86_400_000_000UL;
|
||||||
|
private const ulong MicrosecondsPerWeek = 604_800_000_000UL;
|
||||||
|
private const ulong UnixEpochTicks = 62_135_596_800_000_000UL;
|
||||||
|
private const ulong Win32FileTimeEpochTicks = 50_491_123_200_000_000UL;
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "lPEBYdVX0XQ",
|
||||||
|
ExportName = "sceRtcCheckValid",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcCheckValid(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var timeAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (timeAddress == 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50002);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadRtcDateTime(ctx, timeAddress, out var time))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ValidateRtcDateTime(time);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "fNaZ4DbzHAE",
|
||||||
|
ExportName = "sceRtcCompareTick",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcCompareTick(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var tick1Address = ctx[CpuRegister.Rdi];
|
||||||
|
var tick2Address = ctx[CpuRegister.Rsi];
|
||||||
|
if (tick1Address == 0 || tick2Address == 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50002);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryReadUInt64(tick1Address, out var tick1) || !ctx.TryReadUInt64(tick2Address, out var tick2))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tick1 < tick2 ? -1 : tick1 > tick2 ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "8Yr143yEnRo",
|
||||||
|
ExportName = "sceRtcConvertLocalTimeToUtc",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcConvertLocalTimeToUtc(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var tickLocalAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var tickUtcAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (tickLocalAddress == 0 || tickUtcAddress == 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50002);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryReadUInt64(tickLocalAddress, out var tickLocal))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryConvertTickToDateTime(tickLocal, out var localDateTime))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime utcDateTime;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
utcDateTime = TimeZoneInfo.ConvertTimeToUtc(localDateTime, TimeZoneInfo.Local);
|
||||||
|
}
|
||||||
|
catch (ArgumentException)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryWriteTickFromDateTime(ctx, tickUtcAddress, utcDateTime))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "M1TvFst-jrM",
|
||||||
|
ExportName = "sceRtcConvertUtcToLocalTime",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcConvertUtcToLocalTime(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var tickUtcAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var tickLocalAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (tickUtcAddress == 0 || tickLocalAddress == 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50002);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryReadUInt64(tickUtcAddress, out var tickUtc))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryConvertTickToDateTime(tickUtc, out var utcDateTime))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime localDateTime;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
localDateTime = TimeZoneInfo.ConvertTimeFromUtc(utcDateTime, TimeZoneInfo.Local);
|
||||||
|
}
|
||||||
|
catch (ArgumentException)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryWriteTickFromDateTime(ctx, tickLocalAddress, localDateTime))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "8SljQx6pDP8",
|
||||||
|
ExportName = "sceRtcEnd",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcEnd(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "LN3Zcb72Q0c",
|
||||||
|
ExportName = "sceRtcGetCurrentAdNetworkTick",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcGetCurrentAdNetworkTick(CpuContext ctx) => RtcGetCurrentTick(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "8lfvnRMqwEM",
|
||||||
|
ExportName = "sceRtcGetCurrentClock",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcGetCurrentClock(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var timeAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (timeAddress == 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50002);
|
||||||
|
}
|
||||||
|
|
||||||
|
var timeZoneMinutes = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
DateTimeOffset currentTime;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
currentTime = DateTimeOffset.UtcNow.ToOffset(TimeSpan.FromMinutes(timeZoneMinutes));
|
||||||
|
}
|
||||||
|
catch (ArgumentOutOfRangeException)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryWriteRtcDateTime(ctx, timeAddress, ToRtcDateTime(currentTime.DateTime)))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "ZPD1YOKI+Kw",
|
Nid = "ZPD1YOKI+Kw",
|
||||||
@@ -20,30 +203,38 @@ public static class RtcExports
|
|||||||
var timeAddress = ctx[CpuRegister.Rdi];
|
var timeAddress = ctx[CpuRegister.Rdi];
|
||||||
if (timeAddress == 0)
|
if (timeAddress == 0)
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return unchecked((int)0x80B50002);
|
||||||
}
|
}
|
||||||
|
|
||||||
var now = DateTimeOffset.Now;
|
if (!TryWriteRtcDateTime(ctx, timeAddress, ToRtcDateTime(DateTimeOffset.Now.DateTime)))
|
||||||
Span<byte> rtcDateTime = stackalloc byte[16];
|
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[0..2], checked((ushort)now.Year));
|
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[2..4], checked((ushort)now.Month));
|
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[4..6], checked((ushort)now.Day));
|
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[6..8], checked((ushort)now.Hour));
|
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[8..10], checked((ushort)now.Minute));
|
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[10..12], checked((ushort)now.Second));
|
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(
|
|
||||||
rtcDateTime[12..16],
|
|
||||||
checked((uint)((now.Ticks % TimeSpan.TicksPerSecond) / 10)));
|
|
||||||
|
|
||||||
if (!ctx.Memory.TryWrite(timeAddress, rtcDateTime))
|
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Ot1DE3gif84",
|
||||||
|
ExportName = "sceRtcGetCurrentDebugNetworkTick",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcGetCurrentDebugNetworkTick(CpuContext ctx) => RtcGetCurrentTick(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "zO9UL3qIINQ",
|
||||||
|
ExportName = "sceRtcGetCurrentNetworkTick",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcGetCurrentNetworkTick(CpuContext ctx) => RtcGetCurrentTick(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "HWxHOdbM-Pg",
|
||||||
|
ExportName = "sceRtcGetCurrentRawNetworkTick",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcGetCurrentRawNetworkTick(CpuContext ctx) => RtcGetCurrentTick(ctx);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "18B2NS1y9UU",
|
Nid = "18B2NS1y9UU",
|
||||||
ExportName = "sceRtcGetCurrentTick",
|
ExportName = "sceRtcGetCurrentTick",
|
||||||
@@ -54,7 +245,7 @@ public static class RtcExports
|
|||||||
var tickAddress = ctx[CpuRegister.Rdi];
|
var tickAddress = ctx[CpuRegister.Rdi];
|
||||||
if (tickAddress == 0)
|
if (tickAddress == 0)
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return unchecked((int)0x80B50002);
|
||||||
}
|
}
|
||||||
|
|
||||||
var tickValue = unchecked((ulong)(DateTime.UtcNow.Ticks / DateTimeTicksPerMicrosecond));
|
var tickValue = unchecked((ulong)(DateTime.UtcNow.Ticks / DateTimeTicksPerMicrosecond));
|
||||||
@@ -63,7 +254,87 @@ public static class RtcExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "CyIK-i4XdgQ",
|
||||||
|
ExportName = "sceRtcGetDayOfWeek",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcGetDayOfWeek(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var year = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var month = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
var day = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
|
if (!IsValidCalendarDate(year, month, day))
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50004);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)new DateTime(year, month, day).DayOfWeek;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "3O7Ln8AqJ1o",
|
||||||
|
ExportName = "sceRtcGetDaysInMonth",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcGetDaysInMonth(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var year = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var month = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
if (year < 1 || year > 9999)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50008);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (month < 1 || month > 12)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50009);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DateTime.DaysInMonth(year, month);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "E7AR4o7Ny7E",
|
||||||
|
ExportName = "sceRtcGetDosTime",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcGetDosTime(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var timeAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var dosTimeAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (timeAddress == 0 || dosTimeAddress == 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50002);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadRtcDateTime(ctx, timeAddress, out var time))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var validationResult = ValidateRtcDateTime(time);
|
||||||
|
if (validationResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
{
|
||||||
|
return validationResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint dosTime = 0;
|
||||||
|
dosTime |= (uint)((time.Second / 2) & 0x1F);
|
||||||
|
dosTime |= (uint)(time.Minute & 0x3F) << 5;
|
||||||
|
dosTime |= (uint)(time.Hour & 0x1F) << 11;
|
||||||
|
dosTime |= (uint)(time.Day & 0x1F) << 16;
|
||||||
|
dosTime |= (uint)(time.Month & 0x0F) << 21;
|
||||||
|
dosTime |= (uint)((time.Year - 1980) & 0x7F) << 25;
|
||||||
|
|
||||||
|
if (!ctx.TryWriteUInt32(dosTimeAddress, dosTime))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,45 +345,220 @@ public static class RtcExports
|
|||||||
LibraryName = "libSceRtc")]
|
LibraryName = "libSceRtc")]
|
||||||
public static int RtcGetTick(CpuContext ctx)
|
public static int RtcGetTick(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var dateTimeAddress = ctx[CpuRegister.Rdi];
|
var timeAddress = ctx[CpuRegister.Rdi];
|
||||||
var tickAddress = ctx[CpuRegister.Rsi];
|
var tickAddress = ctx[CpuRegister.Rsi];
|
||||||
if (dateTimeAddress == 0 || tickAddress == 0)
|
if (timeAddress == 0 || tickAddress == 0)
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return unchecked((int)0x80B50002);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadRtcDateTime(ctx, dateTimeAddress, out var rtcDateTime))
|
if (!TryReadRtcDateTime(ctx, timeAddress, out var time))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong tickValue;
|
var validationResult = ValidateRtcDateTime(time);
|
||||||
try
|
if (validationResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
{
|
{
|
||||||
var baseDateTime = new DateTime(
|
return validationResult;
|
||||||
rtcDateTime.Year,
|
|
||||||
rtcDateTime.Month,
|
|
||||||
rtcDateTime.Day,
|
|
||||||
rtcDateTime.Hour,
|
|
||||||
rtcDateTime.Minute,
|
|
||||||
rtcDateTime.Second,
|
|
||||||
DateTimeKind.Utc);
|
|
||||||
tickValue = checked((ulong)((baseDateTime.Ticks / DateTimeTicksPerMicrosecond) + rtcDateTime.Microsecond));
|
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (ex is ArgumentOutOfRangeException or OverflowException)
|
|
||||||
|
if (!TryConvertRtcDateTimeToTick(time, out var tick))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt64(tickAddress, tickValue))
|
if (!ctx.TryWriteUInt64(tickAddress, tick))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "jMNwqYr4R-k",
|
||||||
|
ExportName = "sceRtcGetTickResolution",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcGetTickResolution(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return (int)MicrosecondsPerSecond;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "BtqmpTRXHgk",
|
||||||
|
ExportName = "sceRtcGetTime_t",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcGetTimeT(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var timeAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var timeTAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (timeAddress == 0 || timeTAddress == 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50002);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadRtcDateTime(ctx, timeAddress, out var time))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var validationResult = ValidateRtcDateTime(time);
|
||||||
|
if (validationResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
{
|
||||||
|
return validationResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryConvertRtcDateTimeToTick(time, out var tick))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var unixSeconds = tick < UnixEpochTicks ? 0UL : (tick - UnixEpochTicks) / MicrosecondsPerSecond;
|
||||||
|
if (!ctx.TryWriteUInt64(timeTAddress, unixSeconds))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "jfRO0uTjtzA",
|
||||||
|
ExportName = "sceRtcGetWin32FileTime",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcGetWin32FileTime(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var timeAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var fileTimeAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (timeAddress == 0 || fileTimeAddress == 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50002);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadRtcDateTime(ctx, timeAddress, out var time))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var validationResult = ValidateRtcDateTime(time);
|
||||||
|
if (validationResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
{
|
||||||
|
return validationResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryConvertRtcDateTimeToTick(time, out var tick))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var win32Time = tick < Win32FileTimeEpochTicks ? 0UL : (tick - Win32FileTimeEpochTicks) * 10UL;
|
||||||
|
if (!ctx.TryWriteUInt64(fileTimeAddress, win32Time))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "LlodCMDbk3o",
|
||||||
|
ExportName = "sceRtcInit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcInit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Ug8pCwQvh0c",
|
||||||
|
ExportName = "sceRtcIsLeapYear",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcIsLeapYear(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var year = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
if (year < 1 || year > 9999)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50008);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DateTime.IsLeapYear(year) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "NR1J0N7L2xY",
|
||||||
|
ExportName = "sceRtcTickAddDays",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcTickAddDays(CpuContext ctx) => AddTickDelta(ctx, MicrosecondsPerDay);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "MDc5cd8HfCA",
|
||||||
|
ExportName = "sceRtcTickAddHours",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcTickAddHours(CpuContext ctx) => AddTickDelta(ctx, MicrosecondsPerHour);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "XPIiw58C+GM",
|
||||||
|
ExportName = "sceRtcTickAddMicroseconds",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcTickAddMicroseconds(CpuContext ctx) => AddTickDelta(ctx, 1UL);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "mn-tf4QiFzk",
|
||||||
|
ExportName = "sceRtcTickAddMinutes",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcTickAddMinutes(CpuContext ctx) => AddTickDelta(ctx, MicrosecondsPerMinute);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "CL6y9q-XbuQ",
|
||||||
|
ExportName = "sceRtcTickAddMonths",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcTickAddMonths(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return AddCalendarDelta(ctx, dateTime => dateTime.AddMonths(unchecked((int)ctx[CpuRegister.Rdx])));
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "07O525HgICs",
|
||||||
|
ExportName = "sceRtcTickAddSeconds",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcTickAddSeconds(CpuContext ctx) => AddTickDelta(ctx, MicrosecondsPerSecond);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "AqVMssr52Rc",
|
||||||
|
ExportName = "sceRtcTickAddTicks",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcTickAddTicks(CpuContext ctx) => AddTickDelta(ctx, 1UL);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "gI4t194c2W8",
|
||||||
|
ExportName = "sceRtcTickAddWeeks",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcTickAddWeeks(CpuContext ctx) => AddTickDelta(ctx, MicrosecondsPerWeek);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "-5y2uJ62qS8",
|
||||||
|
ExportName = "sceRtcTickAddYears",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcTickAddYears(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return AddCalendarDelta(ctx, dateTime => dateTime.AddYears(unchecked((int)ctx[CpuRegister.Rdx])));
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "ueega6v3GUw",
|
Nid = "ueega6v3GUw",
|
||||||
ExportName = "sceRtcSetTick",
|
ExportName = "sceRtcSetTick",
|
||||||
@@ -120,11 +566,11 @@ public static class RtcExports
|
|||||||
LibraryName = "libSceRtc")]
|
LibraryName = "libSceRtc")]
|
||||||
public static int RtcSetTick(CpuContext ctx)
|
public static int RtcSetTick(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var dateTimeAddress = ctx[CpuRegister.Rdi];
|
var timeAddress = ctx[CpuRegister.Rdi];
|
||||||
var tickAddress = ctx[CpuRegister.Rsi];
|
var tickAddress = ctx[CpuRegister.Rsi];
|
||||||
if (dateTimeAddress == 0 || tickAddress == 0)
|
if (timeAddress == 0 || tickAddress == 0)
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return unchecked((int)0x80B50002);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadUInt64(tickAddress, out var tickValue))
|
if (!ctx.TryReadUInt64(tickAddress, out var tickValue))
|
||||||
@@ -132,40 +578,249 @@ public static class RtcExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tickValue > long.MaxValue / DateTimeTicksPerMicrosecond)
|
if (!TryConvertTickToRtcDateTime(tickValue, out var time))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return unchecked((int)0x80B50004);
|
||||||
}
|
}
|
||||||
|
|
||||||
DateTime dateTime;
|
if (!TryWriteRtcDateTime(ctx, timeAddress, time))
|
||||||
try
|
|
||||||
{
|
|
||||||
dateTime = new DateTime(checked((long)tickValue * DateTimeTicksPerMicrosecond), DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
catch (ArgumentOutOfRangeException)
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryWriteRtcDateTime(ctx, dateTimeAddress, dateTime))
|
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryReadRtcDateTime(CpuContext ctx, ulong address, out RtcDateTime rtcDateTime)
|
[SysAbiExport(
|
||||||
|
Nid = "aYPCd1cChyg",
|
||||||
|
ExportName = "sceRtcSetDosTime",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcSetDosTime(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var timeAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (timeAddress == 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50002);
|
||||||
|
}
|
||||||
|
|
||||||
|
var dosTime = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||||
|
var time = new RtcDateTime(
|
||||||
|
(ushort)(1980 + ((dosTime >> 25) & 0x7F)),
|
||||||
|
(ushort)((dosTime >> 21) & 0x0F),
|
||||||
|
(ushort)((dosTime >> 16) & 0x1F),
|
||||||
|
(ushort)((dosTime >> 11) & 0x1F),
|
||||||
|
(ushort)((dosTime >> 5) & 0x3F),
|
||||||
|
(ushort)((dosTime << 1) & 0x3E),
|
||||||
|
0);
|
||||||
|
|
||||||
|
if (!TryWriteRtcDateTime(ctx, timeAddress, time))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "bDEVVP4bTjQ",
|
||||||
|
ExportName = "sceRtcSetTime_t",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcSetTimeT(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var timeAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (timeAddress == 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50002);
|
||||||
|
}
|
||||||
|
|
||||||
|
var timeSeconds = unchecked((long)ctx[CpuRegister.Rsi]);
|
||||||
|
if (timeSeconds < 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50003);
|
||||||
|
}
|
||||||
|
|
||||||
|
var tick = UnixEpochTicks + unchecked((ulong)timeSeconds) * MicrosecondsPerSecond;
|
||||||
|
if (!TryConvertTickToRtcDateTime(tick, out var time))
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50003);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryWriteRtcDateTime(ctx, timeAddress, time))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "n5JiAJXsbcs",
|
||||||
|
ExportName = "sceRtcSetWin32FileTime",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcSetWin32FileTime(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var timeAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (timeAddress == 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50002);
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileTime = unchecked((long)ctx[CpuRegister.Rsi]);
|
||||||
|
if (fileTime < 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50003);
|
||||||
|
}
|
||||||
|
|
||||||
|
var tick = Win32FileTimeEpochTicks + unchecked((ulong)fileTime / 10UL);
|
||||||
|
if (!TryConvertTickToRtcDateTime(tick, out var time))
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50003);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryWriteRtcDateTime(ctx, timeAddress, time))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int AddTickDelta(CpuContext ctx, ulong microsecondsPerUnit)
|
||||||
|
{
|
||||||
|
var destinationAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var sourceAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var delta = unchecked((long)ctx[CpuRegister.Rdx]);
|
||||||
|
if (destinationAddress == 0 || sourceAddress == 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50002);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryReadUInt64(sourceAddress, out var sourceTick))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var resultTick = checked((long)sourceTick + checked(delta * (long)microsecondsPerUnit));
|
||||||
|
if (resultTick < 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50003);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryWriteUInt64(destinationAddress, unchecked((ulong)resultTick)))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OverflowException)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50003);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int AddCalendarDelta(CpuContext ctx, Func<DateTime, DateTime> transform)
|
||||||
|
{
|
||||||
|
var destinationAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var sourceAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (destinationAddress == 0 || sourceAddress == 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50002);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryReadUInt64(sourceAddress, out var sourceTick))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryConvertTickToDateTime(sourceTick, out var sourceDateTime))
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50003);
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime resultDateTime;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
resultDateTime = transform(sourceDateTime);
|
||||||
|
}
|
||||||
|
catch (ArgumentOutOfRangeException)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50003);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryWriteTickFromDateTime(ctx, destinationAddress, resultDateTime))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ValidateRtcDateTime(RtcDateTime time)
|
||||||
|
{
|
||||||
|
if (time.Year < 1 || time.Year > 9999)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50008);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (time.Month < 1 || time.Month > 12)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50009);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (time.Day < 1 || time.Day > DateTime.DaysInMonth(time.Year, time.Month))
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B5000A);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (time.Hour > 23)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B5000B);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (time.Minute > 59)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B5000C);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (time.Second > 59)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B5000D);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (time.Microsecond > 999_999)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B5000E);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsValidCalendarDate(int year, int month, int day)
|
||||||
|
{
|
||||||
|
if (year < 1 || year > 9999 || month < 1 || month > 12)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return day >= 1 && day <= DateTime.DaysInMonth(year, month);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadRtcDateTime(CpuContext ctx, ulong address, out RtcDateTime time)
|
||||||
{
|
{
|
||||||
Span<byte> buffer = stackalloc byte[16];
|
Span<byte> buffer = stackalloc byte[16];
|
||||||
if (!ctx.Memory.TryRead(address, buffer))
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
{
|
{
|
||||||
rtcDateTime = default;
|
time = default;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
rtcDateTime = new RtcDateTime(
|
time = new RtcDateTime(
|
||||||
BinaryPrimitives.ReadUInt16LittleEndian(buffer[0..2]),
|
BinaryPrimitives.ReadUInt16LittleEndian(buffer[0..2]),
|
||||||
BinaryPrimitives.ReadUInt16LittleEndian(buffer[2..4]),
|
BinaryPrimitives.ReadUInt16LittleEndian(buffer[2..4]),
|
||||||
BinaryPrimitives.ReadUInt16LittleEndian(buffer[4..6]),
|
BinaryPrimitives.ReadUInt16LittleEndian(buffer[4..6]),
|
||||||
@@ -176,19 +831,117 @@ public static class RtcExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryWriteRtcDateTime(CpuContext ctx, ulong address, DateTime dateTime)
|
private static bool TryWriteRtcDateTime(CpuContext ctx, ulong address, RtcDateTime dateTime)
|
||||||
{
|
{
|
||||||
Span<byte> rtcDateTime = stackalloc byte[16];
|
Span<byte> buffer = stackalloc byte[16];
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[0..2], checked((ushort)dateTime.Year));
|
BinaryPrimitives.WriteUInt16LittleEndian(buffer[0..2], dateTime.Year);
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[2..4], checked((ushort)dateTime.Month));
|
BinaryPrimitives.WriteUInt16LittleEndian(buffer[2..4], dateTime.Month);
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[4..6], checked((ushort)dateTime.Day));
|
BinaryPrimitives.WriteUInt16LittleEndian(buffer[4..6], dateTime.Day);
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[6..8], checked((ushort)dateTime.Hour));
|
BinaryPrimitives.WriteUInt16LittleEndian(buffer[6..8], dateTime.Hour);
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[8..10], checked((ushort)dateTime.Minute));
|
BinaryPrimitives.WriteUInt16LittleEndian(buffer[8..10], dateTime.Minute);
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[10..12], checked((ushort)dateTime.Second));
|
BinaryPrimitives.WriteUInt16LittleEndian(buffer[10..12], dateTime.Second);
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(
|
BinaryPrimitives.WriteUInt32LittleEndian(buffer[12..16], dateTime.Microsecond);
|
||||||
rtcDateTime[12..16],
|
return ctx.Memory.TryWrite(address, buffer);
|
||||||
checked((uint)((dateTime.Ticks % TimeSpan.TicksPerSecond) / DateTimeTicksPerMicrosecond)));
|
}
|
||||||
return ctx.Memory.TryWrite(address, rtcDateTime);
|
|
||||||
|
private static RtcDateTime ToRtcDateTime(DateTime dateTime)
|
||||||
|
{
|
||||||
|
return new RtcDateTime(
|
||||||
|
(ushort)dateTime.Year,
|
||||||
|
(ushort)dateTime.Month,
|
||||||
|
(ushort)dateTime.Day,
|
||||||
|
(ushort)dateTime.Hour,
|
||||||
|
(ushort)dateTime.Minute,
|
||||||
|
(ushort)dateTime.Second,
|
||||||
|
(uint)(dateTime.Ticks % TimeSpan.TicksPerSecond / DateTimeTicksPerMicrosecond));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RtcDateTime ToRtcDateTime(DateTimeOffset dateTimeOffset)
|
||||||
|
{
|
||||||
|
return ToRtcDateTime(dateTimeOffset.DateTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteTickFromDateTime(CpuContext ctx, ulong address, DateTime dateTime)
|
||||||
|
{
|
||||||
|
return ctx.TryWriteUInt64(address, unchecked((ulong)(dateTime.Ticks / DateTimeTicksPerMicrosecond)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryConvertTickToDateTime(ulong tick, out DateTime dateTime)
|
||||||
|
{
|
||||||
|
var maxSupportedTick = unchecked((ulong)(DateTime.MaxValue.Ticks / DateTimeTicksPerMicrosecond));
|
||||||
|
if (tick > maxSupportedTick)
|
||||||
|
{
|
||||||
|
dateTime = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
dateTime = new DateTime(checked((long)(tick * (ulong)DateTimeTicksPerMicrosecond)), DateTimeKind.Utc);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (ArgumentOutOfRangeException)
|
||||||
|
{
|
||||||
|
dateTime = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryConvertRtcDateTimeToTick(RtcDateTime time, out ulong tick)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var year = time.Year;
|
||||||
|
var month = time.Month;
|
||||||
|
var day = time.Day;
|
||||||
|
if (month > 2)
|
||||||
|
{
|
||||||
|
month -= 3;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
month += 9;
|
||||||
|
year -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var century = year / 100;
|
||||||
|
var yearOfCentury = year - (100 * century);
|
||||||
|
|
||||||
|
ulong days = ((146097UL * (ulong)century) >> 2)
|
||||||
|
+ ((1461UL * (ulong)yearOfCentury) >> 2)
|
||||||
|
+ ((153UL * (ulong)month + 2UL) / 5UL)
|
||||||
|
+ day;
|
||||||
|
days -= 307UL;
|
||||||
|
days *= MicrosecondsPerDay;
|
||||||
|
|
||||||
|
var timeOfDay = (ulong)time.Hour * MicrosecondsPerHour
|
||||||
|
+ (ulong)time.Minute * MicrosecondsPerMinute
|
||||||
|
+ (ulong)time.Second * MicrosecondsPerSecond
|
||||||
|
+ time.Microsecond;
|
||||||
|
|
||||||
|
tick = days + timeOfDay;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (OverflowException)
|
||||||
|
{
|
||||||
|
tick = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryConvertTickToRtcDateTime(ulong tick, out RtcDateTime time)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var dateTime = new DateTime(checked((long)(tick * DateTimeTicksPerMicrosecond)), DateTimeKind.Utc);
|
||||||
|
time = ToRtcDateTime(dateTime);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (ArgumentOutOfRangeException)
|
||||||
|
{
|
||||||
|
time = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly record struct RtcDateTime(
|
private readonly record struct RtcDateTime(
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -30,13 +30,19 @@ public static class SaveDataExports
|
|||||||
private const uint MountModeCreate2 = 1u << 5;
|
private const uint MountModeCreate2 = 1u << 5;
|
||||||
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> _preparedTransactionResources = [];
|
||||||
private static string? _titleId;
|
private static string? _titleId;
|
||||||
|
private static int _nextTransactionResource;
|
||||||
|
|
||||||
public static void ConfigureApplicationInfo(string? titleId)
|
public static void ConfigureApplicationInfo(string? titleId)
|
||||||
{
|
{
|
||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
_titleId = string.IsNullOrWhiteSpace(titleId) ? null : SanitizePathSegment(titleId.Trim());
|
_titleId = string.IsNullOrWhiteSpace(titleId) ? null : SanitizePathSegment(titleId.Trim());
|
||||||
|
_transactionResources.Clear();
|
||||||
|
_preparedTransactionResources.Clear();
|
||||||
|
_nextTransactionResource = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,7 +250,6 @@ public static class SaveDataExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int _nextTransactionResource;
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "gjRZNnw0JPE",
|
Nid = "gjRZNnw0JPE",
|
||||||
ExportName = "sceSaveDataCreateTransactionResource",
|
ExportName = "sceSaveDataCreateTransactionResource",
|
||||||
@@ -252,25 +257,124 @@ public static class SaveDataExports
|
|||||||
LibraryName = "libSceSaveData")]
|
LibraryName = "libSceSaveData")]
|
||||||
public static int SaveDataCreateTransactionResource(CpuContext ctx)
|
public static int SaveDataCreateTransactionResource(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
var memorySize = ctx[CpuRegister.Rdi];
|
||||||
var reserved = ctx[CpuRegister.Rsi];
|
int resource;
|
||||||
var resourceAddress = ctx[CpuRegister.Rdx];
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
resource = ++_nextTransactionResource;
|
||||||
|
_transactionResources.Add(resource);
|
||||||
|
}
|
||||||
|
|
||||||
if (resourceAddress == 0)
|
TraceSaveData($"create_transaction_resource memory_size=0x{memorySize:X} resource={resource}");
|
||||||
|
return ctx.SetReturn(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "lJUQuaKqoKY",
|
||||||
|
ExportName = "sceSaveDataDeleteTransactionResource",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSaveData")]
|
||||||
|
public static int SaveDataDeleteTransactionResource(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var resource = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
_transactionResources.Remove(resource);
|
||||||
|
_preparedTransactionResources.Remove(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceSaveData($"delete_transaction_resource resource={resource}");
|
||||||
|
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);
|
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
var id = (uint)Interlocked.Increment(ref _nextTransactionResource);
|
if (!TryReadFixedAscii(ctx, mountPointAddress, 16, out var mountPoint))
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt32(resourceAddress, id))
|
|
||||||
{
|
{
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceSaveData(
|
if (string.IsNullOrWhiteSpace(mountPoint))
|
||||||
$"create_transaction_resource user={userId} reserved=0x{reserved:X} resource_addr=0x{resourceAddress:X} id={id}");
|
{
|
||||||
|
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);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ public static class UserServiceExports
|
|||||||
private const int OrbisUserServiceErrorNoEvent = unchecked((int)0x80960007);
|
private const int OrbisUserServiceErrorNoEvent = unchecked((int)0x80960007);
|
||||||
private const int OrbisUserServiceErrorInvalidParameter = unchecked((int)0x80960009);
|
private const int OrbisUserServiceErrorInvalidParameter = unchecked((int)0x80960009);
|
||||||
private const int OrbisUserServiceErrorBufferTooShort = unchecked((int)0x8096000A);
|
private const int OrbisUserServiceErrorBufferTooShort = unchecked((int)0x8096000A);
|
||||||
private const int PrimaryUserId = 1;
|
// Retail user-service IDs begin at 0x3E8.
|
||||||
|
private const int PrimaryUserId = 1000;
|
||||||
private const int InvalidUserId = -1;
|
private const int InvalidUserId = -1;
|
||||||
private const string PrimaryUserName = "SharpEmu";
|
private const string PrimaryUserName = "SharpEmu";
|
||||||
private static int _loginEventDelivered;
|
private static int _loginEventDelivered;
|
||||||
@@ -111,7 +112,8 @@ public static class UserServiceExports
|
|||||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
var nameAddress = ctx[CpuRegister.Rsi];
|
var nameAddress = ctx[CpuRegister.Rsi];
|
||||||
var capacity = ctx[CpuRegister.Rdx];
|
var capacity = ctx[CpuRegister.Rdx];
|
||||||
if (userId != PrimaryUserId)
|
// Zero selects the current user.
|
||||||
|
if (userId != 0 && userId != PrimaryUserId)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidParameter);
|
return ctx.SetReturn(OrbisUserServiceErrorInvalidParameter);
|
||||||
}
|
}
|
||||||
@@ -137,6 +139,28 @@ public static class UserServiceExports
|
|||||||
return ctx.SetReturn(result);
|
return ctx.SetReturn(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "-sD02mFDBh4",
|
||||||
|
ExportName = "sceUserServiceGetGamePresets",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceUserService")]
|
||||||
|
public static int UserServiceGetGamePresets(CpuContext ctx)
|
||||||
|
{
|
||||||
|
// Return deterministic defaults for the offline profile.
|
||||||
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var resultAddress = ctx[CpuRegister.Rcx];
|
||||||
|
Span<byte> defaults = stackalloc byte[0x18];
|
||||||
|
if (resultAddress == 0 || !ctx.Memory.TryWrite(resultAddress, defaults))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceUserService(
|
||||||
|
$"get_game_presets user={userId} title=0x{ctx[CpuRegister.Rsi]:X16} " +
|
||||||
|
$"key=0x{ctx[CpuRegister.R9]:X} out=0x{resultAddress:X16} result=0x00000000");
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "D-CzAxQL0XI",
|
Nid = "D-CzAxQL0XI",
|
||||||
ExportName = "sceUserServiceGetPlatformPrivacySetting",
|
ExportName = "sceUserServiceGetPlatformPrivacySetting",
|
||||||
|
|||||||
@@ -14,6 +14,17 @@ internal static class PngSplashLoader
|
|||||||
];
|
];
|
||||||
|
|
||||||
public static bool TryLoad(out byte[] pixels, out uint width, out uint height)
|
public static bool TryLoad(out byte[] pixels, out uint width, out uint height)
|
||||||
|
=> TryLoad("pic0.png", requestRgba: false, out pixels, out width, out height);
|
||||||
|
|
||||||
|
public static bool TryLoadIcon(out byte[] pixels, out uint width, out uint height)
|
||||||
|
=> TryLoad("icon0.png", requestRgba: true, out pixels, out width, out height);
|
||||||
|
|
||||||
|
private static bool TryLoad(
|
||||||
|
string fileName,
|
||||||
|
bool requestRgba,
|
||||||
|
out byte[] pixels,
|
||||||
|
out uint width,
|
||||||
|
out uint height)
|
||||||
{
|
{
|
||||||
pixels = [];
|
pixels = [];
|
||||||
width = 0;
|
width = 0;
|
||||||
@@ -27,13 +38,18 @@ internal static class PngSplashLoader
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var path = Path.Combine(app0Root, "sce_sys", "pic0.png");
|
var path = Path.Combine(app0Root, "sce_sys", fileName);
|
||||||
if (!File.Exists(path))
|
if (!File.Exists(path))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return TryDecode(File.ReadAllBytes(path), out pixels, out width, out height);
|
return TryDecode(
|
||||||
|
File.ReadAllBytes(path),
|
||||||
|
out pixels,
|
||||||
|
out width,
|
||||||
|
out height,
|
||||||
|
requestRgba);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -48,7 +64,8 @@ internal static class PngSplashLoader
|
|||||||
ReadOnlySpan<byte> png,
|
ReadOnlySpan<byte> png,
|
||||||
out byte[] pixels,
|
out byte[] pixels,
|
||||||
out uint width,
|
out uint width,
|
||||||
out uint height)
|
out uint height,
|
||||||
|
bool requestRgba)
|
||||||
{
|
{
|
||||||
pixels = [];
|
pixels = [];
|
||||||
width = 0;
|
width = 0;
|
||||||
@@ -154,9 +171,13 @@ internal static class PngSplashLoader
|
|||||||
sourceOffset < reconstructed.Length;
|
sourceOffset < reconstructed.Length;
|
||||||
sourceOffset += sourceBytesPerPixel, targetOffset += 4)
|
sourceOffset += sourceBytesPerPixel, targetOffset += 4)
|
||||||
{
|
{
|
||||||
pixels[targetOffset] = reconstructed[sourceOffset + 2];
|
pixels[targetOffset] = requestRgba
|
||||||
|
? reconstructed[sourceOffset]
|
||||||
|
: reconstructed[sourceOffset + 2];
|
||||||
pixels[targetOffset + 1] = reconstructed[sourceOffset + 1];
|
pixels[targetOffset + 1] = reconstructed[sourceOffset + 1];
|
||||||
pixels[targetOffset + 2] = reconstructed[sourceOffset];
|
pixels[targetOffset + 2] = requestRgba
|
||||||
|
? reconstructed[sourceOffset + 2]
|
||||||
|
: reconstructed[sourceOffset];
|
||||||
pixels[targetOffset + 3] = sourceBytesPerPixel == 4
|
pixels[targetOffset + 3] = sourceBytesPerPixel == 4
|
||||||
? reconstructed[sourceOffset + 3]
|
? reconstructed[sourceOffset + 3]
|
||||||
: (byte)0xFF;
|
: (byte)0xFF;
|
||||||
|
|||||||
@@ -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 SharpEmu.Libs.Audio;
|
||||||
using SharpEmu.Libs.Kernel;
|
using SharpEmu.Libs.Kernel;
|
||||||
|
using SharpEmu.Logging;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
@@ -45,10 +47,171 @@ public static class VideoOutExports
|
|||||||
private static readonly object _stateGate = new();
|
private static readonly object _stateGate = new();
|
||||||
private static readonly object _frameDumpGate = new();
|
private static readonly object _frameDumpGate = new();
|
||||||
private static readonly Dictionary<int, VideoOutPortState> _ports = new();
|
private static readonly Dictionary<int, VideoOutPortState> _ports = new();
|
||||||
|
|
||||||
|
// Hardware raises vblank autonomously; UE blocks its frame loop on it.
|
||||||
|
private const double VblankHz = 60.0;
|
||||||
|
private const int VblankWaitTimeoutMilliseconds = 100;
|
||||||
|
private static Thread? _vblankPumpThread;
|
||||||
|
private static int _vblankPumpStarted;
|
||||||
|
private static volatile int _vblankPumpStopRequested;
|
||||||
|
private static volatile int _presentationWindowCloseNotified;
|
||||||
|
|
||||||
|
private static readonly object _vblankEdgeGate = new();
|
||||||
|
private static ulong _vblankEdgeSequence;
|
||||||
|
private static long _vblankMissedEdges;
|
||||||
|
|
||||||
|
private static void EnsureVblankPumpStarted()
|
||||||
|
{
|
||||||
|
if (Interlocked.Exchange(ref _vblankPumpStarted, 1) != 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HostTimerResolution.Request();
|
||||||
|
|
||||||
|
_vblankPumpThread = new Thread(VblankPumpLoop)
|
||||||
|
{
|
||||||
|
IsBackground = true,
|
||||||
|
Name = "SharpEmu VideoOut vblank",
|
||||||
|
Priority = ThreadPriority.AboveNormal,
|
||||||
|
};
|
||||||
|
_vblankPumpThread.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void VblankPumpLoop()
|
||||||
|
{
|
||||||
|
var intervalTicks = Math.Max(1L, (long)(Stopwatch.Frequency / VblankHz));
|
||||||
|
var nextEdge = Stopwatch.GetTimestamp() + intervalTicks;
|
||||||
|
|
||||||
|
while (_vblankPumpStopRequested == 0)
|
||||||
|
{
|
||||||
|
WaitUntilTimestamp(nextEdge);
|
||||||
|
PumpVblanks();
|
||||||
|
|
||||||
|
nextEdge += intervalTicks;
|
||||||
|
|
||||||
|
var now = Stopwatch.GetTimestamp();
|
||||||
|
if (nextEdge < now)
|
||||||
|
{
|
||||||
|
var missed = (now - nextEdge) / intervalTicks + 1;
|
||||||
|
Interlocked.Add(ref _vblankMissedEdges, missed);
|
||||||
|
nextEdge = now + intervalTicks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void NotifyPresentationWindowClosed()
|
||||||
|
{
|
||||||
|
if (Interlocked.Exchange(ref _presentationWindowCloseNotified, 1) != 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine("[LOADER][INFO] VideoOut presentation window closed");
|
||||||
|
RequestHostShutdown("videoout-window-closed");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void NotifyHostInterrupt()
|
||||||
|
{
|
||||||
|
if (Interlocked.Exchange(ref _presentationWindowCloseNotified, 1) != 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine("[LOADER][INFO] Host interrupt requested");
|
||||||
|
RequestHostShutdown("host-interrupt");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RequestHostShutdown(string reason)
|
||||||
|
{
|
||||||
|
AudioOutExports.ShutdownAllPorts();
|
||||||
|
StopVblankPump();
|
||||||
|
HostSessionControl.RequestShutdown(reason);
|
||||||
|
ScheduleProcessExitIfGuestDoesNotStop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ScheduleProcessExitIfGuestDoesNotStop()
|
||||||
|
{
|
||||||
|
ThreadPool.QueueUserWorkItem(static _ =>
|
||||||
|
{
|
||||||
|
Thread.Sleep(2000);
|
||||||
|
Environment.Exit(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void StopVblankPump()
|
||||||
|
{
|
||||||
|
if (Interlocked.Exchange(ref _vblankPumpStopRequested, 1) != 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var thread = _vblankPumpThread;
|
||||||
|
if (thread is { IsAlive: true })
|
||||||
|
{
|
||||||
|
thread.Join(TimeSpan.FromSeconds(2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WaitUntilTimestamp(long deadlineTicks)
|
||||||
|
{
|
||||||
|
var spinThresholdTicks = Stopwatch.Frequency * 2L / 1000L;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var remaining = deadlineTicks - Stopwatch.GetTimestamp();
|
||||||
|
if (remaining <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remaining > spinThresholdTicks)
|
||||||
|
{
|
||||||
|
var sleepMilliseconds =
|
||||||
|
(int)((remaining - spinThresholdTicks) * 1000L / Stopwatch.Frequency);
|
||||||
|
if (sleepMilliseconds > 0)
|
||||||
|
{
|
||||||
|
Thread.Sleep(sleepMilliseconds);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread.SpinWait(64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PumpVblanks()
|
||||||
|
{
|
||||||
|
lock (_vblankEdgeGate)
|
||||||
|
{
|
||||||
|
_vblankEdgeSequence++;
|
||||||
|
Monitor.PulseAll(_vblankEdgeGate);
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoOutPortState[] ports;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
if (_ports.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signalling reaches WakeBlockedThreads -> Pump(), which serialises on one global
|
||||||
|
// flag. Waking an unwatched queue would hold it 60x/sec and starve guest threads.
|
||||||
|
ports = _ports.Values.Where(static port => port.VblankEvents.Count != 0).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var port in ports)
|
||||||
|
{
|
||||||
|
SignalVblank(port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static readonly Dictionary<(int Handle, int BufferIndex, ulong Address), ulong> _lastFrameFingerprints = new();
|
private static readonly Dictionary<(int Handle, int BufferIndex, ulong Address), ulong> _lastFrameFingerprints = new();
|
||||||
private static int _nextHandle = 1;
|
private static int _nextHandle = 1;
|
||||||
private static int _frameDumpCount;
|
private static int _frameDumpCount;
|
||||||
private static long _nextFrameDumpIndex;
|
private static long _nextFrameDumpIndex;
|
||||||
|
private static string _windowTitleBase = "SharpEmu VideoOut";
|
||||||
private static string _windowTitle = "SharpEmu VideoOut";
|
private static string _windowTitle = "SharpEmu VideoOut";
|
||||||
private static readonly bool _logFrameRate = string.Equals(
|
private static readonly bool _logFrameRate = string.Equals(
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT_FPS"),
|
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT_FPS"),
|
||||||
@@ -58,6 +221,10 @@ public static class VideoOutExports
|
|||||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT_SYNC"),
|
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT_SYNC"),
|
||||||
"1",
|
"1",
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
|
private static readonly bool _dumpVideoOut = string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_VIDEOOUT"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal);
|
||||||
private static long _frameRateWindowStart = Stopwatch.GetTimestamp();
|
private static long _frameRateWindowStart = Stopwatch.GetTimestamp();
|
||||||
private static long _submittedFrameCount;
|
private static long _submittedFrameCount;
|
||||||
private static long _presentedFrameCount;
|
private static long _presentedFrameCount;
|
||||||
@@ -80,9 +247,24 @@ public static class VideoOutExports
|
|||||||
var application = parts.Count == 0 ? "VideoOut" : string.Join(' ', parts);
|
var application = parts.Count == 0 ? "VideoOut" : string.Join(' ', parts);
|
||||||
var versionSuffix = string.IsNullOrWhiteSpace(version) ? string.Empty : $" v{version.Trim()}";
|
var versionSuffix = string.IsNullOrWhiteSpace(version) ? string.Empty : $" v{version.Trim()}";
|
||||||
var commitSuffix = string.IsNullOrWhiteSpace(emulatorCommitSha) ? string.Empty : $" \u00b7 {emulatorCommitSha.Trim()}";
|
var commitSuffix = string.IsNullOrWhiteSpace(emulatorCommitSha) ? string.Empty : $" \u00b7 {emulatorCommitSha.Trim()}";
|
||||||
|
var hardwareSuffix = $" \u00b7 {HostSystemInfo.CpuName}";
|
||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
_windowTitle = $"SharpEmu{commitSuffix} - {application}{versionSuffix}";
|
_windowTitleBase = $"SharpEmu{commitSuffix} - {application}{versionSuffix}{hardwareSuffix}";
|
||||||
|
_windowTitle = $"{_windowTitleBase} \u00b7 {HostSystemInfo.GpuName}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void SetSelectedGpuName(string gpuName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(gpuName))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
_windowTitle = $"{_windowTitleBase} \u00b7 {gpuName.Trim()}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,6 +359,7 @@ public static class VideoOutExports
|
|||||||
{
|
{
|
||||||
Handle = handle,
|
Handle = handle,
|
||||||
};
|
};
|
||||||
|
EnsureVblankPumpStarted();
|
||||||
return handle;
|
return handle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -320,7 +503,20 @@ public static class VideoOutExports
|
|||||||
return OrbisVideoOutErrorInvalidHandle;
|
return OrbisVideoOutErrorInvalidHandle;
|
||||||
}
|
}
|
||||||
|
|
||||||
Thread.Sleep(1);
|
EnsureVblankPumpStarted();
|
||||||
|
|
||||||
|
lock (_vblankEdgeGate)
|
||||||
|
{
|
||||||
|
var entryEdge = _vblankEdgeSequence;
|
||||||
|
while (_vblankEdgeSequence == entryEdge)
|
||||||
|
{
|
||||||
|
if (!Monitor.Wait(_vblankEdgeGate, VblankWaitTimeoutMilliseconds))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SignalVblank(port);
|
SignalVblank(port);
|
||||||
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
@@ -931,10 +1127,7 @@ public static class VideoOutExports
|
|||||||
displayBuffer.PitchInPixel);
|
displayBuffer.PitchInPixel);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(
|
if (_dumpVideoOut)
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_VIDEOOUT"),
|
|
||||||
"1",
|
|
||||||
StringComparison.Ordinal))
|
|
||||||
{
|
{
|
||||||
_ = TryDumpFrame(ctx, port, bufferIndex, flipMode, flipArg);
|
_ = TryDumpFrame(ctx, port, bufferIndex, flipMode, flipArg);
|
||||||
}
|
}
|
||||||
@@ -994,9 +1187,11 @@ public static class VideoOutExports
|
|||||||
var elapsedSeconds = (double)elapsedTicks / Stopwatch.Frequency;
|
var elapsedSeconds = (double)elapsedTicks / Stopwatch.Frequency;
|
||||||
var submitted = Interlocked.Exchange(ref _submittedFrameCount, 0);
|
var submitted = Interlocked.Exchange(ref _submittedFrameCount, 0);
|
||||||
var presentedCount = Interlocked.Exchange(ref _presentedFrameCount, 0);
|
var presentedCount = Interlocked.Exchange(ref _presentedFrameCount, 0);
|
||||||
|
var missedEdges = Interlocked.Exchange(ref _vblankMissedEdges, 0);
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][PERF] videoout submitted_fps={submitted / elapsedSeconds:F1} " +
|
$"[LOADER][PERF] videoout submitted_fps={submitted / elapsedSeconds:F1} " +
|
||||||
$"presented_fps={presentedCount / elapsedSeconds:F1}");
|
$"presented_fps={presentedCount / elapsedSeconds:F1} " +
|
||||||
|
$"vblank_missed={missedEdges}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int RegisterBufferRange(VideoOutPortState port, int startIndex, ReadOnlySpan<ulong> addresses, BufferAttribute attribute, int requestedGroupIndex = -1)
|
private static int RegisterBufferRange(VideoOutPortState port, int startIndex, ReadOnlySpan<ulong> addresses, BufferAttribute attribute, int requestedGroupIndex = -1)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Voice;
|
||||||
|
|
||||||
|
public static class VoiceQoSExports
|
||||||
|
{
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "U8IfNl6-Css",
|
||||||
|
ExportName = "sceVoiceQoSInit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceVoiceQoS")]
|
||||||
|
public static int VoiceQoSInit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,19 +6,15 @@ using System.Text;
|
|||||||
|
|
||||||
namespace SharpEmu.Logging;
|
namespace SharpEmu.Logging;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Writes log entries to a file. Thread-safe via an internal lock.
|
|
||||||
/// <see cref="StreamWriter.AutoFlush"/> is enabled so entries survive a crash.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class FileLogSink : ISharpEmuLogSink, IDisposable
|
public sealed class FileLogSink : ISharpEmuLogSink, IDisposable
|
||||||
{
|
{
|
||||||
|
private static readonly TimeSpan FlushInterval = TimeSpan.FromMilliseconds(500);
|
||||||
|
|
||||||
private readonly object _sync = new();
|
private readonly object _sync = new();
|
||||||
private readonly StreamWriter _writer;
|
private readonly StreamWriter _writer;
|
||||||
|
private readonly Timer _flushTimer;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
/// <param name="path">Absolute or relative file path. Parent directories are created if missing.</param>
|
|
||||||
/// <param name="append"><see langword="true"/> to append to an existing file; <see langword="false"/> to overwrite.</param>
|
|
||||||
/// <param name="includeTimestamp">Always recommended for file logs — entries include a full date-time prefix.</param>
|
|
||||||
public FileLogSink(string path, bool append = true, bool includeTimestamp = true)
|
public FileLogSink(string path, bool append = true, bool includeTimestamp = true)
|
||||||
{
|
{
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(path);
|
ArgumentException.ThrowIfNullOrWhiteSpace(path);
|
||||||
@@ -34,14 +30,20 @@ public sealed class FileLogSink : ISharpEmuLogSink, IDisposable
|
|||||||
append ? FileMode.Append : FileMode.Create,
|
append ? FileMode.Append : FileMode.Create,
|
||||||
FileAccess.Write,
|
FileAccess.Write,
|
||||||
FileShare.Read,
|
FileShare.Read,
|
||||||
bufferSize: 4096,
|
bufferSize: 65536,
|
||||||
FileOptions.SequentialScan);
|
FileOptions.SequentialScan);
|
||||||
_writer = new StreamWriter(fileStream, Encoding.UTF8)
|
_writer = new StreamWriter(fileStream, Encoding.UTF8, bufferSize: 65536)
|
||||||
{
|
{
|
||||||
AutoFlush = true
|
AutoFlush = false
|
||||||
};
|
};
|
||||||
|
|
||||||
IncludeTimestamp = includeTimestamp;
|
IncludeTimestamp = includeTimestamp;
|
||||||
|
|
||||||
|
_flushTimer = new Timer(
|
||||||
|
static state => ((FileLogSink)state!).FlushBuffered(),
|
||||||
|
this,
|
||||||
|
FlushInterval,
|
||||||
|
FlushInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IncludeTimestamp { get; set; }
|
public bool IncludeTimestamp { get; set; }
|
||||||
@@ -84,11 +86,31 @@ public sealed class FileLogSink : ISharpEmuLogSink, IDisposable
|
|||||||
{
|
{
|
||||||
_writer.WriteLine(entry.Exception);
|
_writer.WriteLine(entry.Exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (entry.Level >= LogLevel.Error)
|
||||||
|
{
|
||||||
|
_writer.Flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FlushBuffered()
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_writer.Flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
_flushTimer.Dispose();
|
||||||
|
|
||||||
lock (_sync)
|
lock (_sync)
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using Microsoft.Win32;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace SharpEmu.Logging;
|
||||||
|
|
||||||
|
/// <summary>Provides best-effort information about the host system.</summary>
|
||||||
|
public static class HostSystemInfo
|
||||||
|
{
|
||||||
|
private static readonly Lazy<string> CpuNameValue = new(GetCpuName);
|
||||||
|
private static readonly Lazy<string> GpuNameValue = new(GetPreferredGpuName);
|
||||||
|
private static readonly Lazy<string> MemoryDescriptionValue = new(GetMemoryDescription);
|
||||||
|
|
||||||
|
/// <summary>Host CPU name, or a safe fallback when it cannot be determined.</summary>
|
||||||
|
public static string CpuName => CpuNameValue.Value;
|
||||||
|
|
||||||
|
/// <summary>Preferred physical GPU name, or a safe fallback when it cannot be determined.</summary>
|
||||||
|
public static string GpuName => GpuNameValue.Value;
|
||||||
|
|
||||||
|
/// <summary>Returns a concise description of the host hardware for diagnostic logs.</summary>
|
||||||
|
public static string Summary =>
|
||||||
|
$"Host hardware: CPU: {CpuName}; GPU: {GpuName}; RAM: {MemoryDescriptionValue.Value}.";
|
||||||
|
|
||||||
|
private static string GetCpuName()
|
||||||
|
{
|
||||||
|
if (!OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
return $"{Environment.ProcessorCount} logical processors";
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var key = Registry.LocalMachine.OpenSubKey(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0");
|
||||||
|
if (key?.GetValue("ProcessorNameString") is string name && !string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
name = Regex.Replace(
|
||||||
|
name.Trim(),
|
||||||
|
@"\s+\d+-Core Processor$",
|
||||||
|
string.Empty,
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||||
|
return Regex.Replace(
|
||||||
|
name,
|
||||||
|
@"\s+with Radeon Graphics$",
|
||||||
|
string.Empty,
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Hardware information is diagnostic only.
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"{Environment.ProcessorCount} logical processors";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetPreferredGpuName()
|
||||||
|
{
|
||||||
|
if (!OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var preferredName = "unknown";
|
||||||
|
var preferredScore = int.MinValue;
|
||||||
|
for (uint index = 0; ; index++)
|
||||||
|
{
|
||||||
|
var device = new DisplayDevice
|
||||||
|
{
|
||||||
|
cb = Marshal.SizeOf<DisplayDevice>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!EnumDisplayDevices(null, index, ref device, 0))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = device.DeviceString?.Trim();
|
||||||
|
var score = ScoreGpu(name);
|
||||||
|
if (score > preferredScore)
|
||||||
|
{
|
||||||
|
preferredName = name!;
|
||||||
|
preferredScore = score;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return preferredScore > 0 ? preferredName : "unknown";
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Hardware information is diagnostic only.
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ScoreGpu(string? name)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(name) ||
|
||||||
|
name.Contains("virtual", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
name.Contains("remote", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
name.Contains("basic display", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.Contains("nvidia", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
name.Contains("geforce", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.Contains("amd", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
name.Contains("radeon", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetMemoryDescription()
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var status = new MemoryStatusEx
|
||||||
|
{
|
||||||
|
dwLength = (uint)Marshal.SizeOf<MemoryStatusEx>(),
|
||||||
|
};
|
||||||
|
if (GlobalMemoryStatusEx(ref status))
|
||||||
|
{
|
||||||
|
var megabytes = status.ullTotalPhys / (1024 * 1024);
|
||||||
|
var gigabytes = status.ullTotalPhys / (1024d * 1024 * 1024);
|
||||||
|
return $"{megabytes:N0} MB ({gigabytes:N1} GB)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Hardware information is diagnostic only.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||||
|
private struct DisplayDevice
|
||||||
|
{
|
||||||
|
public int cb;
|
||||||
|
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
|
||||||
|
public string? DeviceName;
|
||||||
|
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
|
||||||
|
public string? DeviceString;
|
||||||
|
public int StateFlags;
|
||||||
|
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
|
||||||
|
public string? DeviceId;
|
||||||
|
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
|
||||||
|
public string? DeviceKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
|
||||||
|
private struct MemoryStatusEx
|
||||||
|
{
|
||||||
|
public uint dwLength;
|
||||||
|
public uint dwMemoryLoad;
|
||||||
|
public ulong ullTotalPhys;
|
||||||
|
public ulong ullAvailPhys;
|
||||||
|
public ulong ullTotalPageFile;
|
||||||
|
public ulong ullAvailPageFile;
|
||||||
|
public ulong ullTotalVirtual;
|
||||||
|
public ulong ullAvailVirtual;
|
||||||
|
public ulong ullAvailExtendedVirtual;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static extern bool EnumDisplayDevices(string? deviceName, uint deviceNum, ref DisplayDevice displayDevice, uint flags);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static extern bool GlobalMemoryStatusEx(ref MemoryStatusEx buffer);
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Text;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.Ampr;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Tests.Ampr;
|
||||||
|
|
||||||
|
// PakDirectoryTracker reconstructs the absolute pak offset for a "next sequential chunk" read
|
||||||
|
// (offset -1) from the requested byte count. When several archived files share that byte count,
|
||||||
|
// picking by directory order alone mis-resolves out-of-order reads: Quake requested progs/h_ogre.mdl
|
||||||
|
// (0x3A34 bytes) but the tracker returned bots/navigation/death32c.nav, which shares the size and
|
||||||
|
// sits earlier in the directory, so the guest parsed NAV2 data as a brush model and aborted.
|
||||||
|
public sealed class PakDirectoryTrackerTests
|
||||||
|
{
|
||||||
|
private const int EntrySize = 64;
|
||||||
|
private const int NameLength = 56;
|
||||||
|
|
||||||
|
private readonly record struct PakEntry(string Name, uint FilePos, uint FileLen);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveSequentialOffset_SizeCollision_PicksEntryNearestReadCursor()
|
||||||
|
{
|
||||||
|
const uint fileId = 0x5AA5_0001;
|
||||||
|
const uint collidingLen = 0x3A34;
|
||||||
|
var memory = new FakeCpuMemory(0x1_0000_0000, 0x1000);
|
||||||
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||||
|
|
||||||
|
// "far" collides in size but lives early in the pak; "near" is what the guest actually wants.
|
||||||
|
LoadDirectory(ctx, fileId, memory, 0x1_0000_0000, dirFileOffset: 0x400, new[]
|
||||||
|
{
|
||||||
|
new PakEntry("far.dat", FilePos: 0x1000, FileLen: collidingLen),
|
||||||
|
new PakEntry("near.dat", FilePos: 0x9000, FileLen: collidingLen),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Advance the read cursor next to the "near" entry, mimicking a burst of nearby asset reads.
|
||||||
|
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination: 0x1_0000_0000, fileOffset: 0x8800, bytesRead: 0x400);
|
||||||
|
|
||||||
|
Assert.Equal(0x9000UL, PakDirectoryTracker.ResolveSequentialOffset(fileId, collidingLen));
|
||||||
|
// Once the near entry is consumed, the same size resolves to the remaining match.
|
||||||
|
Assert.Equal(0x1000UL, PakDirectoryTracker.ResolveSequentialOffset(fileId, collidingLen));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveSequentialOffset_ContiguousSameSizeRun_StaysInOrder()
|
||||||
|
{
|
||||||
|
const uint fileId = 0x5AA5_0002;
|
||||||
|
const uint runLen = 0x608;
|
||||||
|
var memory = new FakeCpuMemory(0x1_0000_0000, 0x1000);
|
||||||
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||||
|
|
||||||
|
// A contiguous run of equal-size lumps (like Quake's gfx/weapons/ww_*.lmp) must still resolve
|
||||||
|
// in packed order when the guest streams them sequentially.
|
||||||
|
LoadDirectory(ctx, fileId, memory, 0x1_0000_0000, dirFileOffset: 0x400, new[]
|
||||||
|
{
|
||||||
|
new PakEntry("lump_a", FilePos: 0x1000, FileLen: runLen),
|
||||||
|
new PakEntry("lump_b", FilePos: 0x1000 + runLen, FileLen: runLen),
|
||||||
|
new PakEntry("lump_c", FilePos: 0x1000 + (runLen * 2), FileLen: runLen),
|
||||||
|
});
|
||||||
|
|
||||||
|
var first = PakDirectoryTracker.ResolveSequentialOffset(fileId, runLen);
|
||||||
|
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination: 0x1_0000_0000, fileOffset: first, bytesRead: runLen);
|
||||||
|
var second = PakDirectoryTracker.ResolveSequentialOffset(fileId, runLen);
|
||||||
|
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination: 0x1_0000_0000, fileOffset: second, bytesRead: runLen);
|
||||||
|
var third = PakDirectoryTracker.ResolveSequentialOffset(fileId, runLen);
|
||||||
|
|
||||||
|
Assert.Equal(0x1000UL, first);
|
||||||
|
Assert.Equal(0x1000UL + runLen, second);
|
||||||
|
Assert.Equal(0x1000UL + (runLen * 2), third);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Feeds the tracker a synthetic PACK header + directory table exactly as the AMPR read path does:
|
||||||
|
// first the 12-byte header (which arms directory parsing), then the directory records themselves.
|
||||||
|
private static void LoadDirectory(
|
||||||
|
CpuContext ctx,
|
||||||
|
uint fileId,
|
||||||
|
FakeCpuMemory memory,
|
||||||
|
ulong destination,
|
||||||
|
ulong dirFileOffset,
|
||||||
|
PakEntry[] entries)
|
||||||
|
{
|
||||||
|
Span<byte> header = stackalloc byte[12];
|
||||||
|
header[0] = (byte)'P';
|
||||||
|
header[1] = (byte)'A';
|
||||||
|
header[2] = (byte)'C';
|
||||||
|
header[3] = (byte)'K';
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(header[4..8], (uint)dirFileOffset);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(header[8..12], (uint)(entries.Length * EntrySize));
|
||||||
|
memory.TryWrite(destination, header);
|
||||||
|
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination, fileOffset: 0, bytesRead: 12);
|
||||||
|
|
||||||
|
var table = new byte[entries.Length * EntrySize];
|
||||||
|
for (var i = 0; i < entries.Length; i++)
|
||||||
|
{
|
||||||
|
var record = table.AsSpan(i * EntrySize, EntrySize);
|
||||||
|
var name = Encoding.ASCII.GetBytes(entries[i].Name);
|
||||||
|
name.AsSpan(0, Math.Min(name.Length, NameLength)).CopyTo(record);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(record.Slice(56, 4), entries[i].FilePos);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(record.Slice(60, 4), entries[i].FileLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
memory.TryWrite(destination, table);
|
||||||
|
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination, dirFileOffset, (ulong)table.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Tests;
|
||||||
|
|
||||||
|
// A single contiguous guest region backed by a byte[]. Enough to hand C strings and small
|
||||||
|
// structures to HLE exports under test without a live guest.
|
||||||
|
internal sealed class FakeCpuMemory : ICpuMemory
|
||||||
|
{
|
||||||
|
private readonly ulong _base;
|
||||||
|
private readonly byte[] _storage;
|
||||||
|
|
||||||
|
public FakeCpuMemory(ulong baseAddress, int size)
|
||||||
|
{
|
||||||
|
_base = baseAddress;
|
||||||
|
_storage = new byte[size];
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryRead(ulong virtualAddress, Span<byte> destination)
|
||||||
|
{
|
||||||
|
if (!TryResolve(virtualAddress, destination.Length, out var offset))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_storage.AsSpan(offset, destination.Length).CopyTo(destination);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
|
||||||
|
{
|
||||||
|
if (!TryResolve(virtualAddress, source.Length, out var offset))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
source.CopyTo(_storage.AsSpan(offset, source.Length));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ulong WriteCString(ulong virtualAddress, string text)
|
||||||
|
{
|
||||||
|
var bytes = System.Text.Encoding.UTF8.GetBytes(text);
|
||||||
|
TryWrite(virtualAddress, bytes);
|
||||||
|
TryWrite(virtualAddress + (ulong)bytes.Length, stackalloc byte[] { 0 });
|
||||||
|
return virtualAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryResolve(ulong virtualAddress, int length, out int offset)
|
||||||
|
{
|
||||||
|
offset = 0;
|
||||||
|
if (virtualAddress < _base)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var relative = virtualAddress - _base;
|
||||||
|
if (relative + (ulong)length > (ulong)_storage.Length)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset = (int)relative;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.Json;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Tests.Json;
|
||||||
|
|
||||||
|
// These NIDs came back "unresolved" in the Quake (PPSA01880) import log right before its
|
||||||
|
// access violation. This asserts they now resolve to the Json handlers and dispatch cleanly,
|
||||||
|
// which is the plumbing the direct-call tests cannot cover.
|
||||||
|
[Collection("JsonObjectHeap")]
|
||||||
|
public sealed class JsonExportRegistrationTests
|
||||||
|
{
|
||||||
|
private static readonly (string Nid, string Name)[] ExpectedExports =
|
||||||
|
{
|
||||||
|
("qBMjqyBn3OM", "_ZN3sce4Json5ValueC1Ev"),
|
||||||
|
("5yHuiWXo2gg", "_ZN3sce4Json5Value3setEb"),
|
||||||
|
("QxVVYhP-mvg", "_ZN3sce4Json5Value3setEl"),
|
||||||
|
("SIe1ZmW7e7s", "_ZN3sce4Json5Value3setEm"),
|
||||||
|
("BSmWDIkV4w4", "_ZN3sce4Json5Value3setEd"),
|
||||||
|
("IKQimvG9Wqs", "_ZN3sce4Json5Value3setENS0_9ValueTypeE"),
|
||||||
|
("6l3Bv2gysNc", "_ZN3sce4Json5Value3setERKNS0_6StringE"),
|
||||||
|
("9KUZFjI1IxA", "_ZN3sce4Json6StringC1EPKc"),
|
||||||
|
("cG1VE2HMl6c", "_ZN3sce4Json6StringD1Ev"),
|
||||||
|
("+drDFyAS6u4", "_ZN3sce4Json11Initializer27setGlobalNullAccessCallbackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_"),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static ModuleManager CreateRegisteredManager()
|
||||||
|
{
|
||||||
|
var manager = new ModuleManager();
|
||||||
|
manager.RegisterFromAssembly(typeof(JsonValueExports).Assembly, Generation.Gen5);
|
||||||
|
return manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void QuakeUnresolvedJsonNids_ResolveToJsonExports()
|
||||||
|
{
|
||||||
|
var manager = CreateRegisteredManager();
|
||||||
|
|
||||||
|
foreach (var (nid, name) in ExpectedExports)
|
||||||
|
{
|
||||||
|
Assert.True(manager.TryGetExport(nid, out var export), $"NID {nid} did not register.");
|
||||||
|
Assert.Equal(name, export.Name);
|
||||||
|
Assert.Equal("libSceJson", export.LibraryName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetGlobalNullAccessCallback_StoresHookAndReturnsOk()
|
||||||
|
{
|
||||||
|
JsonObjectHeap.ResetForTests();
|
||||||
|
var manager = CreateRegisteredManager();
|
||||||
|
var ctx = new CpuContext(new FakeCpuMemory(0x1_0000_0000, 0x1000), Generation.Gen5);
|
||||||
|
ctx[CpuRegister.Rdi] = 0x1_0000_0000; // Initializer instance
|
||||||
|
ctx[CpuRegister.Rsi] = 0x8_0012_3456; // guest callback
|
||||||
|
ctx[CpuRegister.Rdx] = 0x1_0000_0800; // user context
|
||||||
|
|
||||||
|
Assert.True(manager.TryDispatch("+drDFyAS6u4", ctx, out var result));
|
||||||
|
Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, result);
|
||||||
|
Assert.Equal(0UL, ctx[CpuRegister.Rax]);
|
||||||
|
Assert.Equal(0x8_0012_3456UL, JsonObjectHeap.GlobalNullAccessCallback);
|
||||||
|
Assert.Equal(0x1_0000_0800UL, JsonObjectHeap.GlobalNullAccessCallbackContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DispatchValueConstructor_RunsHandlerAndReturnsThis()
|
||||||
|
{
|
||||||
|
JsonObjectHeap.ResetForTests();
|
||||||
|
var manager = CreateRegisteredManager();
|
||||||
|
var ctx = new CpuContext(new FakeCpuMemory(0x1_0000_0000, 0x1000), Generation.Gen5);
|
||||||
|
ctx[CpuRegister.Rdi] = 0x1_0000_0000;
|
||||||
|
|
||||||
|
Assert.True(manager.TryDispatch("qBMjqyBn3OM", ctx, out var result));
|
||||||
|
Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, result);
|
||||||
|
Assert.Equal(0x1_0000_0000UL, ctx[CpuRegister.Rax]);
|
||||||
|
Assert.Equal(JsonValueKind.Null, JsonObjectHeap.Values[0x1_0000_0000].Kind);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.Json;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Tests.Json;
|
||||||
|
|
||||||
|
// JsonObjectHeap is shared static state; both Json test classes join one collection so xUnit
|
||||||
|
// does not run them in parallel against it.
|
||||||
|
[Collection("JsonObjectHeap")]
|
||||||
|
public sealed class JsonValueExportsTests
|
||||||
|
{
|
||||||
|
private const ulong ThisAddress = 0x1_0000_0000;
|
||||||
|
private const ulong StringAddress = 0x1_0000_1000;
|
||||||
|
private const ulong TextAddress = 0x1_0000_2000;
|
||||||
|
|
||||||
|
private readonly FakeCpuMemory _memory = new(0x1_0000_0000, 0x10000);
|
||||||
|
private readonly CpuContext _ctx;
|
||||||
|
|
||||||
|
public JsonValueExportsTests()
|
||||||
|
{
|
||||||
|
JsonObjectHeap.ResetForTests();
|
||||||
|
_ctx = new CpuContext(_memory, Generation.Gen5);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValueDefaultConstructor_RegistersNullAndReturnsThis()
|
||||||
|
{
|
||||||
|
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||||
|
|
||||||
|
JsonValueExports.ValueDefaultConstructor(_ctx);
|
||||||
|
|
||||||
|
Assert.Equal(ThisAddress, _ctx[CpuRegister.Rax]);
|
||||||
|
Assert.Equal(JsonValueKind.Null, JsonObjectHeap.Values[ThisAddress].Kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0UL, false)]
|
||||||
|
[InlineData(1UL, true)]
|
||||||
|
[InlineData(0xFFFF_FF00UL, false)] // only the low byte is the bool; 0x00 low byte => false
|
||||||
|
public void ValueSetBoolean_StoresLowByte(ulong raw, bool expected)
|
||||||
|
{
|
||||||
|
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||||
|
_ctx[CpuRegister.Rsi] = raw;
|
||||||
|
|
||||||
|
JsonValueExports.ValueSetBoolean(_ctx);
|
||||||
|
|
||||||
|
var state = JsonObjectHeap.Values[ThisAddress];
|
||||||
|
Assert.Equal(JsonValueKind.Boolean, state.Kind);
|
||||||
|
Assert.Equal(expected, state.Boolean);
|
||||||
|
Assert.Equal(ThisAddress, _ctx[CpuRegister.Rax]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValueSetInteger_RoundTripsSignedValue()
|
||||||
|
{
|
||||||
|
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||||
|
_ctx[CpuRegister.Rsi] = unchecked((ulong)-42L);
|
||||||
|
|
||||||
|
JsonValueExports.ValueSetInteger(_ctx);
|
||||||
|
|
||||||
|
var state = JsonObjectHeap.Values[ThisAddress];
|
||||||
|
Assert.Equal(JsonValueKind.Integer, state.Kind);
|
||||||
|
Assert.Equal(-42L, state.Integer);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValueSetUnsigned_RoundTripsFullWidth()
|
||||||
|
{
|
||||||
|
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||||
|
_ctx[CpuRegister.Rsi] = ulong.MaxValue;
|
||||||
|
|
||||||
|
JsonValueExports.ValueSetUnsigned(_ctx);
|
||||||
|
|
||||||
|
var state = JsonObjectHeap.Values[ThisAddress];
|
||||||
|
Assert.Equal(JsonValueKind.UInteger, state.Kind);
|
||||||
|
Assert.Equal(ulong.MaxValue, state.UnsignedInteger);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValueSetReal_ReadsDoubleFromXmm0()
|
||||||
|
{
|
||||||
|
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||||
|
_ctx.SetXmmRegister(0, unchecked((ulong)BitConverter.DoubleToInt64Bits(3.14159)), 0);
|
||||||
|
|
||||||
|
JsonValueExports.ValueSetReal(_ctx);
|
||||||
|
|
||||||
|
var state = JsonObjectHeap.Values[ThisAddress];
|
||||||
|
Assert.Equal(JsonValueKind.Real, state.Kind);
|
||||||
|
Assert.Equal(3.14159, state.Real, precision: 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValueSetCString_ReadsGuestString()
|
||||||
|
{
|
||||||
|
_memory.WriteCString(TextAddress, "hello json");
|
||||||
|
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||||
|
_ctx[CpuRegister.Rsi] = TextAddress;
|
||||||
|
|
||||||
|
JsonValueExports.ValueSetCString(_ctx);
|
||||||
|
|
||||||
|
var state = JsonObjectHeap.Values[ThisAddress];
|
||||||
|
Assert.Equal(JsonValueKind.String, state.Kind);
|
||||||
|
Assert.Equal("hello json", state.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValueSetType_KeepsRawGuestEnumValue()
|
||||||
|
{
|
||||||
|
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||||
|
_ctx[CpuRegister.Rsi] = 7;
|
||||||
|
|
||||||
|
JsonValueExports.ValueSetType(_ctx);
|
||||||
|
|
||||||
|
var state = JsonObjectHeap.Values[ThisAddress];
|
||||||
|
Assert.Equal(JsonValueKind.ExplicitType, state.Kind);
|
||||||
|
Assert.Equal(7u, state.ExplicitType);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StringConstructThenValueSetString_CopiesText()
|
||||||
|
{
|
||||||
|
_memory.WriteCString(TextAddress, "from string object");
|
||||||
|
_ctx[CpuRegister.Rdi] = StringAddress;
|
||||||
|
_ctx[CpuRegister.Rsi] = TextAddress;
|
||||||
|
JsonValueExports.StringCStringConstructor(_ctx);
|
||||||
|
|
||||||
|
Assert.Equal("from string object", JsonObjectHeap.Strings[StringAddress]);
|
||||||
|
Assert.Equal(StringAddress, _ctx[CpuRegister.Rax]);
|
||||||
|
|
||||||
|
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||||
|
_ctx[CpuRegister.Rsi] = StringAddress;
|
||||||
|
JsonValueExports.ValueSetString(_ctx);
|
||||||
|
|
||||||
|
var state = JsonObjectHeap.Values[ThisAddress];
|
||||||
|
Assert.Equal(JsonValueKind.String, state.Kind);
|
||||||
|
Assert.Equal("from string object", state.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValueSetString_MissingStringShadow_DegradesToEmpty()
|
||||||
|
{
|
||||||
|
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||||
|
_ctx[CpuRegister.Rsi] = StringAddress; // never constructed
|
||||||
|
|
||||||
|
JsonValueExports.ValueSetString(_ctx);
|
||||||
|
|
||||||
|
var state = JsonObjectHeap.Values[ThisAddress];
|
||||||
|
Assert.Equal(JsonValueKind.String, state.Kind);
|
||||||
|
Assert.Equal(string.Empty, state.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Destructors_RemoveShadowState()
|
||||||
|
{
|
||||||
|
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||||
|
JsonValueExports.ValueDefaultConstructor(_ctx);
|
||||||
|
_ctx[CpuRegister.Rdi] = StringAddress;
|
||||||
|
JsonValueExports.StringDefaultConstructor(_ctx);
|
||||||
|
|
||||||
|
Assert.True(JsonObjectHeap.Values.ContainsKey(ThisAddress));
|
||||||
|
Assert.True(JsonObjectHeap.Strings.ContainsKey(StringAddress));
|
||||||
|
|
||||||
|
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||||
|
JsonValueExports.ValueDestructor(_ctx);
|
||||||
|
_ctx[CpuRegister.Rdi] = StringAddress;
|
||||||
|
JsonValueExports.StringDestructor(_ctx);
|
||||||
|
|
||||||
|
Assert.False(JsonObjectHeap.Values.ContainsKey(ThisAddress));
|
||||||
|
Assert.False(JsonObjectHeap.Strings.ContainsKey(StringAddress));
|
||||||
|
Assert.Equal(0UL, _ctx[CpuRegister.Rax]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValueSetCString_FaultingPointer_DegradesToEmptyString()
|
||||||
|
{
|
||||||
|
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||||
|
_ctx[CpuRegister.Rsi] = 0xDEAD_0000_0000; // outside the mapped region
|
||||||
|
|
||||||
|
JsonValueExports.ValueSetCString(_ctx);
|
||||||
|
|
||||||
|
var state = JsonObjectHeap.Values[ThisAddress];
|
||||||
|
Assert.Equal(JsonValueKind.String, state.Kind);
|
||||||
|
Assert.Equal(string.Empty, state.Text);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
-->
|
||||||
|
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\SharpEmu.Libs\SharpEmu.Libs.csproj" />
|
||||||
|
<ProjectReference Include="..\..\src\SharpEmu.HLE\SharpEmu.HLE.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
|
<PackageReference Include="xunit" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"dependencies": {
|
||||||
|
"net10.0": {
|
||||||
|
"Microsoft.NET.Test.Sdk": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[17.14.1, )",
|
||||||
|
"resolved": "17.14.1",
|
||||||
|
"contentHash": "HJKqKOE+vshXra2aEHpi2TlxYX7Z9VFYkr+E5rwEvHC8eIXiyO+K9kNm8vmNom3e2rA56WqxU+/N9NJlLGXsJQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.CodeCoverage": "17.14.1",
|
||||||
|
"Microsoft.TestPlatform.TestHost": "17.14.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"xunit": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[2.9.3, )",
|
||||||
|
"resolved": "2.9.3",
|
||||||
|
"contentHash": "TlXQBinK35LpOPKHAqbLY4xlEen9TBafjs0V5KnA4wZsoQLQJiirCR4CbIXvOH8NzkW4YeJKP5P/Bnrodm0h9Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"xunit.analyzers": "1.18.0",
|
||||||
|
"xunit.assert": "2.9.3",
|
||||||
|
"xunit.core": "[2.9.3]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"xunit.runner.visualstudio": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[3.1.1, )",
|
||||||
|
"resolved": "3.1.1",
|
||||||
|
"contentHash": "gNu2zhnuwjq5vQlU4S7yK/lfaKZDLmtcu+vTjnhfTlMAUYn+Hmgu8IIX0UCwWepYkk+Szx03DHx1bDnc9Fd+9w=="
|
||||||
|
},
|
||||||
|
"Microsoft.CodeCoverage": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "17.14.1",
|
||||||
|
"contentHash": "pmTrhfFIoplzFVbhVwUquT+77CbGH+h4/3mBpdmIlYtBi9nAB+kKI6dN3A/nV4DFi3wLLx/BlHIPK+MkbQ6Tpg=="
|
||||||
|
},
|
||||||
|
"Microsoft.DotNet.PlatformAbstractions": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "3.1.6",
|
||||||
|
"contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg=="
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.DependencyModel": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "9.0.9",
|
||||||
|
"contentHash": "fNGvKct2De8ghm0Bpfq0iWthtzIWabgOTi+gJhNOPhNJIowXNEUE2eZNW/zNCzrHVA3PXg2yZ+3cWZndC2IqYA=="
|
||||||
|
},
|
||||||
|
"Microsoft.TestPlatform.ObjectModel": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "17.14.1",
|
||||||
|
"contentHash": "xTP1W6Mi6SWmuxd3a+jj9G9UoC850WGwZUps1Wah9r1ZxgXhdJfj1QqDLJkFjHDCvN42qDL2Ps5KjQYWUU0zcQ=="
|
||||||
|
},
|
||||||
|
"Microsoft.TestPlatform.TestHost": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "17.14.1",
|
||||||
|
"contentHash": "d78LPzGKkJwsJXAQwsbJJ7LE7D1wB+rAyhHHAaODF+RDSQ0NgMjDFkSA1Djw18VrxO76GlKAjRUhl+H8NL8Z+Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.TestPlatform.ObjectModel": "17.14.1",
|
||||||
|
"Newtonsoft.Json": "13.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Newtonsoft.Json": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "13.0.3",
|
||||||
|
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
|
||||||
|
},
|
||||||
|
"Silk.NET.Core": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "D7AT/nnwlB+4RZ84XY8QNGBZMJI5z9l4CSSETIJ1wCfRJzRt/341y3MRZ4HbnFz4r/IGaWOEZr86iE+0/65yyQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.DotNet.PlatformAbstractions": "3.1.6",
|
||||||
|
"Microsoft.Extensions.DependencyModel": "9.0.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Silk.NET.GLFW": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "UIs4sH57xlPUNHQ/1bt9rymPWlGy8IMDCNv86h0iM4TOA1CkIx0XM/n/tA4AReh1zQkNrvkxPEdZ3Blvy1dyXg==",
|
||||||
|
"dependencies": {
|
||||||
|
"Silk.NET.Core": "2.23.0",
|
||||||
|
"Ultz.Native.GLFW": "3.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Silk.NET.Maths": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "r8PdIVzME8EH0qAgbmRPO87I4GfgR2j8TofT7EMuRJDf1QluoQwnVypDoFJjQ2ZBSRsGYk5unYxxogI05Ogsmw=="
|
||||||
|
},
|
||||||
|
"Silk.NET.Windowing.Common": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "ThStSinmY9KQI8DGiF5XEhkLJVnBcgRTBTzL9ijg1wMZAYuckz7ykrNw04fjRm2Gryh6tCNGbvz2XaY0efeFzg==",
|
||||||
|
"dependencies": {
|
||||||
|
"Silk.NET.Core": "2.23.0",
|
||||||
|
"Silk.NET.Maths": "2.23.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Silk.NET.Windowing.Glfw": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "aYBudKmENmvLRn9p15HbdvlQTnnXskcDfTfbYwSb/4fr263rGLwYuDw/txUEc2jihHJiWCp5+75Y7z5wTJWl7g==",
|
||||||
|
"dependencies": {
|
||||||
|
"Silk.NET.GLFW": "2.23.0",
|
||||||
|
"Silk.NET.Windowing.Common": "2.23.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Ultz.Native.GLFW": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "3.4.0",
|
||||||
|
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||||
|
},
|
||||||
|
"xunit.abstractions": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.0.3",
|
||||||
|
"contentHash": "pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg=="
|
||||||
|
},
|
||||||
|
"xunit.analyzers": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "1.18.0",
|
||||||
|
"contentHash": "OtFMHN8yqIcYP9wcVIgJrq01AfTxijjAqVDy/WeQVSyrDC1RzBWeQPztL49DN2syXRah8TYnfvk035s7L95EZQ=="
|
||||||
|
},
|
||||||
|
"xunit.assert": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.9.3",
|
||||||
|
"contentHash": "/Kq28fCE7MjOV42YLVRAJzRF0WmEqsmflm0cfpMjGtzQ2lR5mYVj1/i0Y8uDAOLczkL3/jArrwehfMD0YogMAA=="
|
||||||
|
},
|
||||||
|
"xunit.core": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.9.3",
|
||||||
|
"contentHash": "BiAEvqGvyme19wE0wTKdADH+NloYqikiU0mcnmiNyXaF9HyHmE6sr/3DC5vnBkgsWaE6yPyWszKSPSApWdRVeQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"xunit.extensibility.core": "[2.9.3]",
|
||||||
|
"xunit.extensibility.execution": "[2.9.3]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"xunit.extensibility.core": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.9.3",
|
||||||
|
"contentHash": "kf3si0YTn2a8J8eZNb+zFpwfoyvIrQ7ivNk5ZYA5yuYk1bEtMe4DxJ2CF/qsRgmEnDr7MnW1mxylBaHTZ4qErA==",
|
||||||
|
"dependencies": {
|
||||||
|
"xunit.abstractions": "2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"xunit.extensibility.execution": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.9.3",
|
||||||
|
"contentHash": "yMb6vMESlSrE3Wfj7V6cjQ3S4TXdXpRqYeNEI3zsX31uTsGMJjEw6oD5F5u1cHnMptjhEECnmZSsPxB6ChZHDQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"xunit.extensibility.core": "[2.9.3]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sharpemu.hle": {
|
||||||
|
"type": "Project",
|
||||||
|
"dependencies": {
|
||||||
|
"SharpEmu.Logging": "[1.0.0, )"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sharpemu.libs": {
|
||||||
|
"type": "Project",
|
||||||
|
"dependencies": {
|
||||||
|
"SharpEmu.HLE": "[1.0.0, )",
|
||||||
|
"Silk.NET.Vulkan": "[2.23.0, )",
|
||||||
|
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
|
||||||
|
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
|
||||||
|
"Silk.NET.Windowing": "[2.23.0, )"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sharpemu.logging": {
|
||||||
|
"type": "Project"
|
||||||
|
},
|
||||||
|
"Silk.NET.Vulkan": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[2.23.0, )",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "3/irtlSWXZ3eTi8N6nelI6L34NTB8ZJHpqVMNzZx2aX7Ek9YEQ34NoQW8/Tljrtmkg8KRhHW8hKTEzZaKV8PgA==",
|
||||||
|
"dependencies": {
|
||||||
|
"Silk.NET.Core": "2.23.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Silk.NET.Vulkan.Extensions.EXT": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[2.23.0, )",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==",
|
||||||
|
"dependencies": {
|
||||||
|
"Silk.NET.Core": "2.23.0",
|
||||||
|
"Silk.NET.Vulkan": "2.23.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Silk.NET.Vulkan.Extensions.KHR": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[2.23.0, )",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "uRaf4j+SmH3DumjSSSUbFg33BnsGZUyXGj93O9NgGKZSJN3OTmNmQDxRew+/KiVLcgH6qzbto8aNGZ++j9GFWg==",
|
||||||
|
"dependencies": {
|
||||||
|
"Silk.NET.Core": "2.23.0",
|
||||||
|
"Silk.NET.Vulkan": "2.23.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Silk.NET.Windowing": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[2.23.0, )",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "OPNPmt/lRyUKVYrFLQXVxyATqD3MKLc1iY1oKx1/2GppgmZxVZPwN12tekrQ4C7408kgB1L5JD1Wnirqqeb2kg==",
|
||||||
|
"dependencies": {
|
||||||
|
"Silk.NET.Windowing.Common": "2.23.0",
|
||||||
|
"Silk.NET.Windowing.Glfw": "2.23.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,412 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
// Executes the SharpEmu-emitted "exec" conformance shader on a real Vulkan
|
||||||
|
// device and compares the buffer results against CPU-computed expected values.
|
||||||
|
//
|
||||||
|
// The shader (exec-cs.spv, produced by SharpEmu.Tools.ShaderDump) was
|
||||||
|
// translated by SharpEmu from hand-assembled Gen5 instruction words and stores
|
||||||
|
// results to guestBuffers[0]:
|
||||||
|
// [0] v_fmac_f32 -> fma(1.5f, 2.25f, 10.0f)
|
||||||
|
// [1] v_mul_hi_i32 -> high 32 bits of (int)0x7FFFFFFF * (int)0x00010003
|
||||||
|
// [2] v_mul_lo_i32 -> low 32 bits of the same product
|
||||||
|
// [3] store attempted with EXEC=0 -> must NOT land (sentinel remains)
|
||||||
|
// [4] store after EXEC restored -> 1.5f (0x3FC00000)
|
||||||
|
// Every other word of the buffer must still hold the sentinel afterwards.
|
||||||
|
//
|
||||||
|
// Creating the compute pipeline doubles as a driver-acceptance check for the
|
||||||
|
// emitted SPIR-V; the dispatch then verifies the arithmetic numerically.
|
||||||
|
//
|
||||||
|
// Usage: SharpEmu.Tools.GpuConformance <path-to-exec-cs.spv>
|
||||||
|
|
||||||
|
using Silk.NET.Core.Native;
|
||||||
|
using Silk.NET.Vulkan;
|
||||||
|
|
||||||
|
const uint Sentinel = 0xCAFEBABE;
|
||||||
|
|
||||||
|
// Must match the 64-byte global-memory binding ShaderDump constructs for the
|
||||||
|
// exec program.
|
||||||
|
const ulong BufferSize = 64;
|
||||||
|
|
||||||
|
var expectedFma = BitConverter.SingleToUInt32Bits(
|
||||||
|
MathF.FusedMultiplyAdd(1.5f, 2.25f, 10.0f));
|
||||||
|
var product = (long)0x7FFFFFFF * 0x00010003;
|
||||||
|
var expectedHi = (uint)(product >> 32);
|
||||||
|
var expectedLo = (uint)product;
|
||||||
|
var expectedRestored = BitConverter.SingleToUInt32Bits(1.5f);
|
||||||
|
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
var spvPath = args.Length > 0
|
||||||
|
? args[0]
|
||||||
|
: throw new InvalidOperationException(
|
||||||
|
"usage: SharpEmu.Tools.GpuConformance <path-to-exec-cs.spv>");
|
||||||
|
var code = File.ReadAllBytes(spvPath);
|
||||||
|
|
||||||
|
var vk = Vk.GetApi();
|
||||||
|
|
||||||
|
var appName = (byte*)SilkMarshal.StringToPtr("SharpEmuGpuConformance");
|
||||||
|
var appInfo = new ApplicationInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.ApplicationInfo,
|
||||||
|
PApplicationName = appName,
|
||||||
|
ApiVersion = Vk.Version13,
|
||||||
|
};
|
||||||
|
var instanceInfo = new InstanceCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.InstanceCreateInfo,
|
||||||
|
PApplicationInfo = &appInfo,
|
||||||
|
};
|
||||||
|
Check(vk.CreateInstance(in instanceInfo, null, out var instance), "vkCreateInstance");
|
||||||
|
|
||||||
|
uint deviceCount = 0;
|
||||||
|
vk.EnumeratePhysicalDevices(instance, &deviceCount, null);
|
||||||
|
if (deviceCount == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine("no Vulkan devices found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var physicalDevices = new PhysicalDevice[deviceCount];
|
||||||
|
fixed (PhysicalDevice* pDevices = physicalDevices)
|
||||||
|
{
|
||||||
|
vk.EnumeratePhysicalDevices(instance, &deviceCount, pDevices);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer the first discrete GPU; fall back to the first device.
|
||||||
|
var physical = physicalDevices[0];
|
||||||
|
foreach (var candidate in physicalDevices)
|
||||||
|
{
|
||||||
|
vk.GetPhysicalDeviceProperties(candidate, out var props);
|
||||||
|
if (props.DeviceType == PhysicalDeviceType.DiscreteGpu)
|
||||||
|
{
|
||||||
|
physical = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vk.GetPhysicalDeviceProperties(physical, out var chosenProps);
|
||||||
|
Console.WriteLine(
|
||||||
|
$"executing on: {SilkMarshal.PtrToString((nint)chosenProps.DeviceName)}");
|
||||||
|
|
||||||
|
uint familyCount = 0;
|
||||||
|
vk.GetPhysicalDeviceQueueFamilyProperties(physical, &familyCount, null);
|
||||||
|
var families = new QueueFamilyProperties[familyCount];
|
||||||
|
fixed (QueueFamilyProperties* pFamilies = families)
|
||||||
|
{
|
||||||
|
vk.GetPhysicalDeviceQueueFamilyProperties(physical, &familyCount, pFamilies);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint? computeFamilyFound = null;
|
||||||
|
for (uint index = 0; index < familyCount; index++)
|
||||||
|
{
|
||||||
|
if (families[index].QueueFlags.HasFlag(QueueFlags.ComputeBit))
|
||||||
|
{
|
||||||
|
computeFamilyFound = index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var computeFamily = computeFamilyFound
|
||||||
|
?? throw new InvalidOperationException("device has no compute-capable queue family");
|
||||||
|
|
||||||
|
// The emitted SPIR-V declares the Int64 capability.
|
||||||
|
vk.GetPhysicalDeviceFeatures(physical, out var supportedFeatures);
|
||||||
|
if (!supportedFeatures.ShaderInt64)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"device does not support shaderInt64, which the emitted SPIR-V requires");
|
||||||
|
}
|
||||||
|
|
||||||
|
var priority = 1f;
|
||||||
|
var queueInfo = new DeviceQueueCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.DeviceQueueCreateInfo,
|
||||||
|
QueueFamilyIndex = computeFamily,
|
||||||
|
QueueCount = 1,
|
||||||
|
PQueuePriorities = &priority,
|
||||||
|
};
|
||||||
|
var features = new PhysicalDeviceFeatures { ShaderInt64 = true };
|
||||||
|
var deviceInfo = new DeviceCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.DeviceCreateInfo,
|
||||||
|
QueueCreateInfoCount = 1,
|
||||||
|
PQueueCreateInfos = &queueInfo,
|
||||||
|
PEnabledFeatures = &features,
|
||||||
|
};
|
||||||
|
Check(vk.CreateDevice(physical, in deviceInfo, null, out var device), "vkCreateDevice");
|
||||||
|
vk.GetDeviceQueue(device, computeFamily, 0, out var queue);
|
||||||
|
|
||||||
|
// Storage buffer, host-visible so the CPU can prefill and read back.
|
||||||
|
var bufferInfo = new BufferCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.BufferCreateInfo,
|
||||||
|
Size = BufferSize,
|
||||||
|
Usage = BufferUsageFlags.StorageBufferBit,
|
||||||
|
SharingMode = SharingMode.Exclusive,
|
||||||
|
};
|
||||||
|
Check(vk.CreateBuffer(device, in bufferInfo, null, out var buffer), "vkCreateBuffer");
|
||||||
|
vk.GetBufferMemoryRequirements(device, buffer, out var requirements);
|
||||||
|
vk.GetPhysicalDeviceMemoryProperties(physical, out var memoryProperties);
|
||||||
|
|
||||||
|
uint memoryType = uint.MaxValue;
|
||||||
|
for (var index = 0; index < memoryProperties.MemoryTypeCount; index++)
|
||||||
|
{
|
||||||
|
var flags = memoryProperties.MemoryTypes[index].PropertyFlags;
|
||||||
|
if ((requirements.MemoryTypeBits & (1u << index)) != 0 &&
|
||||||
|
flags.HasFlag(MemoryPropertyFlags.HostVisibleBit) &&
|
||||||
|
flags.HasFlag(MemoryPropertyFlags.HostCoherentBit))
|
||||||
|
{
|
||||||
|
memoryType = (uint)index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memoryType == uint.MaxValue)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"no host-visible, host-coherent memory type available for the readback buffer");
|
||||||
|
}
|
||||||
|
|
||||||
|
var allocateInfo = new MemoryAllocateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.MemoryAllocateInfo,
|
||||||
|
AllocationSize = requirements.Size,
|
||||||
|
MemoryTypeIndex = memoryType,
|
||||||
|
};
|
||||||
|
Check(vk.AllocateMemory(device, in allocateInfo, null, out var memory), "vkAllocateMemory");
|
||||||
|
Check(vk.BindBufferMemory(device, buffer, memory, 0), "vkBindBufferMemory");
|
||||||
|
|
||||||
|
void* mapped;
|
||||||
|
Check(vk.MapMemory(device, memory, 0, BufferSize, 0, &mapped), "vkMapMemory");
|
||||||
|
var words = (uint*)mapped;
|
||||||
|
for (var index = 0; index < (int)(BufferSize / sizeof(uint)); index++)
|
||||||
|
{
|
||||||
|
words[index] = Sentinel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SharpEmu emits all guest buffers as one descriptor array at set 0,
|
||||||
|
// binding 0; this conformance shader uses a single buffer.
|
||||||
|
ShaderModule module;
|
||||||
|
fixed (byte* pCode = code)
|
||||||
|
{
|
||||||
|
var moduleInfo = new ShaderModuleCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.ShaderModuleCreateInfo,
|
||||||
|
CodeSize = (nuint)code.Length,
|
||||||
|
PCode = (uint*)pCode,
|
||||||
|
};
|
||||||
|
Check(vk.CreateShaderModule(device, in moduleInfo, null, out module), "vkCreateShaderModule");
|
||||||
|
}
|
||||||
|
|
||||||
|
var layoutBinding = new DescriptorSetLayoutBinding
|
||||||
|
{
|
||||||
|
Binding = 0,
|
||||||
|
DescriptorType = DescriptorType.StorageBuffer,
|
||||||
|
DescriptorCount = 1,
|
||||||
|
StageFlags = ShaderStageFlags.ComputeBit,
|
||||||
|
};
|
||||||
|
var setLayoutInfo = new DescriptorSetLayoutCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.DescriptorSetLayoutCreateInfo,
|
||||||
|
BindingCount = 1,
|
||||||
|
PBindings = &layoutBinding,
|
||||||
|
};
|
||||||
|
Check(
|
||||||
|
vk.CreateDescriptorSetLayout(device, in setLayoutInfo, null, out var setLayout),
|
||||||
|
"vkCreateDescriptorSetLayout");
|
||||||
|
|
||||||
|
var pipelineLayoutInfo = new PipelineLayoutCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.PipelineLayoutCreateInfo,
|
||||||
|
SetLayoutCount = 1,
|
||||||
|
PSetLayouts = &setLayout,
|
||||||
|
};
|
||||||
|
Check(
|
||||||
|
vk.CreatePipelineLayout(device, in pipelineLayoutInfo, null, out var pipelineLayout),
|
||||||
|
"vkCreatePipelineLayout");
|
||||||
|
|
||||||
|
var entryName = (byte*)SilkMarshal.StringToPtr("main");
|
||||||
|
var pipelineInfo = new ComputePipelineCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.ComputePipelineCreateInfo,
|
||||||
|
Stage = new PipelineShaderStageCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.PipelineShaderStageCreateInfo,
|
||||||
|
Stage = ShaderStageFlags.ComputeBit,
|
||||||
|
Module = module,
|
||||||
|
PName = entryName,
|
||||||
|
},
|
||||||
|
Layout = pipelineLayout,
|
||||||
|
};
|
||||||
|
Check(
|
||||||
|
vk.CreateComputePipelines(device, default, 1, in pipelineInfo, null, out var pipeline),
|
||||||
|
"vkCreateComputePipelines");
|
||||||
|
Console.WriteLine("driver accepted the SPIR-V (pipeline created)");
|
||||||
|
|
||||||
|
var poolSize = new DescriptorPoolSize
|
||||||
|
{
|
||||||
|
Type = DescriptorType.StorageBuffer,
|
||||||
|
DescriptorCount = 1,
|
||||||
|
};
|
||||||
|
var poolInfo = new DescriptorPoolCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.DescriptorPoolCreateInfo,
|
||||||
|
MaxSets = 1,
|
||||||
|
PoolSizeCount = 1,
|
||||||
|
PPoolSizes = &poolSize,
|
||||||
|
};
|
||||||
|
Check(vk.CreateDescriptorPool(device, in poolInfo, null, out var pool), "vkCreateDescriptorPool");
|
||||||
|
|
||||||
|
var setAllocateInfo = new DescriptorSetAllocateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.DescriptorSetAllocateInfo,
|
||||||
|
DescriptorPool = pool,
|
||||||
|
DescriptorSetCount = 1,
|
||||||
|
PSetLayouts = &setLayout,
|
||||||
|
};
|
||||||
|
Check(vk.AllocateDescriptorSets(device, in setAllocateInfo, out var descriptorSet), "vkAllocateDescriptorSets");
|
||||||
|
|
||||||
|
var descriptorBuffer = new DescriptorBufferInfo
|
||||||
|
{
|
||||||
|
Buffer = buffer,
|
||||||
|
Offset = 0,
|
||||||
|
Range = BufferSize,
|
||||||
|
};
|
||||||
|
var write = new WriteDescriptorSet
|
||||||
|
{
|
||||||
|
SType = StructureType.WriteDescriptorSet,
|
||||||
|
DstSet = descriptorSet,
|
||||||
|
DstBinding = 0,
|
||||||
|
DstArrayElement = 0,
|
||||||
|
DescriptorCount = 1,
|
||||||
|
DescriptorType = DescriptorType.StorageBuffer,
|
||||||
|
PBufferInfo = &descriptorBuffer,
|
||||||
|
};
|
||||||
|
vk.UpdateDescriptorSets(device, 1, in write, 0, null);
|
||||||
|
|
||||||
|
var commandPoolInfo = new CommandPoolCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.CommandPoolCreateInfo,
|
||||||
|
QueueFamilyIndex = computeFamily,
|
||||||
|
};
|
||||||
|
Check(vk.CreateCommandPool(device, in commandPoolInfo, null, out var commandPool), "vkCreateCommandPool");
|
||||||
|
|
||||||
|
var commandBufferInfo = new CommandBufferAllocateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.CommandBufferAllocateInfo,
|
||||||
|
CommandPool = commandPool,
|
||||||
|
Level = CommandBufferLevel.Primary,
|
||||||
|
CommandBufferCount = 1,
|
||||||
|
};
|
||||||
|
Check(vk.AllocateCommandBuffers(device, in commandBufferInfo, out var commandBuffer), "vkAllocateCommandBuffers");
|
||||||
|
|
||||||
|
var beginInfo = new CommandBufferBeginInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.CommandBufferBeginInfo,
|
||||||
|
};
|
||||||
|
Check(vk.BeginCommandBuffer(commandBuffer, in beginInfo), "vkBeginCommandBuffer");
|
||||||
|
vk.CmdBindPipeline(commandBuffer, PipelineBindPoint.Compute, pipeline);
|
||||||
|
vk.CmdBindDescriptorSets(
|
||||||
|
commandBuffer,
|
||||||
|
PipelineBindPoint.Compute,
|
||||||
|
pipelineLayout,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
in descriptorSet,
|
||||||
|
0,
|
||||||
|
null);
|
||||||
|
vk.CmdDispatch(commandBuffer, 1, 1, 1);
|
||||||
|
var barrier = new MemoryBarrier
|
||||||
|
{
|
||||||
|
SType = StructureType.MemoryBarrier,
|
||||||
|
SrcAccessMask = AccessFlags.ShaderWriteBit,
|
||||||
|
DstAccessMask = AccessFlags.HostReadBit,
|
||||||
|
};
|
||||||
|
vk.CmdPipelineBarrier(
|
||||||
|
commandBuffer,
|
||||||
|
PipelineStageFlags.ComputeShaderBit,
|
||||||
|
PipelineStageFlags.HostBit,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
in barrier,
|
||||||
|
0,
|
||||||
|
null,
|
||||||
|
0,
|
||||||
|
null);
|
||||||
|
Check(vk.EndCommandBuffer(commandBuffer), "vkEndCommandBuffer");
|
||||||
|
|
||||||
|
var submitInfo = new SubmitInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.SubmitInfo,
|
||||||
|
CommandBufferCount = 1,
|
||||||
|
PCommandBuffers = &commandBuffer,
|
||||||
|
};
|
||||||
|
Check(vk.QueueSubmit(queue, 1, in submitInfo, default), "vkQueueSubmit");
|
||||||
|
Check(vk.QueueWaitIdle(queue), "vkQueueWaitIdle");
|
||||||
|
|
||||||
|
var results = new (string Name, uint Actual, uint Expected)[]
|
||||||
|
{
|
||||||
|
("v_fmac_f32 fma(1.5, 2.25, 10.0)", words[0], expectedFma),
|
||||||
|
("v_mul_hi_i32 hi(0x7FFFFFFF*0x10003)", words[1], expectedHi),
|
||||||
|
("v_mul_lo_i32 lo(0x7FFFFFFF*0x10003)", words[2], expectedLo),
|
||||||
|
("exec=0 store suppressed (offset 12 sentinel)", words[3], Sentinel),
|
||||||
|
("store after exec restore (offset 16)", words[4], expectedRestored),
|
||||||
|
};
|
||||||
|
var failures = 0;
|
||||||
|
foreach (var (name, actual, expected) in results)
|
||||||
|
{
|
||||||
|
var status = actual == expected ? "PASS" : "FAIL";
|
||||||
|
if (actual != expected)
|
||||||
|
{
|
||||||
|
failures++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"{status} {name}: gpu=0x{actual:X8} expected=0x{expected:X8}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalWords = (int)(BufferSize / sizeof(uint));
|
||||||
|
var trailingClobbered = 0;
|
||||||
|
for (var index = results.Length; index < totalWords; index++)
|
||||||
|
{
|
||||||
|
if (words[index] != Sentinel)
|
||||||
|
{
|
||||||
|
trailingClobbered++;
|
||||||
|
Console.WriteLine(
|
||||||
|
$"FAIL trailing word [{index}] clobbered: gpu=0x{words[index]:X8} expected=0x{Sentinel:X8}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
failures += trailingClobbered;
|
||||||
|
if (trailingClobbered == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine(
|
||||||
|
$"PASS trailing words [{results.Length}..{totalWords - 1}] intact (sentinel)");
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine(failures == 0
|
||||||
|
? "RESULT: all values match"
|
||||||
|
: $"RESULT: {failures} mismatch(es)");
|
||||||
|
|
||||||
|
vk.DestroyCommandPool(device, commandPool, null);
|
||||||
|
vk.DestroyDescriptorPool(device, pool, null);
|
||||||
|
vk.DestroyPipeline(device, pipeline, null);
|
||||||
|
vk.DestroyPipelineLayout(device, pipelineLayout, null);
|
||||||
|
vk.DestroyDescriptorSetLayout(device, setLayout, null);
|
||||||
|
vk.DestroyShaderModule(device, module, null);
|
||||||
|
vk.UnmapMemory(device, memory);
|
||||||
|
vk.FreeMemory(device, memory, null);
|
||||||
|
vk.DestroyBuffer(device, buffer, null);
|
||||||
|
vk.DestroyDevice(device, null);
|
||||||
|
vk.DestroyInstance(instance, null);
|
||||||
|
|
||||||
|
Environment.ExitCode = failures == 0 ? 0 : 1;
|
||||||
|
|
||||||
|
static void Check(Result result, string what)
|
||||||
|
{
|
||||||
|
if (result != Result.Success)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"{what} failed: {result}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
-->
|
||||||
|
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
<!-- Standalone dev tool: opt out of the repo-wide lock-file requirement
|
||||||
|
so no packages.lock.json is generated or committed for it. -->
|
||||||
|
<RestorePackagesWithLockFile>false</RestorePackagesWithLockFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Silk.NET.Vulkan" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,459 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
// Synthetic-shader conformance dumper.
|
||||||
|
//
|
||||||
|
// Feeds hand-assembled Gen5 (gfx10) instruction words through the real
|
||||||
|
// decode -> SPIR-V pipeline (Gen5ShaderTranslator / Gen5SpirvTranslator, via
|
||||||
|
// reflection so no emulator source changes are required) and writes the
|
||||||
|
// resulting vertex, pixel, and compute SPIR-V blobs to disk. The blobs can then be
|
||||||
|
// checked with spirv-val / spirv-dis.
|
||||||
|
//
|
||||||
|
// Programs that contain buffer_store_dword automatically get a single
|
||||||
|
// global-memory binding covering every store, which the emitter exposes as
|
||||||
|
// guestBuffers[0] (descriptor set 0, binding 0).
|
||||||
|
//
|
||||||
|
// Each program carries an expectation: ExpectTranslate=true programs must
|
||||||
|
// decode and emit the requested stages; ExpectTranslate=false programs pin a decode
|
||||||
|
// failure that must stay loud. Any unexpected outcome makes the tool exit
|
||||||
|
// non-zero, so it can gate scripts/CI.
|
||||||
|
//
|
||||||
|
// Usage: SharpEmu.Tools.ShaderDump [output-directory]
|
||||||
|
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Reflection;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.CxxAbi;
|
||||||
|
|
||||||
|
const ulong ProgramAddress = 0x100000;
|
||||||
|
|
||||||
|
(string Name, bool ExpectTranslate, uint[] Words)[] testPrograms =
|
||||||
|
[
|
||||||
|
("fmac", true, [
|
||||||
|
0x560A0501, // v_fmac_f32 v5, v1, v2
|
||||||
|
0x580A0501, 0x42280000, // v_fmamk_f32 v5, v1, 42.0, v2
|
||||||
|
0x5A0A0501, 0x42280000, // v_fmaak_f32 v5, v1, v2, 42.0
|
||||||
|
0xD52B0005, 0x00020501, // v_fmac_f32_e64 v5, v1, v2
|
||||||
|
0xBF810000, // s_endpgm
|
||||||
|
]),
|
||||||
|
("muls", true, [
|
||||||
|
0xD5690005, 0x00020501, // v_mul_lo_u32 v5, v1, v2
|
||||||
|
0xD56A0005, 0x00020501, // v_mul_hi_u32 v5, v1, v2
|
||||||
|
0xD56B0005, 0x00020501, // v_mul_lo_i32 v5, v1, v2
|
||||||
|
0xD56C0005, 0x00020501, // v_mul_hi_i32 v5, v1, v2
|
||||||
|
0xBF810000, // s_endpgm
|
||||||
|
]),
|
||||||
|
("mrt", true, [
|
||||||
|
0x7E0002FF, 0x3F800000, // v_mov_b32 v0, 1.0f
|
||||||
|
0x7E0202FF, 0x00000000, // v_mov_b32 v1, 0.0f
|
||||||
|
0x7E0402FF, 0x00000000, // v_mov_b32 v2, 0.0f
|
||||||
|
0x7E0602FF, 0x3F800000, // v_mov_b32 v3, 1.0f
|
||||||
|
0x7E0802FF, 0x00000001, // v_mov_b32 v4, 1u
|
||||||
|
0x7E0A02FF, 0x00000002, // v_mov_b32 v5, 2u
|
||||||
|
0x7E0C02FF, 0x00000003, // v_mov_b32 v6, 3u
|
||||||
|
0x7E0E02FF, 0x00000004, // v_mov_b32 v7, 4u
|
||||||
|
0x7E1002FF, 0xFFFFFFFF, // v_mov_b32 v8, -1
|
||||||
|
0x7E1202FF, 0x00000002, // v_mov_b32 v9, 2
|
||||||
|
0x7E1402FF, 0xFFFFFFFD, // v_mov_b32 v10, -3
|
||||||
|
0x7E1602FF, 0x00000004, // v_mov_b32 v11, 4
|
||||||
|
0xF800000F, 0x03020100, // exp mrt0 v0, v1, v2, v3
|
||||||
|
0xF800003F, 0x07060504, // exp mrt3 v4, v5, v6, v7
|
||||||
|
0xF800086F, 0x0B0A0908, // exp mrt6 v8, v9, v10, v11 done
|
||||||
|
0xBF810000, // s_endpgm
|
||||||
|
]),
|
||||||
|
("mrt-float2", true, [
|
||||||
|
0x7E0002FF, 0x3F800000, // v_mov_b32 v0, 1.0f
|
||||||
|
0x7E0202FF, 0x3E800000, // v_mov_b32 v1, 0.25f
|
||||||
|
0x7E0402FF, 0x3E800000, // v_mov_b32 v2, 0.25f
|
||||||
|
0x7E0602FF, 0x3F000000, // v_mov_b32 v3, 0.5f
|
||||||
|
0xF800000F, 0x03020100, // exp mrt0 v0, v1, v2, v3
|
||||||
|
0xF800081F, 0x03020100, // exp mrt1 v0, v1, v2, v3 done
|
||||||
|
0xBF810000, // s_endpgm
|
||||||
|
]),
|
||||||
|
("mrt8", true, [
|
||||||
|
0x7E0002FF, 0x3F800000, // v_mov_b32 v0, 1.0f
|
||||||
|
0x7E0202FF, 0x00000000, // v_mov_b32 v1, 0.0f
|
||||||
|
0x7E0402FF, 0x00000000, // v_mov_b32 v2, 0.0f
|
||||||
|
0x7E0602FF, 0x3F800000, // v_mov_b32 v3, 1.0f
|
||||||
|
0xF800000F, 0x03020100, // exp mrt0 v0, v1, v2, v3
|
||||||
|
0xF800001F, 0x03020100, // exp mrt1 v0, v1, v2, v3
|
||||||
|
0xF800002F, 0x03020100, // exp mrt2 v0, v1, v2, v3
|
||||||
|
0xF800003F, 0x03020100, // exp mrt3 v0, v1, v2, v3
|
||||||
|
0xF800004F, 0x03020100, // exp mrt4 v0, v1, v2, v3
|
||||||
|
0xF800005F, 0x03020100, // exp mrt5 v0, v1, v2, v3
|
||||||
|
0xF800006F, 0x03020100, // exp mrt6 v0, v1, v2, v3
|
||||||
|
0xF800087F, 0x03020100, // exp mrt7 v0, v1, v2, v3 done
|
||||||
|
0xBF810000, // s_endpgm
|
||||||
|
]),
|
||||||
|
("mrt-partial", true, [
|
||||||
|
0x7E0002FF, 0x3F4CCCCD, // v_mov_b32 v0, 0.8f
|
||||||
|
0x7E0202FF, 0x3F333333, // v_mov_b32 v1, 0.7f
|
||||||
|
0xF8000803, 0x03020100, // exp mrt0 v0, v1, off, off done
|
||||||
|
0xBF810000, // s_endpgm
|
||||||
|
]),
|
||||||
|
("mrt-partial-merge", true, [
|
||||||
|
0x7E0002FF, 0x3DCCCCCD, // v_mov_b32 v0, 0.1f
|
||||||
|
0x7E0202FF, 0x3E4CCCCD, // v_mov_b32 v1, 0.2f
|
||||||
|
0x7E0C02FF, 0x3E99999A, // v_mov_b32 v6, 0.3f
|
||||||
|
0x7E0E02FF, 0x3ECCCCCD, // v_mov_b32 v7, 0.4f
|
||||||
|
0xF8000003, 0x03020100, // exp mrt0 v0, v1, off, off
|
||||||
|
0xF800080C, 0x07060504, // exp mrt0 off, off, v6, v7 done
|
||||||
|
0xBF810000, // s_endpgm
|
||||||
|
]),
|
||||||
|
("sopp-hints", true, [
|
||||||
|
0xBFA10001, // s_clause 0x1
|
||||||
|
0xBFA30000, // s_waitcnt_depctr 0x0
|
||||||
|
0xBF810000, // s_endpgm
|
||||||
|
]),
|
||||||
|
// s_round_mode / s_denorm_mode write the FP MODE state and must keep
|
||||||
|
// failing decode loudly until their semantics are modeled (see #108);
|
||||||
|
// this program pins that behavior.
|
||||||
|
("sopp-mode", false, [
|
||||||
|
0xBFA40000, // s_round_mode 0x0
|
||||||
|
0xBFA50000, // s_denorm_mode 0x0
|
||||||
|
0xBF810000, // s_endpgm
|
||||||
|
]),
|
||||||
|
// Executable end-to-end test: compute with real ALU instructions, then
|
||||||
|
// buffer_store_dword results to guestBuffers[0] at offsets 0/4/8, prove
|
||||||
|
// that a store with EXEC=0 does not land (offset 12 stays sentinel), and
|
||||||
|
// that stores work again after EXEC is restored (offset 16).
|
||||||
|
("exec", true, [
|
||||||
|
0xBFA10001, // s_clause 0x1 (hint no-op in an executed program, needs #108)
|
||||||
|
0x7E0002FF, 0x3FC00000, // v_mov_b32 v0, 1.5f
|
||||||
|
0x7E0202FF, 0x40100000, // v_mov_b32 v1, 2.25f
|
||||||
|
0x7E0402FF, 0x41200000, // v_mov_b32 v2, 10.0f
|
||||||
|
0x56040300, // v_fmac_f32 v2, v0, v1 -> v2 = fma(1.5, 2.25, 10.0)
|
||||||
|
0x7E0602FF, 0x7FFFFFFF, // v_mov_b32 v3, 0x7FFFFFFF
|
||||||
|
0x7E0802FF, 0x00010003, // v_mov_b32 v4, 0x00010003
|
||||||
|
0xD56C0005, 0x00020903, // v_mul_hi_i32 v5, v3, v4
|
||||||
|
0xD56B0006, 0x00020903, // v_mul_lo_i32 v6, v3, v4
|
||||||
|
0xE0700000, 0x80020200, // buffer_store_dword v2, off, s[8:11], 0
|
||||||
|
0xE0700004, 0x80020500, // buffer_store_dword v5, off, s[8:11], 0 offset:4
|
||||||
|
0xE0700008, 0x80020600, // buffer_store_dword v6, off, s[8:11], 0 offset:8
|
||||||
|
0xBEFE0380, // s_mov_b32 exec_lo, 0 -> lane inactive
|
||||||
|
0xE070000C, 0x80020200, // buffer_store_dword v2, off, s[8:11], 0 offset:12 (masked, must not land)
|
||||||
|
0xBEFE03C1, // s_mov_b32 exec_lo, -1 -> lane active again
|
||||||
|
0xE0700010, 0x80020000, // buffer_store_dword v0, off, s[8:11], 0 offset:16
|
||||||
|
0xBF810000, // s_endpgm
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
|
||||||
|
var assembly = typeof(CxaGuardExports).Assembly;
|
||||||
|
var shaderTranslator = assembly.GetType("SharpEmu.Libs.Agc.Gen5ShaderTranslator")
|
||||||
|
?? throw new InvalidOperationException("Gen5ShaderTranslator not found");
|
||||||
|
var spirvTranslator = assembly.GetType("SharpEmu.Libs.Agc.Gen5SpirvTranslator")
|
||||||
|
?? throw new InvalidOperationException("Gen5SpirvTranslator not found");
|
||||||
|
var describe = shaderTranslator.GetMethod(
|
||||||
|
"Describe",
|
||||||
|
BindingFlags.Public | BindingFlags.Static)
|
||||||
|
?? throw new InvalidOperationException("Gen5ShaderTranslator.Describe not found");
|
||||||
|
var tryDecode = shaderTranslator.GetMethod(
|
||||||
|
"TryDecodeProgram",
|
||||||
|
BindingFlags.NonPublic | BindingFlags.Static)
|
||||||
|
?? throw new InvalidOperationException("Gen5ShaderTranslator.TryDecodeProgram not found");
|
||||||
|
var stateType = assembly.GetType("SharpEmu.Libs.Agc.Gen5ShaderState")
|
||||||
|
?? throw new InvalidOperationException("Gen5ShaderState not found");
|
||||||
|
var evaluationType = assembly.GetType("SharpEmu.Libs.Agc.Gen5ShaderEvaluation")
|
||||||
|
?? throw new InvalidOperationException("Gen5ShaderEvaluation not found");
|
||||||
|
var imageBindingType = assembly.GetType("SharpEmu.Libs.Agc.Gen5ImageBinding")
|
||||||
|
?? throw new InvalidOperationException("Gen5ImageBinding not found");
|
||||||
|
var globalBindingType = assembly.GetType("SharpEmu.Libs.Agc.Gen5GlobalMemoryBinding")
|
||||||
|
?? throw new InvalidOperationException("Gen5GlobalMemoryBinding not found");
|
||||||
|
var pixelOutputBindingType = assembly.GetType("SharpEmu.Libs.Agc.Gen5PixelOutputBinding")
|
||||||
|
?? throw new InvalidOperationException("Gen5PixelOutputBinding not found");
|
||||||
|
var pixelOutputKindType = assembly.GetType("SharpEmu.Libs.Agc.Gen5PixelOutputKind")
|
||||||
|
?? throw new InvalidOperationException("Gen5PixelOutputKind not found");
|
||||||
|
var tryCompile = spirvTranslator.GetMethod(
|
||||||
|
"TryCompileVertexShader",
|
||||||
|
BindingFlags.Public | BindingFlags.Static)
|
||||||
|
?? throw new InvalidOperationException("Gen5SpirvTranslator.TryCompileVertexShader not found");
|
||||||
|
var tryCompilePixel = spirvTranslator.GetMethods(BindingFlags.Public | BindingFlags.Static)
|
||||||
|
.Single(method =>
|
||||||
|
method.Name == "TryCompilePixelShader" &&
|
||||||
|
method.GetParameters()[2].ParameterType.IsGenericType);
|
||||||
|
var tryCompileCompute = spirvTranslator.GetMethod(
|
||||||
|
"TryCompileComputeShader",
|
||||||
|
BindingFlags.Public | BindingFlags.Static)
|
||||||
|
?? throw new InvalidOperationException("Gen5SpirvTranslator.TryCompileComputeShader not found");
|
||||||
|
|
||||||
|
var outputDirectory = args.Length > 0
|
||||||
|
? args[0]
|
||||||
|
: Path.Combine(AppContext.BaseDirectory, "spv");
|
||||||
|
Directory.CreateDirectory(outputDirectory);
|
||||||
|
|
||||||
|
var failures = 0;
|
||||||
|
foreach (var (name, expectTranslate, words) in testPrograms)
|
||||||
|
{
|
||||||
|
var memory = new FakeMemory();
|
||||||
|
memory.AddRegion(ProgramAddress, words);
|
||||||
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||||
|
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[{name}] decode: " +
|
||||||
|
(string)describe.Invoke(null, [ctx, ProgramAddress, ProgramAddress])!);
|
||||||
|
|
||||||
|
object?[] decodeArgs = [ctx, ProgramAddress, null, null];
|
||||||
|
if (!(bool)tryDecode.Invoke(null, decodeArgs)!)
|
||||||
|
{
|
||||||
|
if (expectTranslate)
|
||||||
|
{
|
||||||
|
failures++;
|
||||||
|
Console.WriteLine($"[{name}] FAILED: decode error ({decodeArgs[3]})");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[{name}] decode failed as expected ({decodeArgs[3]})");
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!expectTranslate)
|
||||||
|
{
|
||||||
|
failures++;
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[{name}] FAILED: decoded successfully but is pinned as a decode failure — " +
|
||||||
|
"if the new decode support is intentional, its semantics need verifying here first");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer stores need a global-memory binding; the emitter resolves them by
|
||||||
|
// instruction PC, so collect store PCs from the decoded program itself.
|
||||||
|
var programObj = decodeArgs[2]!;
|
||||||
|
var instructions = (System.Collections.IEnumerable)programObj
|
||||||
|
.GetType().GetProperty("Instructions")!.GetValue(programObj)!;
|
||||||
|
var storePcs = new List<uint>();
|
||||||
|
foreach (var instruction in instructions)
|
||||||
|
{
|
||||||
|
var op = (string)instruction.GetType().GetProperty("Opcode")!.GetValue(instruction)!;
|
||||||
|
if (op.StartsWith("BufferStore", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
storePcs.Add((uint)instruction.GetType().GetProperty("Pc")!.GetValue(instruction)!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The binding's scalar base (8 -> s[8:11]) must match the srsrc field of
|
||||||
|
// the hand-assembled buffer_store words, and the 64-byte backing store
|
||||||
|
// must cover every hand-assembled store offset.
|
||||||
|
var globalBindings = Array.CreateInstance(globalBindingType, storePcs.Count > 0 ? 1 : 0);
|
||||||
|
if (storePcs.Count > 0)
|
||||||
|
{
|
||||||
|
globalBindings.SetValue(
|
||||||
|
Activator.CreateInstance(
|
||||||
|
globalBindingType,
|
||||||
|
8u,
|
||||||
|
0UL,
|
||||||
|
(IReadOnlyList<uint>)storePcs,
|
||||||
|
new byte[64]),
|
||||||
|
0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var state = Activator.CreateInstance(
|
||||||
|
stateType,
|
||||||
|
programObj,
|
||||||
|
new uint[16],
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
0u)!;
|
||||||
|
var evaluation = Activator.CreateInstance(
|
||||||
|
evaluationType,
|
||||||
|
new uint[256],
|
||||||
|
new uint[256],
|
||||||
|
new Dictionary<uint, IReadOnlyList<uint>>(),
|
||||||
|
Array.CreateInstance(imageBindingType, 0),
|
||||||
|
globalBindings,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null)!;
|
||||||
|
|
||||||
|
var compileArgs = PadWithDefaults(tryCompile, [state, evaluation, null, null]);
|
||||||
|
if ((bool)tryCompile.Invoke(null, BindingFlags.OptionalParamBinding, null, compileArgs, null)!)
|
||||||
|
{
|
||||||
|
var shader = compileArgs[2]!;
|
||||||
|
var spirv = (byte[])shader.GetType().GetProperty("Spirv")!.GetValue(shader)!;
|
||||||
|
var path = Path.Combine(outputDirectory, $"{name}.spv");
|
||||||
|
File.WriteAllBytes(path, spirv);
|
||||||
|
Console.WriteLine($"[{name}] emit: success, {spirv.Length} bytes -> {path}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
failures++;
|
||||||
|
Console.WriteLine($"[{name}] emit: FAILED ({compileArgs[3]})");
|
||||||
|
}
|
||||||
|
|
||||||
|
var computeArgs = PadWithDefaults(tryCompileCompute, [state, evaluation, 1u, 1u, 1u, null, null]);
|
||||||
|
if ((bool)tryCompileCompute.Invoke(null, BindingFlags.OptionalParamBinding, null, computeArgs, null)!)
|
||||||
|
{
|
||||||
|
var shader = computeArgs[5]!;
|
||||||
|
var spirv = (byte[])shader.GetType().GetProperty("Spirv")!.GetValue(shader)!;
|
||||||
|
var path = Path.Combine(outputDirectory, $"{name}-cs.spv");
|
||||||
|
File.WriteAllBytes(path, spirv);
|
||||||
|
Console.WriteLine($"[{name}] compute emit: success, {spirv.Length} bytes -> {path}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
failures++;
|
||||||
|
Console.WriteLine($"[{name}] compute emit: FAILED ({computeArgs[6]})");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.StartsWith("mrt", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
(uint GuestSlot, uint HostLocation, string Kind)[] outputSpecs = name switch
|
||||||
|
{
|
||||||
|
"mrt" => new (uint GuestSlot, uint HostLocation, string Kind)[]
|
||||||
|
{
|
||||||
|
(0, 0, "Float"),
|
||||||
|
(3, 1, "Uint"),
|
||||||
|
(6, 2, "Sint"),
|
||||||
|
},
|
||||||
|
"mrt-float2" => [(0, 0, "Float"), (1, 1, "Float")],
|
||||||
|
"mrt8" => Enumerable.Range(0, 8)
|
||||||
|
.Select(index => ((uint)index, (uint)index, "Float"))
|
||||||
|
.ToArray(),
|
||||||
|
_ => [(0, 0, "Float")],
|
||||||
|
};
|
||||||
|
var pixelOutputs = Array.CreateInstance(pixelOutputBindingType, outputSpecs.Length);
|
||||||
|
for (var index = 0; index < outputSpecs.Length; index++)
|
||||||
|
{
|
||||||
|
var spec = outputSpecs[index];
|
||||||
|
pixelOutputs.SetValue(
|
||||||
|
Activator.CreateInstance(
|
||||||
|
pixelOutputBindingType,
|
||||||
|
spec.GuestSlot,
|
||||||
|
spec.HostLocation,
|
||||||
|
Enum.Parse(pixelOutputKindType, spec.Kind)),
|
||||||
|
index);
|
||||||
|
}
|
||||||
|
|
||||||
|
var pixelArgs = PadWithDefaults(
|
||||||
|
tryCompilePixel,
|
||||||
|
[state, evaluation, pixelOutputs, null, null]);
|
||||||
|
if ((bool)tryCompilePixel.Invoke(
|
||||||
|
null,
|
||||||
|
BindingFlags.OptionalParamBinding,
|
||||||
|
null,
|
||||||
|
pixelArgs,
|
||||||
|
null)!)
|
||||||
|
{
|
||||||
|
var shader = pixelArgs[3]!;
|
||||||
|
var spirv = (byte[])shader.GetType().GetProperty("Spirv")!.GetValue(shader)!;
|
||||||
|
var path = Path.Combine(outputDirectory, $"{name}-ps.spv");
|
||||||
|
File.WriteAllBytes(path, spirv);
|
||||||
|
Console.WriteLine($"[{name}] pixel emit: success, {spirv.Length} bytes -> {path}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
failures++;
|
||||||
|
Console.WriteLine($"[{name}] pixel emit: FAILED ({pixelArgs[4]})");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name == "mrt")
|
||||||
|
{
|
||||||
|
var invalidOutputs = Array.CreateInstance(pixelOutputBindingType, 2);
|
||||||
|
invalidOutputs.SetValue(
|
||||||
|
Activator.CreateInstance(
|
||||||
|
pixelOutputBindingType,
|
||||||
|
0u,
|
||||||
|
0u,
|
||||||
|
Enum.Parse(pixelOutputKindType, "Float")),
|
||||||
|
0);
|
||||||
|
invalidOutputs.SetValue(
|
||||||
|
Activator.CreateInstance(
|
||||||
|
pixelOutputBindingType,
|
||||||
|
3u,
|
||||||
|
7u,
|
||||||
|
Enum.Parse(pixelOutputKindType, "Float")),
|
||||||
|
1);
|
||||||
|
var invalidPixelArgs = PadWithDefaults(
|
||||||
|
tryCompilePixel,
|
||||||
|
[state, evaluation, invalidOutputs, null, null]);
|
||||||
|
if ((bool)tryCompilePixel.Invoke(
|
||||||
|
null,
|
||||||
|
BindingFlags.OptionalParamBinding,
|
||||||
|
null,
|
||||||
|
invalidPixelArgs,
|
||||||
|
null)!)
|
||||||
|
{
|
||||||
|
failures++;
|
||||||
|
Console.WriteLine("[mrt] FAILED: sparse host locations were accepted");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[mrt] sparse host locations rejected as expected ({invalidPixelArgs[4]})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine(failures == 0
|
||||||
|
? "RESULT: all programs behaved as expected"
|
||||||
|
: $"RESULT: {failures} unexpected outcome(s)");
|
||||||
|
Environment.ExitCode = failures == 0 ? 0 : 1;
|
||||||
|
|
||||||
|
// Reflection Invoke does not apply C# default parameter values, so a newly
|
||||||
|
// added optional parameter on a translator entry point would otherwise throw
|
||||||
|
// TargetParameterCountException. Type.Missing + OptionalParamBinding lets the
|
||||||
|
// runtime substitute the declared defaults; only a new *required* parameter
|
||||||
|
// should force a tool update.
|
||||||
|
static object?[] PadWithDefaults(MethodInfo method, object?[] arguments)
|
||||||
|
{
|
||||||
|
var parameters = method.GetParameters();
|
||||||
|
if (arguments.Length > parameters.Length)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"{method.DeclaringType?.Name}.{method.Name} takes fewer parameters than the tool supplies");
|
||||||
|
}
|
||||||
|
|
||||||
|
var padded = new object?[parameters.Length];
|
||||||
|
arguments.CopyTo(padded, 0);
|
||||||
|
for (var i = arguments.Length; i < padded.Length; i++)
|
||||||
|
{
|
||||||
|
if (!parameters[i].IsOptional)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"{method.DeclaringType?.Name}.{method.Name} gained a required parameter " +
|
||||||
|
$"'{parameters[i].Name}' — the tool needs updating");
|
||||||
|
}
|
||||||
|
|
||||||
|
padded[i] = Type.Missing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return padded;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class FakeMemory : ICpuMemory
|
||||||
|
{
|
||||||
|
private readonly List<(ulong Base, byte[] Data)> _regions = [];
|
||||||
|
|
||||||
|
public void AddRegion(ulong baseAddress, uint[] words)
|
||||||
|
{
|
||||||
|
var bytes = new byte[words.Length * sizeof(uint)];
|
||||||
|
for (var index = 0; index < words.Length; index++)
|
||||||
|
{
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(
|
||||||
|
bytes.AsSpan(index * sizeof(uint)),
|
||||||
|
words[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
_regions.Add((baseAddress, bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryRead(ulong virtualAddress, Span<byte> destination)
|
||||||
|
{
|
||||||
|
foreach (var (baseAddress, data) in _regions)
|
||||||
|
{
|
||||||
|
if (virtualAddress >= baseAddress &&
|
||||||
|
virtualAddress + (ulong)destination.Length <= baseAddress + (ulong)data.Length)
|
||||||
|
{
|
||||||
|
data.AsSpan(
|
||||||
|
(int)(virtualAddress - baseAddress),
|
||||||
|
destination.Length).CopyTo(destination);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source) => false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
-->
|
||||||
|
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<!-- Standalone dev tool: opt out of the repo-wide lock-file requirement
|
||||||
|
so no packages.lock.json is generated or committed for it. -->
|
||||||
|
<RestorePackagesWithLockFile>false</RestorePackagesWithLockFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\SharpEmu.Libs\SharpEmu.Libs.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Reference in New Issue
Block a user