Compare commits

..

3 Commits

Author SHA1 Message Date
ParantezTech ddfda7e220 [log] log to file for CLI and fix some issues 2026-07-13 14:30:30 +03:00
ParantezTech af106d4b38 reuse 2026-07-13 14:11:22 +03:00
ParantezTech 1dafc306b5 [emulator] Fix mitigated child process handling 2026-07-13 14:06:31 +03:00
150 changed files with 2664 additions and 21907 deletions
-3
View File
@@ -100,9 +100,6 @@ jobs:
- name: Build solution
run: dotnet build SharpEmu.slnx -c Release --no-restore
- name: Validate synthetic shaders
run: dotnet run --project tools/SharpEmu.Tools.ShaderDump/SharpEmu.Tools.ShaderDump.csproj -c Release -- artifacts/shader-dump
- name: Publish win-x64 CLI
run: dotnet publish src/SharpEmu.CLI/SharpEmu.CLI.csproj -c Release -r win-x64 --self-contained true --no-restore -p:PublishDir="${env:PUBLISH_DIR}"
-3
View File
@@ -12,14 +12,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.3.18" />
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.3.18" />
<PackageVersion Include="Iced" Version="1.21.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageVersion Include="Silk.NET.Vulkan" Version="2.23.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.23.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.23.0" />
<PackageVersion Include="Silk.NET.Windowing" Version="2.23.0" />
<!-- Transitive of Avalonia.Desktop; pinned to fix GHSA-xrw6-gwf8-vvr9 -->
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.21.3" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.1" />
</ItemGroup>
</Project>
-1
View File
@@ -8,7 +8,6 @@ path = [
"**/packages.lock.json",
"scripts/ps5_names.txt",
"src/SharpEmu.HLE/Aerolib/aerolib.bin",
"src/SharpEmu.GUI/Languages/**",
"_logs/**",
".github/images/**",
"assets/images/**"
-3
View File
@@ -12,7 +12,4 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Project Path="src/SharpEmu.Libs/SharpEmu.Libs.csproj" />
<Project Path="src/SharpEmu.Logging/SharpEmu.Logging.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/SharpEmu.Libs.Tests/SharpEmu.Libs.Tests.csproj" />
</Folder>
</Solution>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 698 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 802 B

-172
View File
@@ -1,172 +0,0 @@
# 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())
+6 -7
View File
@@ -3,8 +3,8 @@
#!/usr/bin/env python3
import hashlib
import struct
import hashlib
from base64 import b64encode as base64enc
from binascii import unhexlify as uhx
from pathlib import Path
@@ -21,7 +21,7 @@ def name2nid(name):
def generate():
names_path = Path(NAMES)
output_path = Path(OUTPUT)
entries = []
with open(names_path, 'r', encoding='utf-8') as f:
for line in f:
@@ -29,12 +29,12 @@ def generate():
if name:
nid = name2nid(name)
entries.append((nid, name))
print(f"Found {len(entries)} entries")
data = bytearray()
data.extend(struct.pack('<I', len(entries)))
for nid, name in entries:
nid_bytes = nid.encode('utf-8')
name_bytes = name.encode('utf-8')
@@ -42,11 +42,10 @@ def generate():
data.extend(nid_bytes)
data.extend(struct.pack('<H', len(name_bytes)))
data.extend(name_bytes)
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, 'wb') as f:
f.write(data)
print(f"Generated: {output_path} ({len(data):,} bytes)")
print(f"Total entries: {len(entries)}")
File diff suppressed because it is too large Load Diff
-17
View File
@@ -5,7 +5,6 @@ using SharpEmu.Core.Runtime;
using SharpEmu.Core.Cpu;
using SharpEmu.GUI;
using SharpEmu.HLE;
using SharpEmu.Libs.VideoOut;
using SharpEmu.Logging;
using System.Runtime.InteropServices;
using System.Text;
@@ -96,7 +95,6 @@ internal static partial class Program
SharpEmuLog.MinimumLevel = logLevel;
Log.Info(BuildInfo.Banner);
Log.Info(HostSystemInfo.Summary);
ebootPath = Path.GetFullPath(ebootPath);
Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}");
@@ -112,16 +110,8 @@ internal static partial class Program
using var runtime = SharpEmuRuntime.CreateDefault(runtimeOptions);
OrbisGen2Result result;
ConsoleCancelEventHandler? cancelHandler = null;
try
{
cancelHandler = (_, eventArgs) =>
{
eventArgs.Cancel = true;
VideoOutExports.NotifyHostInterrupt();
};
Console.CancelKeyPress += cancelHandler;
Console.Error.WriteLine($"[DEBUG] Running: {ebootPath}");
result = runtime.Run(ebootPath);
Console.Error.WriteLine($"[DEBUG] Result: {result}");
@@ -132,13 +122,6 @@ internal static partial class Program
Log.Error("SharpEmu failed to run.", ex);
return 3;
}
finally
{
if (cancelHandler is not null)
{
Console.CancelKeyPress -= cancelHandler;
}
}
Log.Info($"SharpEmu execution completed. Result={result} (0x{(int)result:X8})");
if (!string.IsNullOrWhiteSpace(runtime.LastSessionSummary))
-8
View File
@@ -24,9 +24,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Version>0.0.1</Version>
<ServerGarbageCollection>true</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
<TieredPGO>true</TieredPGO>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
@@ -51,11 +48,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
<TargetPath>LICENSE.txt</TargetPath>
<Visible>False</Visible>
</Content>
<Content Include="..\SharpEmu.GUI\Languages\*.json" Condition="'$(Configuration)' != 'Release'">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<TargetPath>Languages\%(Filename)%(Extension)</TargetPath>
<Visible>False</Visible>
</Content>
</ItemGroup>
<!-- Keep glfw as a loose file next to the executable; every other native
+2 -6
View File
@@ -7,7 +7,6 @@ using SharpEmu.Core.Cpu.Native;
using SharpEmu.Core.Loader;
using SharpEmu.Core.Memory;
using SharpEmu.HLE;
using SharpEmu.HLE.Host;
using SharpEmu.Logging;
namespace SharpEmu.Core.Cpu;
@@ -42,19 +41,16 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
];
private readonly IVirtualMemory _virtualMemory;
private readonly IModuleManager _moduleManager;
private readonly IHostPlatform? _hostPlatform;
private INativeCpuBackend? _nativeCpuBackend;
public CpuDispatcher(
IVirtualMemory virtualMemory,
IModuleManager moduleManager,
INativeCpuBackend? nativeCpuBackend = null,
IHostPlatform? hostPlatform = null)
INativeCpuBackend? nativeCpuBackend = null)
{
_virtualMemory = virtualMemory ?? throw new ArgumentNullException(nameof(virtualMemory));
_moduleManager = moduleManager ?? throw new ArgumentNullException(nameof(moduleManager));
_nativeCpuBackend = nativeCpuBackend;
_hostPlatform = hostPlatform;
}
public ulong? LastEntryPoint { get; private set; }
@@ -270,7 +266,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
entryFrameDiagnostic,
Environment.NewLine,
"CpuEngine: native-only");
_nativeCpuBackend ??= new DirectExecutionBackend(_moduleManager, _hostPlatform);
_nativeCpuBackend ??= new DirectExecutionBackend(_moduleManager);
if (_nativeCpuBackend.TryExecute(
context,
entryPoint,
@@ -8,7 +8,6 @@ using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.HLE;
using SharpEmu.HLE.Host;
using SharpEmu.Logging;
namespace SharpEmu.Core.Cpu.Native;
@@ -135,9 +134,8 @@ public sealed partial class DirectExecutionBackend
int num2 = 0;
List<ulong> list = new List<ulong>(16);
ulong num3 = scanStart;
var hostMemory = ResolveDiagnosticsHostMemory();
HostRegionInfo lpBuffer;
while (num3 < scanEnd && hostMemory.Query(num3, out lpBuffer))
MEMORY_BASIC_INFORMATION64 lpBuffer;
while (num3 < scanEnd && VirtualQuery((void*)num3, out lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) != 0)
{
ulong baseAddress = lpBuffer.BaseAddress;
ulong num4 = baseAddress + lpBuffer.RegionSize;
@@ -147,7 +145,7 @@ public sealed partial class DirectExecutionBackend
}
ulong value = Math.Max(num3, baseAddress);
ulong num5 = Math.Min(num4, scanEnd);
if (lpBuffer.State == HostRegionState.Committed && IsReadableProtection(lpBuffer.RawProtection) && !IsExecutableProtection(lpBuffer.RawProtection))
if (lpBuffer.State == 4096 && IsReadableProtection(lpBuffer.Protect) && !IsExecutableProtection(lpBuffer.Protect))
{
ulong num6 = AlignUp(value, 8uL);
for (ulong num7 = num6; num7 + 8 <= num5; num7 += 8)
@@ -352,7 +350,7 @@ public sealed partial class DirectExecutionBackend
{
return false;
}
if (!ResolveDiagnosticsHostMemory().Query(address, out var lpBuffer))
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
{
return false;
}
@@ -361,7 +359,7 @@ public sealed partial class DirectExecutionBackend
{
return false;
}
if (lpBuffer.State != HostRegionState.Committed || !IsReadableProtection(lpBuffer.RawProtection))
if (lpBuffer.State != 4096 || !IsReadableProtection(lpBuffer.Protect))
{
return false;
}
@@ -393,12 +391,12 @@ public sealed partial class DirectExecutionBackend
return true;
}
if (!ResolveDiagnosticsHostMemory().Query(address, out var lpBuffer))
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
{
return false;
}
var executable = lpBuffer.State == HostRegionState.Committed && IsExecutableProtection(lpBuffer.RawProtection);
var executable = lpBuffer.State == 4096 && IsExecutableProtection(lpBuffer.Protect);
if (executable)
{
_knownExecutablePages.TryAdd(pageAddress, 0);
@@ -417,14 +415,6 @@ public sealed partial class DirectExecutionBackend
return (value + num) & ~num;
}
// Diagnostics helpers are static (reachable from static handler paths), so
// they use the platform injected into the backend active on this thread and
// fall back to the process-wide singleton only when no run is bound.
private static IHostMemory ResolveDiagnosticsHostMemory()
{
return _activeExecutionBackend?._hostMemory ?? HostPlatform.Current.Memory;
}
private static bool IsReadableProtection(uint protect)
{
if ((protect & 0x100) != 0 || (protect & 1) != 0)
@@ -9,9 +9,7 @@ using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.Core.Cpu.Disasm;
using SharpEmu.Core.Cpu.Native.Windows;
using SharpEmu.HLE;
using SharpEmu.HLE.Host;
namespace SharpEmu.Core.Cpu.Native;
@@ -24,12 +22,12 @@ public sealed partial class DirectExecutionBackend
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_RAW_HANDLER"), "1", StringComparison.Ordinal))
{
_rawExceptionHandlerStub = _faultHandling.CreateHandlerThunk(RawVectoredHandlerPtrManaged, _hostRspSlotTlsIndex, _tlsGetValueAddress);
_rawExceptionHandlerStub = CreateExceptionHandlerTrampoline(RawVectoredHandlerPtrManaged);
if (_rawExceptionHandlerStub == 0)
{
throw new InvalidOperationException("Failed to create raw exception handler trampoline");
}
_rawExceptionHandler = _faultHandling.AddFirstChanceHandler(_rawExceptionHandlerStub);
_rawExceptionHandler = (nint)AddVectoredExceptionHandler(1u, _rawExceptionHandlerStub);
Console.Error.WriteLine($"[LOADER][INFO] Raw exception handler installed: 0x{_rawExceptionHandler:X16}");
}
else
@@ -39,22 +37,22 @@ public sealed partial class DirectExecutionBackend
_handlerDelegate = VectoredHandler;
_handlerHandle = GCHandle.Alloc(_handlerDelegate);
_exceptionHandlerStub = _faultHandling.CreateHandlerThunk(Marshal.GetFunctionPointerForDelegate(_handlerDelegate), _hostRspSlotTlsIndex, _tlsGetValueAddress);
_exceptionHandlerStub = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_handlerDelegate));
if (_exceptionHandlerStub == 0)
{
throw new InvalidOperationException("Failed to create exception handler trampoline");
}
_exceptionHandler = _faultHandling.AddFirstChanceHandler(_exceptionHandlerStub);
_exceptionHandler = (nint)AddVectoredExceptionHandler(1u, _exceptionHandlerStub);
Console.Error.WriteLine($"[LOADER][INFO] Exception handler installed: 0x{_exceptionHandler:X16}");
_unhandledFilterDelegate = UnhandledExceptionFilter;
_unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate);
_unhandledFilterStub = _faultHandling.CreateHandlerThunk(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate), _hostRspSlotTlsIndex, _tlsGetValueAddress);
_unhandledFilterStub = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate));
if (_unhandledFilterStub == 0)
{
throw new InvalidOperationException("Failed to create unhandled exception filter trampoline");
}
_faultHandling.SetUnhandledFilter(_unhandledFilterStub);
SetUnhandledExceptionFilter(_unhandledFilterStub);
}
private unsafe int UnhandledExceptionFilter(void* exceptionInfo)
@@ -62,8 +60,8 @@ public sealed partial class DirectExecutionBackend
try
{
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
ulong rip = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, CTX_RIP);
ulong rsp = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, CTX_RSP);
ulong rip = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, 248);
ulong rsp = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, 152);
Console.Error.WriteLine("[LOADER][FATAL] Unhandled exception filter fired.");
Console.Error.WriteLine($"[LOADER][FATAL] Code: 0x{exceptionRecord->ExceptionCode:X8}");
Console.Error.WriteLine($"[LOADER][FATAL] Exception Address: 0x{(ulong)(nint)exceptionRecord->ExceptionAddress:X16}");
@@ -102,8 +100,8 @@ public sealed partial class DirectExecutionBackend
return 0;
}
ulong rip = ReadCtxU64(contextRecord, CTX_RIP);
ulong rsp = ReadCtxU64(contextRecord, CTX_RSP);
ulong rip = ReadCtxU64(contextRecord, 248);
ulong rsp = ReadCtxU64(contextRecord, 152);
// Thread-mode probe: a hardware exception raised while this thread is inside
// the managed import gateway means the VEH->managed reentry happened from
@@ -114,7 +112,7 @@ public sealed partial class DirectExecutionBackend
$"veh_in_gateway code=0x{exceptionCode:X8} rip=0x{rip:X16} gateway_depth={_threadModeGatewayDepth}");
}
if (exceptionCode == WindowsFaultCodes.AccessViolation && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
{
return -1;
}
@@ -129,10 +127,10 @@ public sealed partial class DirectExecutionBackend
switch (exceptionCode)
{
case WindowsFaultCodes.AccessViolation:
case 3221225477u:
LogAccessViolationTrace(exceptionAddress, exceptionRecord);
break;
case WindowsFaultCodes.FastFail:
case 3221226505u:
{
ulong p0 = exceptionRecord->NumberParameters >= 1 ? (*exceptionRecord->ExceptionInformation) : 0;
ulong p1 = exceptionRecord->NumberParameters >= 2 ? exceptionRecord->ExceptionInformation[1] : 0;
@@ -142,21 +140,21 @@ public sealed partial class DirectExecutionBackend
}
}
ulong rax = ReadCtxU64(contextRecord, CTX_RAX);
ulong rbx = ReadCtxU64(contextRecord, CTX_RBX);
ulong rcx = ReadCtxU64(contextRecord, CTX_RCX);
ulong rdx = ReadCtxU64(contextRecord, CTX_RDX);
ulong rsi = ReadCtxU64(contextRecord, CTX_RSI);
ulong rdi = ReadCtxU64(contextRecord, CTX_RDI);
ulong rbp = ReadCtxU64(contextRecord, CTX_RBP);
ulong r8 = ReadCtxU64(contextRecord, CTX_R8);
ulong r9 = ReadCtxU64(contextRecord, CTX_R9);
ulong r10 = ReadCtxU64(contextRecord, CTX_R10);
ulong r11 = ReadCtxU64(contextRecord, CTX_R11);
ulong r12 = ReadCtxU64(contextRecord, CTX_R12);
ulong r13 = ReadCtxU64(contextRecord, CTX_R13);
ulong r14 = ReadCtxU64(contextRecord, CTX_R14);
ulong r15 = ReadCtxU64(contextRecord, CTX_R15);
ulong rax = ReadCtxU64(contextRecord, 120);
ulong rbx = ReadCtxU64(contextRecord, 144);
ulong rcx = ReadCtxU64(contextRecord, 128);
ulong rdx = ReadCtxU64(contextRecord, 136);
ulong rsi = ReadCtxU64(contextRecord, 168);
ulong rdi = ReadCtxU64(contextRecord, 176);
ulong rbp = ReadCtxU64(contextRecord, 160);
ulong r8 = ReadCtxU64(contextRecord, 184);
ulong r9 = ReadCtxU64(contextRecord, 192);
ulong r10 = ReadCtxU64(contextRecord, 200);
ulong r11 = ReadCtxU64(contextRecord, 208);
ulong r12 = ReadCtxU64(contextRecord, 216);
ulong r13 = ReadCtxU64(contextRecord, 224);
ulong r14 = ReadCtxU64(contextRecord, 232);
ulong r15 = ReadCtxU64(contextRecord, 240);
Console.Error.WriteLine("[LOADER][INFO] =========================================");
Console.Error.WriteLine("[LOADER][INFO] NATIVE EXCEPTION CAUGHT!");
@@ -187,7 +185,7 @@ public sealed partial class DirectExecutionBackend
ulong accessType = 0;
ulong target = 0;
if (exceptionCode == WindowsFaultCodes.AccessViolation && exceptionRecord->NumberParameters >= 2)
if (exceptionCode == 3221225477u && exceptionRecord->NumberParameters >= 2)
{
accessType = *exceptionRecord->ExceptionInformation;
target = exceptionRecord->ExceptionInformation[1];
@@ -200,9 +198,9 @@ public sealed partial class DirectExecutionBackend
};
Console.Error.WriteLine("[LOADER][INFO] AV access: " + accessText);
Console.Error.WriteLine($"[LOADER][INFO] AV target: 0x{target:X16}");
if (_hostMemory.Query(target, out var mbi))
if (VirtualQuery((void*)target, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) != 0)
{
Console.Error.WriteLine($"[LOADER][INFO] AV target region: base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} state=0x{mbi.RawState:X08} protect=0x{mbi.RawProtection:X08}");
Console.Error.WriteLine($"[LOADER][INFO] AV target region: base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} state=0x{mbi.State:X08} protect=0x{mbi.Protect:X08}");
}
}
@@ -250,7 +248,7 @@ public sealed partial class DirectExecutionBackend
switch (exceptionCode)
{
case WindowsFaultCodes.AccessViolation:
case 3221225477u:
Console.Error.WriteLine("[LOADER][ERROR] Type: Access Violation");
Console.Error.WriteLine("[LOADER][ERROR] This usually means:");
Console.Error.WriteLine("[LOADER][ERROR] - Guest code called an unmapped import");
@@ -297,11 +295,11 @@ public sealed partial class DirectExecutionBackend
DumpGuestReferenceDiagnostics();
DumpGuestPointerWindowDiagnostics();
break;
case WindowsFaultCodes.Breakpoint:
case 2147483651u:
Console.Error.WriteLine("[LOADER][WARNING] Type: Breakpoint (int3)");
Console.Error.WriteLine("[LOADER][WARNING] Unexpected breakpoint in direct-bridge mode");
break;
case WindowsFaultCodes.IllegalInstruction:
case 3221225501u:
Console.Error.WriteLine("[LOADER][INFO] Type: Illegal Instruction");
break;
}
@@ -334,8 +332,8 @@ public sealed partial class DirectExecutionBackend
EXCEPTION_POINTERS* pointers = (EXCEPTION_POINTERS*)exceptionInfo;
EXCEPTION_RECORD* record = pointers->ExceptionRecord;
void* contextRecord = pointers->ContextRecord;
ulong rip = contextRecord != null ? ReadCtxU64(contextRecord, CTX_RIP) : 0;
ulong rsp = contextRecord != null ? ReadCtxU64(contextRecord, CTX_RSP) : 0;
ulong rip = contextRecord != null ? ReadCtxU64(contextRecord, 248) : 0;
ulong rsp = contextRecord != null ? ReadCtxU64(contextRecord, 152) : 0;
ulong accessType = record->NumberParameters >= 1 ? *record->ExceptionInformation : 0;
ulong target = record->NumberParameters >= 2 ? record->ExceptionInformation[1] : 0;
Console.Error.WriteLine(
@@ -481,7 +479,7 @@ public sealed partial class DirectExecutionBackend
ulong address = scanBase;
while (address < scanEnd)
{
if (!_hostMemory.Query(address, out var mbi))
if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
{
break;
}
@@ -493,9 +491,9 @@ public sealed partial class DirectExecutionBackend
break;
}
if (mbi.State == HostRegionState.Committed &&
IsReadableProtection(mbi.RawProtection) &&
IsExecutableProtection(mbi.RawProtection))
if (mbi.State == MEM_COMMIT &&
IsReadableProtection(mbi.Protect) &&
IsExecutableProtection(mbi.Protect))
{
ScanExecutableRegionForTargetReferences(regionBase, regionEnd, targetList, hitCounts, maxHitsPerTarget);
}
@@ -800,13 +798,13 @@ public sealed partial class DirectExecutionBackend
return false;
}
if (!_hostMemory.Query(address, out var mbi))
if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
{
return false;
}
ulong regionEnd = mbi.BaseAddress + mbi.RegionSize;
if (mbi.State != HostRegionState.Committed || !IsReadableProtection(mbi.RawProtection) || regionEnd <= address || address > regionEnd - 8)
if (mbi.State != MEM_COMMIT || !IsReadableProtection(mbi.Protect) || regionEnd <= address || address > regionEnd - 8)
{
return false;
}
@@ -918,25 +916,25 @@ public sealed partial class DirectExecutionBackend
{
return false;
}
if (!_hostMemory.Query(faultAddress, out var mbi))
if (VirtualQuery((void*)faultAddress, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
{
return false;
}
ulong pageBase = faultAddress & 0xFFFFFFFFFFFFF000uL;
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.RawAllocationProtection);
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.AllocationProtect);
int traceIndex = Interlocked.Increment(ref _lazyCommitTraceCount);
bool traceLazyCommit = ShouldTraceLazyCommit(traceIndex);
if (traceLazyCommit)
{
Console.Error.WriteLine($"[LOADER][TRACE] lazy-query#{traceIndex}: fault=0x{faultAddress:X16} owner={owner} rip=0x{rip:X16} rsp=0x{rsp:X16} state=0x{mbi.RawState:X08} base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} alloc=0x{mbi.RawAllocationProtection:X08} prot=0x{mbi.RawProtection:X08}");
Console.Error.WriteLine($"[LOADER][TRACE] lazy-query#{traceIndex}: fault=0x{faultAddress:X16} owner={owner} rip=0x{rip:X16} rsp=0x{rsp:X16} state=0x{mbi.State:X08} base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} alloc=0x{mbi.AllocationProtect:X08} prot=0x{mbi.Protect:X08}");
}
if (mbi.State == HostRegionState.Committed && IsAccessCompatible(accessType, mbi.RawProtection))
if (mbi.State == 4096 && IsAccessCompatible(accessType, mbi.Protect))
{
if (traceLazyCommit)
{
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit-race#{traceIndex}: fault=0x{faultAddress:X16} protect=0x{mbi.RawProtection:X08}");
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit-race#{traceIndex}: fault=0x{faultAddress:X16} protect=0x{mbi.Protect:X08}");
}
return true;
}
@@ -945,10 +943,10 @@ public sealed partial class DirectExecutionBackend
ulong committedBase = 0;
ulong committedSize = 0;
if (mbi.State == HostRegionState.Free)
if (mbi.State == 65536)
{
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var windowBase, out var windowSize) &&
TryReserveThenCommit(_hostMemory, windowBase, windowSize, windowBase, windowSize, commitProtect))
TryReserveThenCommit(windowBase, windowSize, windowBase, windowSize, commitProtect))
{
committed = true;
committedBase = windowBase;
@@ -957,7 +955,7 @@ public sealed partial class DirectExecutionBackend
else
{
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL;
if (TryReserveThenCommit(_hostMemory, largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
if (TryReserveThenCommit(largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
{
committed = true;
committedBase = largeBase;
@@ -968,13 +966,13 @@ public sealed partial class DirectExecutionBackend
if (!committed)
{
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
if (TryReserveThenCommit(_hostMemory, region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
if (TryReserveThenCommit(region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
{
committed = true;
committedBase = region64kBase;
committedSize = 65536uL;
}
else if (TryReserveThenCommit(_hostMemory, pageBase, 4096uL, pageBase, 4096uL, commitProtect))
else if (TryReserveThenCommit(pageBase, 4096uL, pageBase, 4096uL, commitProtect))
{
committed = true;
committedBase = pageBase;
@@ -987,7 +985,7 @@ public sealed partial class DirectExecutionBackend
return false;
}
TryCommitRange(_hostMemory, pageBase + 4096, 4096uL, commitProtect);
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
if (traceLazyCommit)
{
Console.Error.WriteLine($"[LOADER][TRACE] lazy-reserve-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
@@ -995,13 +993,13 @@ public sealed partial class DirectExecutionBackend
return true;
}
if (mbi.State != HostRegionState.Reserved)
if (mbi.State != 8192)
{
return false;
}
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var commitWindowBase, out var commitWindowSize) &&
TryCommitRange(_hostMemory, commitWindowBase, commitWindowSize, commitProtect))
TryCommitRange(commitWindowBase, commitWindowSize, commitProtect))
{
committed = true;
committedBase = commitWindowBase;
@@ -1010,7 +1008,7 @@ public sealed partial class DirectExecutionBackend
else
{
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL;
if (TryCommitRange(_hostMemory, largeCommitBase, 2097152uL, commitProtect))
if (TryCommitRange(largeCommitBase, 2097152uL, commitProtect))
{
committed = true;
committedBase = largeCommitBase;
@@ -1021,19 +1019,19 @@ public sealed partial class DirectExecutionBackend
if (!committed)
{
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
if (TryCommitRange(_hostMemory, region64kBase, 65536uL, commitProtect))
if (TryCommitRange(region64kBase, 65536uL, commitProtect))
{
committed = true;
committedBase = region64kBase;
committedSize = 65536uL;
}
else if (TryCommitRange(_hostMemory, pageBase, 8192uL, commitProtect))
else if (TryCommitRange(pageBase, 8192uL, commitProtect))
{
committed = true;
committedBase = pageBase;
committedSize = 8192uL;
}
else if (TryCommitRange(_hostMemory, pageBase, 4096uL, commitProtect))
else if (TryCommitRange(pageBase, 4096uL, commitProtect))
{
committed = true;
committedBase = pageBase;
@@ -1046,7 +1044,7 @@ public sealed partial class DirectExecutionBackend
return false;
}
TryCommitRange(_hostMemory, pageBase + 4096, 4096uL, commitProtect);
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
if (traceLazyCommit)
{
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
@@ -1087,33 +1085,31 @@ public sealed partial class DirectExecutionBackend
return true;
}
// The commit protection is one of the two raw values ResolveLazyCommitProtection
// produces (0x40 RWX / 0x04 RW); the enum mapping reproduces those exactly.
static bool TryCommitRange(IHostMemory hostMemory, ulong baseAddress, ulong length, uint protection)
static unsafe bool TryCommitRange(ulong baseAddress, ulong length, uint protection)
{
if (length == 0)
{
return false;
}
return hostMemory.Commit(baseAddress, length, protection == 64u ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite);
return VirtualAlloc((void*)baseAddress, (nuint)length, 4096u, protection) != null;
}
static bool TryReserveRange(IHostMemory hostMemory, ulong baseAddress, ulong length)
static unsafe bool TryReserveRange(ulong baseAddress, ulong length)
{
if (length == 0)
{
return false;
}
return hostMemory.Reserve(baseAddress, length, HostPageProtection.ReadWrite) != 0;
return VirtualAlloc((void*)baseAddress, (nuint)length, 8192u, 4u) != null;
}
static bool TryReserveThenCommit(IHostMemory hostMemory, ulong reserveAddress, ulong reserveSize, ulong commitAddress, ulong commitSize, uint protection)
static bool TryReserveThenCommit(ulong reserveAddress, ulong reserveSize, ulong commitAddress, ulong commitSize, uint protection)
{
if (!TryReserveRange(hostMemory, reserveAddress, reserveSize))
if (!TryReserveRange(reserveAddress, reserveSize))
{
return false;
}
return TryCommitRange(hostMemory, commitAddress, commitSize, protection);
return TryCommitRange(commitAddress, commitSize, protection);
}
static bool IsAccessCompatible(ulong accessType, uint protection)
@@ -8,10 +8,8 @@ using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.Core.Cpu;
using SharpEmu.Core.Cpu.Native.Windows;
using SharpEmu.Core.Loader;
using SharpEmu.HLE;
using SharpEmu.HLE.Host;
namespace SharpEmu.Core.Cpu.Native;
@@ -71,23 +69,23 @@ public sealed partial class DirectExecutionBackend
private unsafe static int TryRecoverUnresolvedSentinel(void* exceptionInfo)
{
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
if (exceptionRecord->ExceptionCode != WindowsFaultCodes.AccessViolation)
if (exceptionRecord->ExceptionCode != 3221225477u)
{
return 0;
}
void* contextRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord;
ulong value = ReadCtxU64(contextRecord, CTX_RIP);
ulong value = ReadCtxU64(contextRecord, 248);
ulong value2 = (ulong)exceptionRecord->ExceptionAddress;
if (!IsUnresolvedSentinel(value) && !IsUnresolvedSentinel(value2))
{
return 0;
}
ulong rsp = ReadCtxU64(contextRecord, CTX_RSP);
WriteCtxU64(contextRecord, CTX_RAX, 0uL);
ulong rsp = ReadCtxU64(contextRecord, 152);
WriteCtxU64(contextRecord, 120, 0uL);
if (TryGetPlausibleReturnFromStack(rsp, out var returnRip, out var nextRsp))
{
WriteCtxU64(contextRecord, CTX_RSP, nextRsp);
WriteCtxU64(contextRecord, CTX_RIP, returnRip);
WriteCtxU64(contextRecord, 152, nextRsp);
WriteCtxU64(contextRecord, 248, returnRip);
Interlocked.Increment(ref _rawSentinelRecoveries);
if (LogThreadMode)
{
@@ -163,7 +161,7 @@ public sealed partial class DirectExecutionBackend
*(ulong*)(xmmSlot + 8));
}
cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL;
if (importStubEntry.Kind == ImportStubKind.BootstrapBridge)
if (string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
{
NormalizeKernelDynlibDlsymArguments(cpuContext, out _, out _);
*(ulong*)argPackPtr = cpuContext[CpuRegister.Rdi];
@@ -240,22 +238,6 @@ public sealed partial class DirectExecutionBackend
cpuContext[CpuRegister.Rax] = 0uL;
return 0uL;
}
if (_hostShutdownRequested)
{
if (isGuestWorker &&
TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, "host shutdown"))
{
cpuContext[CpuRegister.Rax] = 0uL;
return 0uL;
}
if (!isGuestWorker &&
TryAbortGuestForHostShutdown(argPackPtr, num, num7))
{
cpuContext[CpuRegister.Rax] = 1uL;
return 1uL;
}
}
bool flag0 = ShouldSuppressStrlenTrace(importStubEntry.Nid);
bool flag = num7 >= 2156221920u && num7 <= 2156225024u;
bool flag2 = num7 >= 2156351360u && num7 <= 2156352080u;
@@ -263,13 +245,12 @@ public sealed partial class DirectExecutionBackend
bool flag4 = !string.IsNullOrWhiteSpace(_importFilter);
bool flag5 = false;
ExportedFunction? matchedExport = importStubEntry.Export;
var traceFlags = importStubEntry.TraceFlags;
bool periodicTrace = num <= 128 ||
(num >= 240 && num <= 400) ||
(num >= 900 && num <= 1300) ||
num % 100000 == 0L ||
((traceFlags & ImportStubTraceFlags.PeriodicEvery1000) != 0 && (num <= 256 || num % 1000 == 0L)) ||
((traceFlags & ImportStubTraceFlags.PeriodicEvery128) != 0 && (num <= 256 || num % 128 == 0)) ||
(importStubEntry.Nid == "tsvEmnenz48" && (num <= 256 || num % 1000 == 0L)) ||
(importStubEntry.Nid == "rTXw65xmLIA" && (num <= 256 || num % 128 == 0)) ||
flag ||
flag2 ||
flag3;
@@ -330,15 +311,15 @@ public sealed partial class DirectExecutionBackend
cpuContext[CpuRegister.Rsi],
cpuContext[CpuRegister.Rdx]);
}
if ((traceFlags & ImportStubTraceFlags.Memset) != 0 && num <= 256)
if (importStubEntry.Nid == "8zTFvBIAIN8" && num <= 256)
{
Console.Error.WriteLine($"[LOADER][TRACE] memset#{num}: dst=0x{cpuContext[CpuRegister.Rdi]:X16} val=0x{cpuContext[CpuRegister.Rsi] & 0xFF:X2} len=0x{cpuContext[CpuRegister.Rdx]:X16} ret=0x{num7:X16}");
}
if ((traceFlags & ImportStubTraceFlags.CxaAtexit) != 0 && num <= 64)
if (importStubEntry.Nid == "tsvEmnenz48" && num <= 64)
{
Console.Error.WriteLine($"[LOADER][TRACE] __cxa_atexit#{num}: func=0x{cpuContext[CpuRegister.Rdi]:X16} arg=0x{cpuContext[CpuRegister.Rsi]:X16} dso=0x{cpuContext[CpuRegister.Rdx]:X16} ret=0x{num7:X16}");
}
if ((traceFlags & ImportStubTraceFlags.RawArgs) != 0)
if (importStubEntry.Nid == "bzQExy189ZI" || importStubEntry.Nid == "8G2LB+A3rzg")
{
Console.Error.WriteLine($"[LOADER][TRACE] {importStubEntry.Nid}#{num}: rdi=0x{cpuContext[CpuRegister.Rdi]:X16} rsi=0x{cpuContext[CpuRegister.Rsi]:X16} rdx=0x{cpuContext[CpuRegister.Rdx]:X16} ret=0x{num7:X16}");
}
@@ -368,7 +349,7 @@ public sealed partial class DirectExecutionBackend
Console.Error.Flush();
}
}
if ((traceFlags & ImportStubTraceFlags.StackChkFail) != 0)
if (importStubEntry.Nid == "Ou3iL1abvng")
{
if (_logStackCheck)
{
@@ -400,7 +381,7 @@ public sealed partial class DirectExecutionBackend
ActiveGuestReturnSlotAddress);
try
{
if (importStubEntry.Kind == ImportStubKind.BootstrapBridge)
if (string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
{
if (_logBootstrap)
{
@@ -409,11 +390,12 @@ public sealed partial class DirectExecutionBackend
orbisGen2Result = DispatchBootstrapBridge();
}
else if (importStubEntry.Kind == ImportStubKind.KernelDynlibDlsym)
else if (string.Equals(importStubEntry.Nid, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal) ||
string.Equals(importStubEntry.Nid, "LwG8g3niqwA", StringComparison.Ordinal))
{
orbisGen2Result = DispatchKernelDynlibDlsym();
}
else if (importStubEntry.Kind == ImportStubKind.Il2CppApiLookupSymbol)
else if (string.Equals(importStubEntry.Nid, "r8mvOaWdi28", StringComparison.Ordinal))
{
orbisGen2Result = DispatchIl2CppApiLookupSymbol();
}
@@ -535,7 +517,8 @@ public sealed partial class DirectExecutionBackend
out var blockContinuation,
out var hasBlockContinuation,
out var blockWakeKey,
out var blockWaiter,
out var blockResumeHandler,
out var blockWakeHandler,
out var blockDeadlineTimestamp) &&
TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, blockReason))
{
@@ -545,7 +528,8 @@ public sealed partial class DirectExecutionBackend
GuestThreadExecution.CurrentGuestThreadHandle,
blockContinuation,
blockWakeKey,
blockWaiter,
blockResumeHandler,
blockWakeHandler,
blockDeadlineTimestamp);
}
@@ -676,7 +660,8 @@ public sealed partial class DirectExecutionBackend
out var blockContinuation,
out var hasBlockContinuation,
out var blockWakeKey,
out var blockWaiter,
out var blockResumeHandler,
out var blockWakeHandler,
out var blockDeadlineTimestamp) &&
TryYieldGuestThreadToHostStub(argPackPtr, dispatchIndex, returnRip, importStubEntry.Nid, blockReason))
{
@@ -686,7 +671,8 @@ public sealed partial class DirectExecutionBackend
GuestThreadExecution.CurrentGuestThreadHandle,
blockContinuation,
blockWakeKey,
blockWaiter,
blockResumeHandler,
blockWakeHandler,
blockDeadlineTimestamp);
}
@@ -750,9 +736,6 @@ public sealed partial class DirectExecutionBackend
var expectedEqueueTimeout =
string.Equals(nid, "fzyMKs9kim0", StringComparison.Ordinal) &&
result == OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
var expectedEventFlagTimeout =
string.Equals(nid, "JTvBflhYazQ", StringComparison.Ordinal) &&
result == OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
var expectedMutexTrylockBusy =
string.Equals(nid, "K-jXhbt2gn4", StringComparison.Ordinal) &&
result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
@@ -765,7 +748,6 @@ public sealed partial class DirectExecutionBackend
if (!expectedFileProbeMiss &&
!expectedTimedWaitTimeout &&
!expectedEqueueTimeout &&
!expectedEventFlagTimeout &&
!expectedMutexTrylockBusy &&
!expectedUserServiceNoEvent &&
!expectedPrivacyInvalidParameter)
@@ -828,15 +810,11 @@ public sealed partial class DirectExecutionBackend
return !_logUsleep;
}
// Mutex lock uses this block-capable leaf path. Keep it out of the no-block subset.
return nid is
"9UK1vLZQft4" or // scePthreadMutexLock
"7H0iTOciTLo" or // pthread_mutex_lock
"tn3VlD0hG60" or // scePthreadMutexUnlock
"7H0iTOciTLo" or // pthread_mutex_lock
"2Z+PpY6CaJg" or // pthread_mutex_unlock
"EgmLo6EWgso" or // pthread_rwlock_unlock
"+L98PIbGttk" or // scePthreadRwlockUnlock
"q1cHNfGycLI" or // scePadRead
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
"zgXifHT9ErY" or // sceVideoOutIsFlipPending
"V++UgBtQhn0" or // sceAgcGetDataPacketPayloadAddress
@@ -895,7 +873,7 @@ public sealed partial class DirectExecutionBackend
"Q2V+iqvjgC0" or // vsnprintf
"j4ViWNHEgww" or // strlen
"5jNubw4vlAA" or // strnlen
"LHMrG7e8G78" or // wcsmisc
"LHMrG7e8G78" or // wcslen
"WkkeywLJcgU" or // wcslen
"Ovb2dSJOAuE" or // strcmp
"aesyjrHVWy4" or // strncmp
@@ -910,7 +888,12 @@ public sealed partial class DirectExecutionBackend
"6ULAa0fq4jA" or // scePthreadRwlockInit
"1471ajPzxh0" or // pthread_rwlock_destroy
"BB+kb08Tl9A" or // scePthreadRwlockDestroy
// rwlock rd/wr lock removed (can block); init/destroy/unlock stay.
"iGjsr1WAtI0" or // pthread_rwlock_rdlock
"Ox9i0c7L5w0" or // scePthreadRwlockRdlock
"sIlRvQqsN2Y" or // pthread_rwlock_wrlock
"mqdNorrB+gI" or // scePthreadRwlockWrlock
"EgmLo6EWgso" or // pthread_rwlock_unlock
"+L98PIbGttk" or // scePthreadRwlockUnlock
"aI+OeCz8xrQ" or // scePthreadSelf
"EotR8a3ASf4" or // pthread_self
"eoht7mQOCmo" or // scePthreadGetspecific
@@ -989,31 +972,6 @@ public sealed partial class DirectExecutionBackend
return true;
}
private unsafe bool TryAbortGuestForHostShutdown(nint argPackPtr, long dispatchIndex, ulong returnRip)
{
ulong hostExit = ActiveEntryReturnSentinelRip;
if (hostExit < 65536 || !TryPatchActiveGuestReturnSlot(hostExit))
{
return false;
}
try
{
*(ulong*)(argPackPtr + 96) = hostExit;
}
catch
{
return false;
}
ActiveForcedGuestExit = true;
if (string.IsNullOrWhiteSpace(LastError))
{
LastError = "Host shutdown requested.";
}
Console.Error.WriteLine(
$"[LOADER][INFO] Guest unwind for host shutdown at import#{dispatchIndex} ret=0x{returnRip:X16} -> host_exit=0x{hostExit:X16}");
return true;
}
private unsafe bool TryCompleteGuestEntryToHostStub(nint argPackPtr, long dispatchIndex, ulong returnRip, string nid, string reason, ulong value)
{
ulong hostExit = ActiveEntryReturnSentinelRip;
@@ -1120,15 +1078,7 @@ public sealed partial class DirectExecutionBackend
}
private static bool IsImportLoopGuardBoundary(string nid) =>
nid switch
{
"1jfXLRVzisc" => true, // sceKernelUsleep
"QcteRwbsnV0" => true, // usleep
"n88vx3C5nW8" => true, // gettimeofday
"Zxa0VhQVTsk" => true, // sceKernelWaitSema
"T72hz6ffq08" => true, // scePthreadYield
_ => false
};
string.Equals(nid, "1jfXLRVzisc", StringComparison.Ordinal);
private void ResetImportLoopPattern()
{
@@ -1723,9 +1673,9 @@ public sealed partial class DirectExecutionBackend
{
var candidateBase = ImportStubRegionCanonicalBase -
(ulong)candidateIndex * ImportStubRegionAddressStride;
if (!_hostMemory.Query(candidateBase, out var memoryInfo) ||
if (VirtualQuery((void*)candidateBase, out var memoryInfo, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0 ||
memoryInfo.RegionSize == 0 ||
memoryInfo.State != HostRegionState.Committed)
memoryInfo.State != 4096)
{
continue;
}
@@ -1966,36 +1916,23 @@ public sealed partial class DirectExecutionBackend
return false;
}
// Reads stay byte-by-byte through TryReadByteCompat (its Marshal.ReadByte
// fallback must probe exactly up to the terminator), but the bytes land in a
// stack buffer instead of a List<byte> + ToArray per symbol resolution.
const int StackBufferLength = 512;
byte[]? rented = maxLength > StackBufferLength ? System.Buffers.ArrayPool<byte>.Shared.Rent(maxLength) : null;
Span<byte> buffer = rented is null ? stackalloc byte[StackBufferLength] : rented;
try
List<byte> list = new List<byte>(Math.Min(maxLength, 256));
Span<byte> destination = stackalloc byte[1];
for (int i = 0; i < maxLength; i++)
{
for (int i = 0; i < maxLength; i++)
if (!TryReadByteCompat(address + (ulong)i, destination))
{
if (!TryReadByteCompat(address + (ulong)i, buffer.Slice(i, 1)))
{
return false;
}
if (buffer[i] == 0)
{
value = System.Text.Encoding.ASCII.GetString(buffer[..i]);
return true;
}
return false;
}
value = System.Text.Encoding.ASCII.GetString(buffer[..maxLength]);
return true;
}
finally
{
if (rented is not null)
if (destination[0] == 0)
{
System.Buffers.ArrayPool<byte>.Shared.Return(rented);
value = System.Text.Encoding.ASCII.GetString(list.ToArray());
return true;
}
list.Add(destination[0]);
}
value = System.Text.Encoding.ASCII.GetString(list.ToArray());
return true;
}
private bool TryReadByteCompat(ulong address, Span<byte> destination)
@@ -2077,7 +2014,7 @@ public sealed partial class DirectExecutionBackend
uint flNewProtect = default(uint);
try
{
if (Marshal.ReadByte(num2) != 232 || !_hostMemory.Protect((ulong)(void*)num, 5u, HostPageProtection.ReadWriteExecute, out flNewProtect))
if (Marshal.ReadByte(num2) != 232 || !VirtualProtect((void*)num, 5u, 64u, &flNewProtect))
{
return;
}
@@ -2085,7 +2022,7 @@ public sealed partial class DirectExecutionBackend
{
Marshal.WriteByte(num2 + i, 144);
}
_hostMemory.FlushInstructionCache((ulong)(void*)num, 5u);
FlushInstructionCache(GetCurrentProcess(), (void*)num, 5u);
_patchedEa020eLookupCall = true;
Console.Error.WriteLine($"[LOADER][WARNING] Import#{dispatchIndex}: patched hash-lookup call at 0x{num:X16} -> NOP*5");
}
@@ -2096,7 +2033,7 @@ public sealed partial class DirectExecutionBackend
{
if (flNewProtect != 0)
{
_hostMemory.ProtectRaw((ulong)(void*)num, 5u, flNewProtect, out flNewProtect);
VirtualProtect((void*)num, 5u, flNewProtect, &flNewProtect);
}
}
}
@@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.HLE;
using SharpEmu.HLE.Host;
namespace SharpEmu.Core.Cpu.Native;
@@ -36,6 +35,20 @@ public sealed partial class DirectExecutionBackend
private bool _nativeWorkersDisposed;
private int _nativeWorkerCreationFailedLogged;
private const uint StackSizeParamIsAReservation = 0x00010000u;
[DllImport("kernel32.dll", SetLastError = true)]
private static extern nint CreateThread(
nint lpThreadAttributes,
nuint dwStackSize,
nint lpStartAddress,
nint lpParameter,
uint dwCreationFlags,
out uint lpThreadId);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint WaitForSingleObject(nint hHandle, uint dwMilliseconds);
// Runs an emitted guest entry stub. Preferred path is a pooled native worker
// thread; falls back to the historical inline calli (guest frames above this
// thread's managed frames) when workers are disabled or unavailable.
@@ -48,7 +61,7 @@ public sealed partial class DirectExecutionBackend
var worker = RentNativeGuestExecutor();
if (worker is null)
{
_hostThreading.SetTlsValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
return CallNativeEntry(entryStub);
}
try
@@ -216,7 +229,7 @@ public sealed partial class DirectExecutionBackend
public static NativeGuestExecutor? TryCreate(DirectExecutionBackend backend)
{
if (!EnsureHostRuntimeExports(backend._hostSymbols))
if (!EnsureKernel32Exports())
{
return null;
}
@@ -229,27 +242,32 @@ public sealed partial class DirectExecutionBackend
return executor;
}
private static bool EnsureHostRuntimeExports(IHostSymbolResolver symbols)
private static bool EnsureKernel32Exports()
{
if (_exitThreadAddress != 0)
{
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0;
}
_waitForSingleObjectAddress = symbols.GetAddress(HostRuntimeFunction.WaitForSingleObject);
_setEventAddress = symbols.GetAddress(HostRuntimeFunction.SetEvent);
_exitThreadAddress = symbols.GetAddress(HostRuntimeFunction.ExitThread);
nint kernel32 = GetModuleHandle("kernel32.dll");
if (kernel32 == 0)
{
return false;
}
_waitForSingleObjectAddress = GetProcAddress(kernel32, "WaitForSingleObject");
_setEventAddress = GetProcAddress(kernel32, "SetEvent");
_exitThreadAddress = GetProcAddress(kernel32, "ExitThread");
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0 && _exitThreadAddress != 0;
}
private bool Initialize()
{
_selfHandle = GCHandle.Alloc(this);
_controlBlock = (void*)_backend._hostMemory.Allocate(0, 4096u, HostPageProtection.ReadWrite);
_controlBlock = VirtualAlloc(null, 4096u, 12288u, 4u);
if (_controlBlock == null)
{
return false;
}
_loopStub = (void*)_backend._hostMemory.Allocate(0, LoopStubSize, HostPageProtection.ReadWriteExecute);
_loopStub = VirtualAlloc(null, LoopStubSize, 12288u, 64u);
if (_loopStub == null)
{
return false;
@@ -331,15 +349,17 @@ public sealed partial class DirectExecutionBackend
*(int*)(code + skipJump) = skipEntryOffset - (skipJump + sizeof(int));
uint oldProtect = 0;
if (!_backend._hostMemory.Protect((ulong)_loopStub, LoopStubSize, HostPageProtection.ReadExecute, out oldProtect))
if (!VirtualProtect(_loopStub, LoopStubSize, 32u, &oldProtect))
{
return false;
}
_backend._hostMemory.FlushInstructionCache((ulong)_loopStub, LoopStubSize);
_threadHandle = _backend._hostThreading.CreateNativeThread(
(nint)_loopStub,
FlushInstructionCache(GetCurrentProcess(), _loopStub, LoopStubSize);
_threadHandle = CreateThread(
0,
WorkerStackReservation,
(nint)_loopStub,
0,
StackSizeParamIsAReservation,
out _nativeThreadId);
if (_threadHandle == 0)
{
@@ -445,7 +465,7 @@ public sealed partial class DirectExecutionBackend
_prevYieldRequested = _activeGuestThreadYieldRequested;
_prevYieldReason = _activeGuestThreadYieldReason;
_prevState = _activeGuestThreadState;
_prevHostRspSlot = backend._hostThreading.GetTlsValue(backend._hostRspSlotTlsIndex);
_prevHostRspSlot = TlsGetValue(backend._hostRspSlotTlsIndex);
_prevGuestThreadHandle = GuestThreadExecution.EnterGuestThread(_runGuestThreadHandle);
_entered = true;
_activeExecutionBackend = backend;
@@ -457,11 +477,11 @@ public sealed partial class DirectExecutionBackend
_activeGuestThreadYieldReason = null;
_activeGuestThreadState = _runState;
backend.BindTlsBase(_runContext!);
backend._hostThreading.SetTlsValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
TlsSetValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
if (_runState is { } state)
{
_prevHostThreadId = Volatile.Read(ref state.HostThreadId);
Volatile.Write(ref state.HostThreadId, unchecked((int)backend._hostThreading.CurrentThreadId));
Volatile.Write(ref state.HostThreadId, unchecked((int)GetCurrentThreadId()));
}
if (_runAffinityMask != 0)
{
@@ -491,7 +511,7 @@ public sealed partial class DirectExecutionBackend
{
Volatile.Write(ref state.HostThreadId, _prevHostThreadId);
}
_backend._hostThreading.SetTlsValue(_backend._hostRspSlotTlsIndex, _prevHostRspSlot);
TlsSetValue(_backend._hostRspSlotTlsIndex, _prevHostRspSlot);
GuestThreadExecution.RestoreGuestThread(_prevGuestThreadHandle);
_activeExecutionBackend = _prevBackend;
_activeCpuContext = _prevContext;
@@ -528,8 +548,8 @@ public sealed partial class DirectExecutionBackend
var exited = _threadHandle == 0;
if (_threadHandle != 0)
{
exited = _backend._hostThreading.WaitForThreadExit(_threadHandle, 1000u);
_backend._hostThreading.CloseThreadHandle(_threadHandle);
exited = WaitForSingleObject(_threadHandle, 1000u) == 0u;
CloseHandle(_threadHandle);
_threadHandle = 0;
}
if (!exited)
@@ -543,12 +563,12 @@ public sealed partial class DirectExecutionBackend
}
if (_loopStub != null)
{
_backend._hostMemory.Free((ulong)_loopStub);
VirtualFree(_loopStub, 0u, 32768u);
_loopStub = null;
}
if (_controlBlock != null)
{
_backend._hostMemory.Free((ulong)_controlBlock);
VirtualFree(_controlBlock, 0u, 32768u);
_controlBlock = null;
}
if (_selfHandle.IsAllocated)
File diff suppressed because it is too large Load Diff
+31 -6
View File
@@ -3,7 +3,6 @@
using System.Runtime.InteropServices;
using SharpEmu.HLE;
using SharpEmu.HLE.Host;
namespace SharpEmu.Core.Cpu.Native;
@@ -12,15 +11,17 @@ public sealed unsafe class StubManager : IDisposable
private readonly List<nint> _allocatedStubs = new();
private readonly Dictionary<string, nint> _importHandlers = new();
private readonly Dictionary<ulong, nint> _stubAddresses = new();
private readonly IHostMemory _hostMemory;
private byte* _pltMemory;
private int _pltOffset;
private const int PltMemorySize = 1024 * 1024; // 1MB for stubs
public StubManager(IHostMemory? hostMemory = null)
public StubManager()
{
_hostMemory = hostMemory ?? HostPlatform.Current.Memory;
_pltMemory = (byte*)_hostMemory.Allocate(0, PltMemorySize, HostPageProtection.ReadWriteExecute);
_pltMemory = (byte*)VirtualAlloc(
null,
(nuint)PltMemorySize,
AllocationType.Reserve | AllocationType.Commit,
MemoryProtection.ExecuteReadWrite);
if (_pltMemory == null)
{
@@ -184,7 +185,7 @@ public sealed unsafe class StubManager : IDisposable
{
if (_pltMemory != null)
{
_hostMemory.Free((ulong)_pltMemory);
VirtualFree(_pltMemory, 0, FreeType.Release);
_pltMemory = null;
}
@@ -193,5 +194,29 @@ public sealed unsafe class StubManager : IDisposable
_stubAddresses.Clear();
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern void* VirtualAlloc(void* lpAddress, nuint dwSize, AllocationType flAllocationType, MemoryProtection flProtect);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool VirtualFree(void* lpAddress, nuint dwSize, FreeType dwFreeType);
[Flags]
private enum AllocationType : uint
{
Commit = 0x1000,
Reserve = 0x2000,
}
[Flags]
private enum MemoryProtection : uint
{
ExecuteReadWrite = 0x40,
}
private enum FreeType : uint
{
Release = 0x8000,
}
public delegate void ImportHandler(CpuContext context);
}
@@ -1,32 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Core.Cpu.Native.Windows;
/// <summary>
/// Byte offsets into the Win64 CONTEXT record delivered to vectored exception
/// handlers. The handlers read/write guest registers directly at these offsets
/// (no managed CONTEXT struct exists); a future POSIX backend gets a sibling
/// class for its mcontext layout.
/// </summary>
internal static class Win64ContextOffsets
{
public const int Mxcsr = 52;
public const int Rax = 120;
public const int Rcx = 128;
public const int Rdx = 136;
public const int Rbx = 144;
public const int Rsp = 152;
public const int Rbp = 160;
public const int Rsi = 168;
public const int Rdi = 176;
public const int R8 = 184;
public const int R9 = 192;
public const int R10 = 200;
public const int R11 = 208;
public const int R12 = 216;
public const int R13 = 224;
public const int R14 = 232;
public const int R15 = 240;
public const int Rip = 248;
}
@@ -1,24 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Core.Cpu.Native.Windows;
/// <summary>
/// Windows NTSTATUS exception codes and EXCEPTION_RECORD access-type values the
/// fault handlers filter on. Values are the same numbers the handlers previously
/// compared as bare literals; only the spelling changed.
/// </summary>
internal static class WindowsFaultCodes
{
public const uint AccessViolation = 0xC0000005u; // 3221225477
public const uint Breakpoint = 0x80000003u; // 2147483651
public const uint IllegalInstruction = 0xC000001Du; // 3221225501
public const uint FastFail = 0xC0000409u; // 3221226505
public const uint StackOverflow = 0xC00000FDu;
public const uint ClrManagedException = 0xE0434352u;
// EXCEPTION_RECORD.ExceptionInformation[0] for access violations.
public const ulong AccessRead = 0;
public const ulong AccessWrite = 1;
public const ulong AccessExecute = 8;
}
@@ -1,191 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
using SharpEmu.HLE.Host;
namespace SharpEmu.Core.Cpu.Native.Windows;
/// <summary>
/// Vectored-exception-handler installation and the handler pre-filter thunk.
/// The thunk is inherently Windows-shaped (TEB stack-limit reads via gs:,
/// NTSTATUS pre-filtering, Win64 calling convention) and moved here whole from
/// DirectExecutionBackend; a POSIX backend supplies a sibling built around
/// sigaction/sigaltstack instead.
/// </summary>
internal sealed unsafe partial class WindowsFaultHandling : IHostFaultHandling
{
private readonly IHostMemory _memory;
public WindowsFaultHandling(IHostMemory memory)
{
_memory = memory;
}
public nint CreateHandlerThunk(nint managedCallback, uint hostRspSwitchTlsSlot, nint tlsGetValueAddress)
{
const uint stubSize = 256u;
void* ptr = (void*)_memory.Allocate(0, stubSize, HostPageProtection.ReadWriteExecute);
if (ptr == null)
{
return 0;
}
byte* code = (byte*)ptr;
int offset = 0;
// Native pre-filter: these exception codes are raised while the thread can be in
// cooperative GC mode (a C# throw is RaiseException(0xE0434352) on the throwing
// thread; FailFast/stack-overflow arrive mid-runtime-failure). Entering the managed
// handler then trips the CLR's reverse-P/Invoke check and kills the process with
// "Invalid Program: attempted to call a UnmanagedCallersOnly method from managed
// code" — this is why no managed throw (even one with a catch handler) ever
// survived inside the emulator. Continue the handler search without touching
// managed code; the CLR's own VEH handles its exceptions. MSVC C++ exceptions
// (Vulkan drivers, host CRT) are excluded too: the managed handler only ever
// returned CONTINUE_SEARCH for them.
ReadOnlySpan<uint> nonManagedExceptionCodes =
[WindowsFaultCodes.ClrManagedException, 0xE06D7363u, WindowsFaultCodes.FastFail, WindowsFaultCodes.StackOverflow];
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x01); // mov rax, [rcx] (ExceptionRecord*)
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x00); // mov eax, [rax] (ExceptionCode)
var passJumpOffsets = stackalloc int[nonManagedExceptionCodes.Length];
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
{
EmitByte(code, ref offset, 0x3D); // cmp eax, imm32
EmitUInt32(code, ref offset, nonManagedExceptionCodes[i]);
EmitByte(code, ref offset, 0x74); // je pass
passJumpOffsets[i] = offset;
EmitByte(code, ref offset, 0x00);
}
EmitByte(code, ref offset, 0xEB); EmitByte(code, ref offset, 0x03); // jmp over pass block
int passOffset = offset;
EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0); // pass: xor eax, eax (EXCEPTION_CONTINUE_SEARCH)
EmitByte(code, ref offset, 0xC3); // ret
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
{
code[passJumpOffsets[i]] = checked((byte)(passOffset - (passJumpOffsets[i] + 1)));
}
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x54); // push r12
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x55); // push r13
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE4); // mov r12, rsp
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xCD); // mov r13, rcx
EmitByte(code, ref offset, 0x65); EmitByte(code, ref offset, 0x48); // mov rax, gs:[8]
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x04); EmitByte(code, ref offset, 0x25);
EmitUInt32(code, ref offset, 8u);
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x39); EmitByte(code, ref offset, 0xC4); // cmp r12, rax
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x83); // jae guestStack
int aboveStackJump = offset;
EmitUInt32(code, ref offset, 0u);
EmitByte(code, ref offset, 0x65); EmitByte(code, ref offset, 0x48); // mov rax, gs:[0x10]
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x04); EmitByte(code, ref offset, 0x25);
EmitUInt32(code, ref offset, 0x10u);
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x39); EmitByte(code, ref offset, 0xC4); // cmp r12, rax
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x82); // jb guestStack
int belowStackJump = offset;
EmitUInt32(code, ref offset, 0u);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28);
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE9); // mov rcx, r13
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
*(nint*)(code + offset) = managedCallback;
offset += sizeof(nint);
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28);
EmitByte(code, ref offset, 0xE9);
int hostRestoreJump = offset;
EmitUInt32(code, ref offset, 0u);
int guestStackOffset = offset;
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28);
EmitByte(code, ref offset, 0xB9);
EmitUInt32(code, ref offset, hostRspSwitchTlsSlot);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
*(nint*)(code + offset) = tlsGetValueAddress;
offset += sizeof(nint);
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x85); EmitByte(code, ref offset, 0xC0); // test rax, rax
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x84);
int missingTlsJump = offset;
EmitUInt32(code, ref offset, 0u);
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x18); // mov r11, [rax]
EmitByte(code, ref offset, 0x4D); EmitByte(code, ref offset, 0x85); EmitByte(code, ref offset, 0xDB); // test r11, r11
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x84);
int missingHostStackJump = offset;
EmitUInt32(code, ref offset, 0u);
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xDC); // mov rsp, r11
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28);
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE9); // mov rcx, r13
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
*(nint*)(code + offset) = managedCallback;
offset += sizeof(nint);
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28);
EmitByte(code, ref offset, 0xE9);
int guestRestoreJump = offset;
EmitUInt32(code, ref offset, 0u);
int passThroughOffset = offset;
EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0); // xor eax, eax
int restoreOffset = offset;
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE4); // mov rsp, r12
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5D);
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5C);
EmitByte(code, ref offset, 0xC3);
*(int*)(code + aboveStackJump) = guestStackOffset - (aboveStackJump + sizeof(int));
*(int*)(code + belowStackJump) = guestStackOffset - (belowStackJump + sizeof(int));
*(int*)(code + hostRestoreJump) = restoreOffset - (hostRestoreJump + sizeof(int));
*(int*)(code + missingTlsJump) = passThroughOffset - (missingTlsJump + sizeof(int));
*(int*)(code + missingHostStackJump) = passThroughOffset - (missingHostStackJump + sizeof(int));
*(int*)(code + guestRestoreJump) = restoreOffset - (guestRestoreJump + sizeof(int));
if (!_memory.Protect((ulong)ptr, stubSize, HostPageProtection.ReadExecute, out _))
{
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for exception handler trampoline at 0x{(nint)ptr:X16}");
_ = _memory.Free((ulong)ptr);
return 0;
}
_memory.FlushInstructionCache((ulong)ptr, (ulong)offset);
return (nint)ptr;
}
public void FreeThunk(nint thunk)
{
_ = _memory.Free((ulong)thunk);
}
public nint AddFirstChanceHandler(nint thunk)
{
return (nint)AddVectoredExceptionHandler(1u, thunk);
}
public void RemoveHandler(nint handle)
{
_ = RemoveVectoredExceptionHandler((void*)handle);
}
public void SetUnhandledFilter(nint thunk)
{
_ = SetUnhandledExceptionFilter(thunk);
}
private static void EmitByte(byte* code, ref int offset, byte value)
{
code[offset++] = value;
}
private static void EmitUInt32(byte* code, ref int offset, uint value)
{
*(uint*)(code + offset) = value;
offset += sizeof(uint);
}
[LibraryImport("kernel32.dll")]
private static partial void* AddVectoredExceptionHandler(uint first, IntPtr handler);
[LibraryImport("kernel32.dll")]
private static partial uint RemoveVectoredExceptionHandler(void* handle);
[LibraryImport("kernel32.dll")]
private static partial IntPtr SetUnhandledExceptionFilter(IntPtr lpTopLevelExceptionFilter);
}
+1 -6
View File
@@ -5,7 +5,7 @@ using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu;
public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemoryAllocator, ICpuMemoryWrapper
public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemoryAllocator
{
private readonly ICpuMemory _inner;
@@ -50,9 +50,4 @@ public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemo
address = 0;
return false;
}
public bool TryFreeGuestMemory(ulong address)
{
return _inner is IGuestMemoryAllocator allocator && allocator.TryFreeGuestMemory(address);
}
}
+15 -39
View File
@@ -45,13 +45,7 @@ public static class Ps5ParamJsonReader
try
{
ReadOnlyMemory<byte> json = data;
if (json.Span.StartsWith("\uFEFF"u8))
{
json = json[3..];
}
using var doc = JsonDocument.Parse(json);
using var doc = JsonDocument.Parse(data);
return TryReadPs5Param(doc.RootElement);
}
catch (JsonException)
@@ -62,15 +56,12 @@ public static class Ps5ParamJsonReader
private static (string? Title, string? TitleId, string? Version) TryReadPs5Param(JsonElement root)
{
if (root.ValueKind != JsonValueKind.Object)
return (null, null, null);
var titleId = GetString(root, "titleId");
string? titleId = root.TryGetProperty("titleId", out var eTid) ? eTid.GetString() : null;
string? ver =
GetString(root, "contentVersion")
?? GetString(root, "masterVersion")
?? GetString(root, "targetContentVersion");
(root.TryGetProperty("contentVersion", out var cv) ? cv.GetString() : null)
?? (root.TryGetProperty("masterVersion", out var mv) ? mv.GetString() : null)
?? (root.TryGetProperty("targetContentVersion", out var tv) ? tv.GetString() : null);
string? title = ExtractTitleName(root);
@@ -79,49 +70,34 @@ public static class Ps5ParamJsonReader
private static string? ExtractTitleName(JsonElement root)
{
if ((!root.TryGetProperty("localizedParameters", out var lp) || lp.ValueKind != JsonValueKind.Object) &&
root.TryGetProperty("disc", out var disc) && disc.ValueKind == JsonValueKind.Object)
if (!root.TryGetProperty("localizedParameters", out var lp))
{
disc.TryGetProperty("localizedParameters", out lp);
if (root.TryGetProperty("disc", out var disc) && disc.ValueKind == JsonValueKind.Object)
{
disc.TryGetProperty("localizedParameters", out lp);
}
}
if (lp.ValueKind != JsonValueKind.Object)
return null;
var defLang = GetString(lp, "defaultLanguage");
string? defLang = lp.TryGetProperty("defaultLanguage", out var dl) ? dl.GetString() : null;
if (!string.IsNullOrEmpty(defLang))
{
if (lp.TryGetProperty(defLang, out var langObj) && langObj.ValueKind == JsonValueKind.Object)
{
var title = GetString(langObj, "titleName");
if (!string.IsNullOrWhiteSpace(title))
return title;
if (langObj.TryGetProperty("titleName", out var tn))
return tn.GetString();
}
}
if (lp.TryGetProperty("en-US", out var en) && en.ValueKind == JsonValueKind.Object)
{
var title = GetString(en, "titleName");
if (!string.IsNullOrWhiteSpace(title))
return title;
}
foreach (var property in lp.EnumerateObject())
{
if (property.Value.ValueKind == JsonValueKind.Object)
{
var title = GetString(property.Value, "titleName");
if (!string.IsNullOrWhiteSpace(title))
return title;
}
if (en.TryGetProperty("titleName", out var tn2))
return tn2.GetString();
}
return null;
}
private static string? GetString(JsonElement parent, string propertyName) =>
parent.TryGetProperty(propertyName, out var value) && value.ValueKind == JsonValueKind.String
? value.GetString()
: null;
}
+6 -19
View File
@@ -17,9 +17,7 @@ namespace SharpEmu.Core.Loader;
public sealed class SelfLoader : ISelfLoader
{
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Loader");
private const uint ElfMagic = 0x7F454C46;
private const uint Ps4SelfMagic = 0x4F153D1D;
private const uint Ps5SelfMagic = 0x5414F5EE;
private const uint SelfMagic = 0x4F153D1D;
private const ulong SelfSegmentFlag = 0x800;
private const int PageSize = 0x1000;
private const ulong ImportStubBaseAddress = 0x0000_7000_0000_0000UL;
@@ -325,8 +323,7 @@ public sealed class SelfLoader : ISelfLoader
throw new InvalidDataException("Input image is too small to contain an ELF header.");
}
var magic = BinaryPrimitives.ReadUInt32BigEndian(imageData[..sizeof(uint)]);
if (magic is Ps4SelfMagic or Ps5SelfMagic)
if (imageData.Length >= sizeof(uint) && BinaryPrimitives.ReadUInt32BigEndian(imageData[..sizeof(uint)]) == SelfMagic)
{
var selfHeader = ReadUnmanaged<SelfHeader>(imageData, 0);
if (!selfHeader.HasKnownLayout || selfHeader.Unknown != 0x22)
@@ -348,12 +345,6 @@ public sealed class SelfLoader : ISelfLoader
return new LoadContext(IsSelf: true, elfOffset, selfHeader.FileSize, segments);
}
if (magic != ElfMagic)
{
throw new InvalidDataException(
$"Unsupported executable signature 0x{magic:X8}");
}
return new LoadContext(IsSelf: false, ElfOffset: 0, SelfFileSize: 0, Array.Empty<SelfSegment>());
}
@@ -2389,14 +2380,10 @@ public sealed class SelfLoader : ISelfLoader
public ulong FileSize => _fileSize;
public bool HasKnownLayout =>
((_ident0 == 0x4F &&
_ident1 == 0x15 &&
_ident2 == 0x3D &&
_ident3 == 0x1D) ||
(_ident0 == 0x54 &&
_ident1 == 0x14 &&
_ident2 == 0xF5 &&
_ident3 == 0xEE)) &&
_ident0 == 0x4F &&
_ident1 == 0x15 &&
_ident2 == 0x3D &&
_ident3 == 0x1D &&
_ident4 == 0x00 &&
_ident5 == 0x01 &&
_ident6 == 0x01 &&
+95 -249
View File
@@ -4,12 +4,11 @@
using System.Runtime.InteropServices;
using SharpEmu.Core.Loader;
using SharpEmu.HLE;
using SharpEmu.HLE.Host;
using SharpEmu.Logging;
namespace SharpEmu.Core.Memory;
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryAllocator, IGuestAddressSpace, IDisposable
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryAllocator, IDisposable
{
private static readonly SharpEmuLogger Log = SharpEmuLog.For("VMEM");
@@ -29,27 +28,41 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
private const ulong DefaultLazyReservePrimeBytes = 0x0400_0000UL; // 64 MiB
private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB
// Raw Windows PAGE_* values retained for the internal region/protection
// bookkeeping: regions and saved old-protection values always carry the raw
// value of the host platform in use, and these classification helpers only
// ever see values this class itself assigned (see IHostMemory.ProtectRaw).
private const uint MEM_COMMIT = 0x1000;
private const uint MEM_RESERVE = 0x2000;
private const uint MEM_RELEASE = 0x8000;
private const uint PAGE_EXECUTE_READ = 0x20;
private const uint PAGE_EXECUTE_READWRITE = 0x40;
private const uint PAGE_EXECUTE = 0x10;
private const uint PAGE_EXECUTE_WRITECOPY = 0x80;
private const uint PAGE_NOACCESS = 0x01;
private const uint PAGE_READWRITE = 0x04;
private const uint PAGE_READONLY = 0x02;
private readonly IHostMemory _hostMemory;
private ulong _guestAllocationArenaBase;
private readonly SortedDictionary<ulong, ulong> _guestAllocationFreeRanges = new();
private readonly Dictionary<ulong, (ulong Offset, ulong Size)> _guestAllocations = new();
private ulong _guestAllocationOffset;
private static readonly ulong LazyReservePrimeBytes = ResolveLazyReservePrimeBytes();
public PhysicalVirtualMemory(IHostMemory? hostMemory = null)
{
_hostMemory = hostMemory ?? HostPlatform.Current.Memory;
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern void* VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool VirtualFree(void* lpAddress, nuint dwSize, uint dwFreeType);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool VirtualProtect(void* lpAddress, nuint dwSize, uint flNewProtect, out uint lpflOldProtect);
[DllImport("kernel32.dll")]
private static extern nuint VirtualQuery(void* lpAddress, out MemoryBasicInformation64 lpBuffer, nuint dwLength);
[DllImport("kernel32.dll")]
private static extern void* GetCurrentProcess();
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
public bool TryAllocateAtExact(ulong desiredAddress, ulong size, bool executable, out ulong actualAddress)
{
@@ -61,17 +74,17 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
var alignedSize = (size + 0xFFF) & ~0xFFFUL;
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
var hostProtection = executable ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite;
var result = _hostMemory.Allocate(desiredAddress, alignedSize, hostProtection);
if (result == 0)
var allocationType = MEM_COMMIT | MEM_RESERVE;
var result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, allocationType, protection);
if (result == null)
{
return false;
}
actualAddress = result;
actualAddress = (ulong)result;
if (actualAddress != desiredAddress)
{
_hostMemory.Free(result);
VirtualFree(result, 0, MEM_RELEASE);
actualAddress = 0;
return false;
}
@@ -106,33 +119,33 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
var alignedSize = (size + 0xFFF) & ~0xFFFUL;
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
var hostProtection = executable ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite;
var allocationType = MEM_COMMIT | MEM_RESERVE;
var reservedOnly = false;
var preferReserveOnly = !executable &&
alignedSize >= LargeDataReserveThreshold &&
alignedSize > FullCommitRegionLimit;
ulong result = 0;
void* result = null;
if (preferReserveOnly)
{
result = _hostMemory.Reserve(desiredAddress, alignedSize, HostPageProtection.ReadWrite);
if (result == 0 && allowAlternative)
result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, MEM_RESERVE, PAGE_READWRITE);
if (result == null && allowAlternative)
{
result = _hostMemory.Reserve(0, alignedSize, HostPageProtection.ReadWrite);
result = VirtualAlloc(null, (nuint)alignedSize, MEM_RESERVE, PAGE_READWRITE);
}
if (result != 0)
if (result != null)
{
reservedOnly = true;
}
}
if (result == 0)
if (result == null)
{
result = _hostMemory.Allocate(desiredAddress, alignedSize, hostProtection);
result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, allocationType, protection);
}
if (result == 0)
if (result == null)
{
if (!allowAlternative)
{
@@ -140,32 +153,32 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
}
TraceVmem($"Could not allocate at 0x{desiredAddress:X16}, trying any address...");
result = _hostMemory.Allocate(0, alignedSize, hostProtection);
result = VirtualAlloc(null, (nuint)alignedSize, allocationType, protection);
if (result == 0)
if (result == null)
{
if (!executable)
{
result = _hostMemory.Reserve(desiredAddress, alignedSize, HostPageProtection.ReadWrite);
if (result == 0 && allowAlternative)
result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, MEM_RESERVE, PAGE_READWRITE);
if (result == null && allowAlternative)
{
result = _hostMemory.Reserve(0, alignedSize, HostPageProtection.ReadWrite);
result = VirtualAlloc(null, (nuint)alignedSize, MEM_RESERVE, PAGE_READWRITE);
}
if (result != 0)
if (result != null)
{
reservedOnly = true;
}
}
if (result == 0)
if (result == null)
{
throw new OutOfMemoryException($"Failed to allocate {alignedSize} bytes of virtual memory");
}
}
}
var actualAddress = result;
var actualAddress = (ulong)result;
var lazyPrimeState = "n/a";
if (reservedOnly)
@@ -178,8 +191,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
{
var remaining = primeBytes - committedBytes;
var chunkBytes = Math.Min(remaining, LazyReservePrimeChunkBytes);
var commitAddress = actualAddress + committedBytes;
if (!_hostMemory.Commit(commitAddress, chunkBytes, HostPageProtection.ReadWrite))
var commitAddress = (void*)(actualAddress + committedBytes);
var committed = VirtualAlloc(commitAddress, (nuint)chunkBytes, MEM_COMMIT, PAGE_READWRITE);
if (committed == null)
{
break;
}
@@ -302,9 +316,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
GuestAllocationArenaSize,
executable: false,
allowAlternative: true);
_guestAllocationFreeRanges.Add(
GuestAllocationArenaStartOffset,
GuestAllocationArenaSize - GuestAllocationArenaStartOffset);
_guestAllocationOffset = GuestAllocationArenaStartOffset;
}
catch (Exception)
{
@@ -312,128 +324,18 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
}
}
ulong rangeOffset = 0;
ulong rangeSize = 0;
ulong alignedOffset = 0;
var found = false;
foreach (var range in _guestAllocationFreeRanges)
{
alignedOffset = AlignUp(range.Key, alignment);
if (alignedOffset >= range.Key &&
alignedOffset - range.Key <= range.Value &&
size <= range.Value - (alignedOffset - range.Key))
{
rangeOffset = range.Key;
rangeSize = range.Value;
found = true;
break;
}
}
if (!found)
var alignedOffset = AlignUp(_guestAllocationOffset, alignment);
if (alignedOffset > GuestAllocationArenaSize || size > GuestAllocationArenaSize - alignedOffset)
{
return false;
}
_guestAllocationFreeRanges.Remove(rangeOffset);
if (alignedOffset > rangeOffset)
{
_guestAllocationFreeRanges.Add(rangeOffset, alignedOffset - rangeOffset);
}
var allocationEnd = alignedOffset + size;
var rangeEnd = rangeOffset + rangeSize;
if (allocationEnd < rangeEnd)
{
_guestAllocationFreeRanges.Add(allocationEnd, rangeEnd - allocationEnd);
}
address = _guestAllocationArenaBase + alignedOffset;
_guestAllocations.Add(address, (alignedOffset, size));
_guestAllocationOffset = alignedOffset + size;
return true;
}
}
public bool TryFreeGuestMemory(ulong address)
{
lock (_guestAllocationGate)
{
if (!_guestAllocations.Remove(address, out var allocation))
{
return false;
}
var freeOffset = allocation.Offset;
var freeSize = allocation.Size;
ulong? previousOffset = null;
ulong? nextOffset = null;
foreach (var range in _guestAllocationFreeRanges)
{
if (range.Key < freeOffset)
{
previousOffset = range.Key;
continue;
}
nextOffset = range.Key;
break;
}
if (previousOffset is { } previous &&
previous + _guestAllocationFreeRanges[previous] == freeOffset)
{
freeOffset = previous;
freeSize += _guestAllocationFreeRanges[previous];
_guestAllocationFreeRanges.Remove(previous);
}
if (nextOffset is { } next && freeOffset + freeSize == next)
{
freeSize += _guestAllocationFreeRanges[next];
_guestAllocationFreeRanges.Remove(next);
}
_guestAllocationFreeRanges.Add(freeOffset, freeSize);
return true;
}
}
public bool TryProtect(ulong address, ulong size, GuestPageProtection protection)
{
if (size == 0)
{
return false;
}
return _hostMemory.Protect(address, size, ResolveProtection(protection), out _);
}
// Reproduces the decomposition KernelMemoryCompatExports.ResolveHostProtection
// performed before this seam existed; the Windows backend maps each case back
// to the identical PAGE_* value.
private static HostPageProtection ResolveProtection(GuestPageProtection protection)
{
var read = (protection & GuestPageProtection.Read) != 0;
var write = (protection & GuestPageProtection.Write) != 0;
var execute = (protection & GuestPageProtection.Execute) != 0;
if (execute)
{
return write
? HostPageProtection.ReadWriteExecute
: read
? HostPageProtection.ReadExecute
: HostPageProtection.Execute;
}
return write
? HostPageProtection.ReadWrite
: read
? HostPageProtection.ReadOnly
: HostPageProtection.NoAccess;
}
public void Clear()
{
lock (_guestAllocationGate)
@@ -443,7 +345,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
{
foreach (var region in _regions)
{
_hostMemory.Free(region.VirtualAddress);
VirtualFree((void*)region.VirtualAddress, 0, MEM_RELEASE);
}
_regions.Clear();
_pageProtections.Clear();
@@ -458,8 +360,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
}
_guestAllocationArenaBase = 0;
_guestAllocationFreeRanges.Clear();
_guestAllocations.Clear();
_guestAllocationOffset = 0;
}
}
@@ -518,67 +419,46 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
private void ApplySegmentProtection(ulong mapStart, ulong mapEnd, ProgramHeaderFlags flags)
{
var runStart = mapStart;
var runFlags = ProgramHeaderFlags.None;
var hasRun = false;
for (var pageAddress = mapStart; pageAddress < mapEnd; pageAddress += PageSize)
{
_pageProtections.TryGetValue(pageAddress, out var existingFlags);
var mergedFlags = existingFlags | flags;
_pageProtections[pageAddress] = mergedFlags;
if (!hasRun)
{
runStart = pageAddress;
runFlags = mergedFlags;
hasRun = true;
}
else if (mergedFlags != runFlags)
{
SetProtection(runStart, pageAddress - runStart, runFlags);
runStart = pageAddress;
runFlags = mergedFlags;
}
}
if (hasRun)
{
SetProtection(runStart, mapEnd - runStart, runFlags);
SetProtection(pageAddress, PageSize, mergedFlags);
}
}
private void SetProtection(ulong address, ulong size, ProgramHeaderFlags flags)
{
HostPageProtection protection;
uint protection;
if (flags == ProgramHeaderFlags.None)
{
protection = HostPageProtection.NoAccess;
protection = PAGE_NOACCESS;
}
else if ((flags & ProgramHeaderFlags.Execute) != 0)
{
protection = (flags & ProgramHeaderFlags.Write) != 0
? HostPageProtection.ReadWriteExecute
: HostPageProtection.ReadExecute;
? PAGE_EXECUTE_READWRITE
: PAGE_EXECUTE_READ;
}
else if ((flags & ProgramHeaderFlags.Write) != 0)
{
protection = HostPageProtection.ReadWrite;
protection = PAGE_READWRITE;
}
else
{
protection = HostPageProtection.ReadOnly;
protection = PAGE_READONLY;
}
if (!_hostMemory.Protect(address, size, protection, out _))
if (!VirtualProtect((void*)address, (nuint)size, protection, out _))
{
throw new InvalidOperationException($"Failed to set memory protection at 0x{address:X16}");
}
if ((flags & ProgramHeaderFlags.Execute) != 0)
{
_hostMemory.FlushInstructionCache(address, size);
FlushInstructionCache(GetCurrentProcess(), (void*)address, (nuint)size);
}
}
@@ -670,47 +550,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
}
}
public bool TryCompare(ulong virtualAddress, ReadOnlySpan<byte> expected)
{
_gate.EnterReadLock();
try
{
var region = FindRegion(virtualAddress, (ulong)expected.Length);
if (region is null ||
!TryResolveRegionOffset(
virtualAddress,
(ulong)expected.Length,
region,
out var offset))
{
return false;
}
if (expected.IsEmpty)
{
return true;
}
var srcPtr = (void*)(region.VirtualAddress + offset);
if (region.IsReservedOnly &&
!EnsureRangeCommitted((ulong)srcPtr, (ulong)expected.Length, region))
{
return false;
}
if (!CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)expected.Length, region))
{
return false;
}
return new ReadOnlySpan<byte>(srcPtr, expected.Length).SequenceEqual(expected);
}
finally
{
_gate.ExitReadLock();
}
}
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
{
var requiresExclusiveAccess = false;
@@ -850,7 +689,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
return true;
}
if (!_hostMemory.Protect((ulong)destPtr, (ulong)source.Length, HostPageProtection.ReadWriteExecute, out var oldProtect))
if (!VirtualProtect(destPtr, (nuint)source.Length, PAGE_EXECUTE_READWRITE, out var oldProtect))
{
return false;
}
@@ -864,10 +703,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
}
finally
{
_hostMemory.ProtectRaw((ulong)destPtr, (ulong)source.Length, oldProtect, out _);
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _);
if (IsExecutableProtection(oldProtect))
{
_hostMemory.FlushInstructionCache((ulong)destPtr, (ulong)source.Length);
FlushInstructionCache(GetCurrentProcess(), destPtr, (nuint)source.Length);
}
}
@@ -889,14 +728,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
_gate.EnterReadLock();
try
{
var region = FindRegion(virtualAddress, 1);
if (region is null ||
(region.IsReservedOnly && !EnsureRangeCommitted(virtualAddress, 1, region)))
{
return null;
}
return (void*)virtualAddress;
return FindRegion(virtualAddress, 1) is not null
? (void*)virtualAddress
: null;
}
finally
{
@@ -1098,12 +932,12 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
return protection is PAGE_READWRITE or PAGE_EXECUTE_READWRITE;
}
private static HostPageProtection GetCommitProtection(MemoryRegion region)
private static uint GetCommitProtection(MemoryRegion region)
{
return region.IsExecutable ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite;
return region.IsExecutable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
}
private bool EnsureRangeCommitted(ulong address, ulong size, MemoryRegion region)
private static unsafe bool EnsureRangeCommitted(ulong address, ulong size, MemoryRegion region)
{
if (size == 0 || !region.IsReservedOnly)
{
@@ -1117,7 +951,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
var pageAddress = startPage;
while (pageAddress < endPage)
{
if (!_hostMemory.Query(pageAddress, out var info))
if (VirtualQuery((void*)pageAddress, out var info, (nuint)sizeof(MemoryBasicInformation64)) == 0)
{
return false;
}
@@ -1131,19 +965,19 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
return false;
}
if (info.State == HostRegionState.Committed)
if (info.State == MEM_COMMIT)
{
pageAddress = rangeEnd;
continue;
}
if (info.State != HostRegionState.Reserved)
if (info.State != MEM_RESERVE)
{
return false;
}
var commitSize = rangeEnd - pageAddress;
if (!_hostMemory.Commit(pageAddress, commitSize, commitProtection))
if (VirtualAlloc((void*)pageAddress, (nuint)commitSize, MEM_COMMIT, commitProtection) == null)
{
return false;
}
@@ -1164,11 +998,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
var startPage = AlignDown(address, PageSize);
var endPage = AlignUp(address + size, PageSize);
var temporaryProtection = region.IsExecutable ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite;
var temporaryProtection = region.IsExecutable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
for (var pageAddress = startPage; pageAddress < endPage; pageAddress += PageSize)
{
if (!_hostMemory.Protect(pageAddress, PageSize, temporaryProtection, out var oldProtection))
if (!VirtualProtect((void*)pageAddress, (nuint)PageSize, temporaryProtection, out var oldProtection))
{
RestorePageProtections(touchedPages);
touchedPages.Clear();
@@ -1181,11 +1015,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
return true;
}
private void RestorePageProtections(List<(ulong Address, uint Protection)> touchedPages)
private static void RestorePageProtections(List<(ulong Address, uint Protection)> touchedPages)
{
foreach (var (pageAddress, protection) in touchedPages)
{
_hostMemory.ProtectRaw(pageAddress, PageSize, protection, out _);
VirtualProtect((void*)pageAddress, (nuint)PageSize, protection, out _);
}
}
@@ -1242,4 +1076,16 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
public uint Protection { get; set; }
}
private struct MemoryBasicInformation64
{
public ulong BaseAddress;
public ulong AllocationBase;
public uint AllocationProtect;
public uint Alignment1;
public ulong RegionSize;
public uint State;
public uint Protect;
public uint Type;
public uint Alignment2;
}
}
+23 -110
View File
@@ -41,14 +41,15 @@ public sealed class VirtualMemory : IVirtualMemory
lock (_gate)
{
var insertionIndex = FindInsertionIndex(virtualAddress);
if ((insertionIndex > 0 && virtualAddress < _regions[insertionIndex - 1].EndAddress) ||
(insertionIndex < _regions.Count && endAddress > _regions[insertionIndex].Region.VirtualAddress))
foreach (var existing in _regions)
{
throw new InvalidOperationException("Attempted to map an overlapping virtual memory region.");
if (virtualAddress < existing.EndAddress && endAddress > existing.Region.VirtualAddress)
{
throw new InvalidOperationException("Attempted to map an overlapping virtual memory region.");
}
}
_regions.Insert(insertionIndex, new MappedRegion(
_regions.Add(new MappedRegion(
new VirtualMemoryRegion(virtualAddress, memorySize, fileOffset, (ulong)fileData.Length, protection),
endAddress,
backingMemory));
@@ -73,12 +74,12 @@ public sealed class VirtualMemory : IVirtualMemory
{
lock (_gate)
{
if (!TryValidateRange(virtualAddress, destination.Length, ProgramHeaderFlags.Read, out var regionIndex))
if (!TryResolveRegion(virtualAddress, destination.Length, out var region, out var offset))
{
return false;
}
CopyFromRegions(virtualAddress, destination, regionIndex);
region.BackingMemory.AsSpan(offset, destination.Length).CopyTo(destination);
return true;
}
}
@@ -87,127 +88,39 @@ public sealed class VirtualMemory : IVirtualMemory
{
lock (_gate)
{
if (!TryValidateRange(virtualAddress, source.Length, ProgramHeaderFlags.Write, out var regionIndex))
if (!TryResolveRegion(virtualAddress, source.Length, out var region, out var offset))
{
return false;
}
CopyToRegions(virtualAddress, source, regionIndex);
source.CopyTo(region.BackingMemory.AsSpan(offset, source.Length));
return true;
}
}
private bool TryValidateRange(
ulong virtualAddress,
int length,
ProgramHeaderFlags requiredProtection,
out int regionIndex)
private bool TryResolveRegion(ulong virtualAddress, int length, out MappedRegion region, out int offset)
{
regionIndex = FindContainingRegionIndex(virtualAddress);
if (regionIndex < 0)
foreach (var candidate in _regions)
{
return false;
}
var currentAddress = virtualAddress;
var remaining = length;
var currentIndex = regionIndex;
while (true)
{
if (currentIndex >= _regions.Count)
if (virtualAddress < candidate.Region.VirtualAddress || virtualAddress >= candidate.EndAddress)
{
return false;
continue;
}
var region = _regions[currentIndex];
if (currentAddress < region.Region.VirtualAddress ||
currentAddress >= region.EndAddress ||
(region.Region.Protection & requiredProtection) == 0)
var candidateOffset = checked((int)(virtualAddress - candidate.Region.VirtualAddress));
if (candidateOffset + length > candidate.BackingMemory.Length)
{
return false;
break;
}
if (remaining == 0)
{
return true;
}
var available = region.EndAddress - currentAddress;
var chunkLength = (int)Math.Min((ulong)remaining, available);
remaining -= chunkLength;
if (remaining == 0)
{
return true;
}
currentAddress += (ulong)chunkLength;
currentIndex++;
}
}
private int FindContainingRegionIndex(ulong virtualAddress)
{
var insertionIndex = FindInsertionIndex(virtualAddress);
if (insertionIndex < _regions.Count &&
_regions[insertionIndex].Region.VirtualAddress == virtualAddress)
{
return insertionIndex;
region = candidate;
offset = candidateOffset;
return true;
}
var candidateIndex = insertionIndex - 1;
return candidateIndex >= 0 && virtualAddress < _regions[candidateIndex].EndAddress
? candidateIndex
: -1;
}
private void CopyFromRegions(ulong virtualAddress, Span<byte> destination, int regionIndex)
{
var copied = 0;
var currentAddress = virtualAddress;
while (copied < destination.Length)
{
var region = _regions[regionIndex++];
var regionOffset = checked((int)(currentAddress - region.Region.VirtualAddress));
var chunkLength = Math.Min(destination.Length - copied, region.BackingMemory.Length - regionOffset);
region.BackingMemory.AsSpan(regionOffset, chunkLength).CopyTo(destination[copied..]);
copied += chunkLength;
currentAddress += (ulong)chunkLength;
}
}
private void CopyToRegions(ulong virtualAddress, ReadOnlySpan<byte> source, int regionIndex)
{
var copied = 0;
var currentAddress = virtualAddress;
while (copied < source.Length)
{
var region = _regions[regionIndex++];
var regionOffset = checked((int)(currentAddress - region.Region.VirtualAddress));
var chunkLength = Math.Min(source.Length - copied, region.BackingMemory.Length - regionOffset);
source.Slice(copied, chunkLength).CopyTo(region.BackingMemory.AsSpan(regionOffset, chunkLength));
copied += chunkLength;
currentAddress += (ulong)chunkLength;
}
}
private int FindInsertionIndex(ulong virtualAddress)
{
var lower = 0;
var upper = _regions.Count;
while (lower < upper)
{
var middle = lower + ((upper - lower) / 2);
if (_regions[middle].Region.VirtualAddress < virtualAddress)
{
lower = middle + 1;
}
else
{
upper = middle;
}
}
return lower;
region = default;
offset = 0;
return false;
}
private readonly record struct MappedRegion(VirtualMemoryRegion Region, ulong EndAddress, byte[] BackingMemory);
+3 -8
View File
@@ -7,7 +7,6 @@ using SharpEmu.Core.Cpu.Disasm;
using SharpEmu.Core.Loader;
using SharpEmu.Core.Memory;
using SharpEmu.HLE;
using SharpEmu.HLE.Host;
using SharpEmu.Libs.VideoOut;
using SharpEmu.Libs.Kernel;
using SharpEmu.Libs.AppContent;
@@ -84,22 +83,18 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
ImportTraceLimit = Math.Max(0, options.ImportTraceLimit),
};
var moduleManager = new ModuleManager();
moduleManager.RegisterFromAssembly(typeof(VideoOutExports).Assembly, Generation.Gen4 | Generation.Gen5, Aerolib.Instance);
moduleManager.RegisterFromAssembly(typeof(KernelExports).Assembly, Generation.Gen4 | Generation.Gen5, Aerolib.Instance);
moduleManager.Freeze();
// Resolve the host platform once at the composition root; on unsupported
// OSes this throws PlatformNotSupportedException with a clear message
// instead of failing on the first native call.
var hostPlatform = HostPlatform.Current;
var virtualMemory = new PhysicalVirtualMemory(hostPlatform.Memory);
var virtualMemory = new PhysicalVirtualMemory();
var fileSystem = new PhysicalFileSystem();
return new SharpEmuRuntime(
new SelfLoader(),
virtualMemory,
new CpuDispatcher(virtualMemory, moduleManager, hostPlatform: hostPlatform),
new CpuDispatcher(virtualMemory, moduleManager),
moduleManager,
Aerolib.Instance,
cpuExecutionOptions,
-28
View File
@@ -127,34 +127,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
</Style>
<!-- Top-level page switcher (Library / Options): plain transparent
buttons, not TabItem, so there is no Fluent selected-tab underline.
The active page is conveyed by brightness alone; LB/RB gamepad
hints flank the pair. -->
<Style Selector="Button.segment">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Foreground" Value="{StaticResource MutedBrush}" />
<Setter Property="FontSize" Value="22" />
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="Padding" Value="6,4" />
<Setter Property="CornerRadius" Value="8" />
</Style>
<Style Selector="Button.segment:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
</Style>
<Style Selector="Button.segment.active">
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
</Style>
<!-- Gamepad shoulder-button hint chip (LB/RB, L1/R1). -->
<Style Selector="Border.padHint">
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
<Setter Property="CornerRadius" Value="6" />
<Setter Property="Padding" Value="8,3" />
</Style>
<Style Selector="ContextMenu">
<Setter Property="Background" Value="{StaticResource CardBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
+6 -18
View File
@@ -27,10 +27,8 @@ public sealed class ConsoleWindow : Window
Action clear,
bool autoScroll)
{
var loc = Localization.Instance;
_sourceLines = lines;
Title = loc.Get("Console.WindowTitle");
Title = "SharpEmu Console";
Width = 980;
Height = 620;
MinWidth = 520;
@@ -38,15 +36,10 @@ public sealed class ConsoleWindow : Window
Background = new SolidColorBrush(Color.Parse("#0D1017"));
Icon = new WindowIcon(AssetLoader.Open(new Uri("avares://SharpEmu.GUI/Assets/SharpEmu.ico")));
_searchBox = new TextBox
{
Watermark = loc.Get("Console.SearchWatermark"),
Width = 320,
Margin = new Thickness(0, 0, 12, 0),
};
_searchBox = new TextBox { Watermark = "Search...", Width = 320, Margin = new Thickness(0, 0, 12, 0) };
_autoScrollCheck = new CheckBox
{
Content = loc.Get("Console.AutoScroll"),
Content = "Auto-scroll",
IsChecked = autoScroll,
FontSize = 12,
Margin = new Thickness(0, 0, 12, 0),
@@ -55,16 +48,11 @@ public sealed class ConsoleWindow : Window
var copyButton = new Button
{
Classes = { "ghost" },
Content = loc.Get("Console.Copy"),
Content = "Copy",
Padding = new Thickness(10, 4),
Margin = new Thickness(0, 0, 8, 0),
};
var clearButton = new Button
{
Classes = { "ghost" },
Content = loc.Get("Console.Clear"),
Padding = new Thickness(10, 4),
};
var clearButton = new Button { Classes = { "ghost" }, Content = "Clear", Padding = new Thickness(10, 4) };
copyButton.Click += async (_, _) => await CopyAsync();
clearButton.Click += (_, _) => clear();
_searchBox.TextChanged += (_, _) => RefreshVisibleLines();
@@ -99,7 +87,7 @@ public sealed class ConsoleWindow : Window
new TextBlock
{
Classes = { "sectionTitle" },
Text = loc.Get("Console.Title"),
Text = "CONSOLE",
VerticalAlignment = VerticalAlignment.Center,
},
_searchBox.WithGridColumn(1),
+5 -16
View File
@@ -29,12 +29,10 @@ public sealed class GameEntry : INotifyPropertyChanged
private long _sizeBytes;
public GameEntry(
string name, string? titleId, string? version, string path, long sizeBytes,
string? coverPath, string? backgroundPath)
string name, string? titleId, string path, long sizeBytes, string? coverPath, string? backgroundPath)
{
Name = name;
TitleId = titleId;
Version = version;
Path = path;
_sizeBytes = sizeBytes;
CoverPath = coverPath;
@@ -48,9 +46,6 @@ public sealed class GameEntry : INotifyPropertyChanged
public string? TitleId { get; }
/// <summary>Content version from sce_sys/param.json, e.g. "01.000.000".</summary>
public string? Version { get; }
public string Path { get; }
/// <summary>
@@ -70,7 +65,7 @@ public sealed class GameEntry : INotifyPropertyChanged
_sizeBytes = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SizeBytes)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SizeText)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Detail)));
}
}
@@ -111,15 +106,9 @@ public sealed class GameEntry : INotifyPropertyChanged
public bool HasCover => _cover is not null;
public bool HasTitleId => TitleId is not null;
/// <summary>Badge text shown in the launch bar, e.g. "v01.000.000".</summary>
public string? VersionText => Version is null ? null : $"v{Version}";
public bool HasVersion => Version is not null;
/// <summary>Formatted install size badge shown in the launch bar.</summary>
public string SizeText => FormatSize(SizeBytes);
public string Detail => TitleId is not null
? $"{TitleId} • {FormatSize(SizeBytes)}"
: FormatSize(SizeBytes);
private static string ComputeInitials(string name)
{
-6
View File
@@ -42,15 +42,9 @@ public sealed class GuiSettings
public string? EmulatorPath { get; set; }
/// <summary>UI language, matching a file code under Languages/ (e.g. "en", "tr").</summary>
public string Language { get; set; } = "en";
/// <summary>Publish launcher/game status to Discord Rich Presence.</summary>
public bool DiscordRichPresence { get; set; } = true;
/// <summary>Names of SHARPEMU_* switches set to "1" in the emulator's environment at launch.</summary>
public List<string> EnvironmentToggles { get; set; } = new();
/// <summary>
/// Discord application ID used for Rich Presence; the default is the
/// SharpEmu application. Override to rebrand what Discord shows as
-129
View File
@@ -1,129 +0,0 @@
{
"_languageName": "العربية",
"Page.Library": "المكتبة",
"Page.Options": "الخيارات",
"Page.GameCount.One": "لعبة واحدة",
"Page.GameCount.Other": "{0} لعبة",
"Library.SearchWatermark": "ابحث في المكتبة...",
"Library.AddFolder": "+ إضافة مجلد",
"Library.Rescan": "⟳ إعادة الفحص",
"Library.OpenFile": "فتح ملف...",
"Library.Context.Launch": "تشغيل",
"Library.Context.OpenFolder": "فتح مجلد اللعبة",
"Library.Context.CopyPath": "نسخ المسار",
"Library.Context.CopyTitleId": "نسخ معرف العنوان",
"Library.Context.Remove": "إزالة من المكتبة",
"Library.Empty.Title": "مكتبتك فارغة",
"Library.Empty.Hint": "أضف مجلداً يحتوي على ألعابك للبدء.",
"Library.Empty.SearchTitle": "لا توجد ألعاب تطابق بحثك",
"Library.Empty.SearchHint": "لا شيء في المكتبة يطابق “{0}”.",
"Library.Empty.AddFolder": "+ إضافة مجلد ألعاب",
"Library.Loading": "جارٍ تحميل المكتبة...",
"Options.General": "عام",
"Options.Section.Emulation": "المحاكاة",
"Options.Section.Logging": "التسجيل",
"Options.Section.Launcher": "المُشغِّل",
"Options.CpuEngine.Label": "محرك المعالج",
"Options.CpuEngine.Desc": "محرك التنفيذ المستخدم لتشغيل كود اللعبة.",
"Options.CpuEngine.Native": "أصلي",
"Options.Strict.Label": "ربط صارم للمكتبات الديناميكية (dynlib)",
"Options.Strict.Desc": "إفشال التشغيل عندما يتعذر التعرف على رمز مستورد.",
"Options.LogLevel.Label": "مستوى التسجيل",
"Options.LogLevel.Desc": "مدى تفصيل مخرجات نافذة سجلات المحاكي.",
"Options.LogLevel.Trace": "تتبع",
"Options.LogLevel.Debug": "تصحيح الأخطاء",
"Options.LogLevel.Info": "معلومات",
"Options.LogLevel.Warning": "تحذير",
"Options.LogLevel.Error": "خطأ",
"Options.LogLevel.Critical": "حرج",
"Options.TraceImports.Label": "حد تتبع الاستيراد",
"Options.TraceImports.Desc": "تتبع أول N استيراد لكل وحدة (0 = إيقاف).",
"Options.LogToFile.Label": "التسجيل في ملف",
"Options.LogToFile.Desc": "نسخ مخرجات المحاكي إلى ملف سجل.",
"Options.LogFilePath.Label": "مسار ملف السجل",
"Options.LogFilePath.Default": "لا يوجد مسار مخصص — تُحفظ السجلات في user/logs بجوار المحاكي.",
"Options.LogFilePath.Select": "تحديد...",
"Options.OverrideLogFile.Label": "الكتابة فوق ملف السجل",
"Options.OverrideLogFile.Desc": "استخدام مسار الملف الدقيق بدلاً من إلحاق معرف العنوان والطابع الزمني.",
"Options.TitleMusic.Label": "موسيقى اللعبة",
"Options.TitleMusic.Desc": "تكرار موسيقى المعاينة للعبة المحددة في المكتبة.",
"Options.Discord.Label": "حالة دسكورد",
"Options.Discord.Desc": "إظهار اللعبة قيد التشغيل في ملفك الشخصي على دسكورد.",
"Options.Language.Label": "لغة المحاكي",
"Options.Language.Desc": "اللغة المستخدمة في جميع أنحاء المشغل. تُطبق فوراً.",
"Common.On": "تشغيل",
"Common.Off": "إيقاف",
"Console.Title": "نافذة السجلات",
"Console.SearchWatermark": "بحث...",
"Console.AutoScroll": "تمرير تلقائي",
"Console.Split": "تقسيم",
"Console.Copy": "نسخ",
"Console.Clear": "مسح",
"Console.WindowTitle": "نافذة سجلات SharpEmu",
"Launch.NoGameSelected": "لم تُحدد أي لعبة",
"Launch.NoGameHint": "اختر لعبة من المكتبة، أو افتح ملف eboot.bin مباشرة.",
"Launch.Idle": "خامل",
"Launch.Console": "≡ نافذة السجلات",
"Launch.Launch": "▶ تشغيل",
"Launch.Stop": "■ إيقاف",
"Launch.Running": "قيد التشغيل — {0}",
"Launch.Stopping": "جارٍ الإيقاف...",
"Launch.Exited": "انتهى برمز {0} ({1})",
"Launch.ExeNotFound": "لم يُعثر على الملف التنفيذي SharpEmu. ابنِ مشروع SharpEmu.CLI أولاً (dotnet build).",
"Launch.LogFile": "ملف السجل: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "فشل بدء تشغيل المحاكي: {0}",
"Launch.ProcessExited": "انتهت العملية برمز {0} ({1}).",
"Exit.Ok": "موافق",
"Exit.InvalidArguments": "معطيات غير صالحة",
"Exit.EbootNotFound": "لم يُعثر على eboot",
"Exit.RuntimeException": "استثناء وقت التشغيل",
"Exit.EmulationError": "خطأ في المحاكاة",
"Exit.Unknown": "غير معروف",
"Status.EmulatorLocating": "المحاكي: جارٍ تحديد الموقع...",
"Status.EmulatorPath": "المحاكي: {0}",
"Status.EmulatorNotFound": "المحاكي: لم يُعثر على الملف التنفيذي SharpEmu — ابنِ SharpEmu.CLI أولاً.",
"Status.ScanningLibrary": "جارٍ فحص المكتبة...",
"Status.AddFolderPrompt": "أضف مجلد ألعاب لملء المكتبة.",
"Status.LibraryScanned": "فُحصت المكتبة: {0} لعبة في {1} مجلد.",
"Status.CouldNotOpenFolder": "تعذر فتح المجلد: {0}",
"Status.CopiedToClipboard": "نُسخ {0} إلى الحافظة.",
"Status.RemovedFromLibrary": "أُزيل “{0}” من المكتبة. أعد إضافة مجلده لاستعادته.",
"Status.Running": "جارٍ تشغيل {0}",
"Status.Stopping": "جارٍ الإيقاف...",
"Status.Idle": "خامل",
"Clipboard.Path": "المسار",
"Clipboard.TitleId": "معرف العنوان",
"Discord.Playing": "يلعب {0}",
"Discord.Browsing": "يتصفح المكتبة",
"Dialog.ChooseGameFolder": "اختر مجلداً يحتوي على ألعاب",
"Dialog.OpenExecutable": "افتح ملفاً تنفيذياً لتشغيله",
"Dialog.PsExecutables": "ملفات PS التنفيذية",
"Dialog.SaveLogFile": "حدد مكان حفظ ملف السجل",
"Dialog.PlainTextFiles": "ملفات نصية عادية",
"Dialog.LogFiles": "ملفات السجل"
}
-138
View File
@@ -1,138 +0,0 @@
{
"_languageName": "Português (Brasil)",
"Page.Library": "Biblioteca",
"Page.Options": "Opções",
"Page.GameCount.One": "1 Jogo",
"Page.GameCount.Other": "{0} jogos",
"Library.SearchWatermark": "Pesquisar na biblioteca…",
"Library.AddFolder": " Adicionar pasta",
"Library.Rescan": "⟳ Atualizar biblioteca",
"Library.OpenFile": "Abrir arquivo…",
"Library.Context.Launch": "Jogar",
"Library.Context.OpenFolder": "Abrir pasta do jogo",
"Library.Context.CopyPath": "Copiar o caminho",
"Library.Context.CopyTitleId": "Copiar ID do título",
"Library.Context.Remove": "Remover da biblioteca",
"Library.Empty.Title": "Sua biblioteca está vazia",
"Library.Empty.Hint": "Adicione uma pasta contendo seus jogos para começar.",
"Library.Empty.SearchTitle": "Nenhum jogo corresponde à sua busca",
"Library.Empty.SearchHint": "Nada na biblioteca corresponde a “{0}”.",
"Library.Empty.AddFolder": " Adicionar pasta do jogo",
"Library.Loading": "Carregando biblioteca…",
"Options.General": "Opções Gerais",
"Options.Env.Tab": "Ambiente",
"Options.Section.Environment": "VARIÁVEIS DE AMBIENTE",
"Options.Env.Desc": "Switches passados para o emulador como variáveis de ambiente na inicialização.",
"Options.Env.Bthid.Desc": "Reporta o Bluetooth HID como indisponível para títulos cujo middleware de volante/FFB fica esperando indefinidamente.\nDeixe desativado normalmente. Alguns títulos travam quando a inicialização falha.",
"Options.Env.LoopGuard.Desc": "Não force o encerramento de títulos que repetem a mesma chamada por tempo demais.\nExperimente isso quando um jogo fecha sozinho durante o carregamento.",
"Options.Env.VkValidation.Desc": "Ativa as camadas de validação do Vulkan para depuração de GPU.\nLento. Requer que o Vulkan SDK esteja instalado.",
"Options.Env.DumpSpirv.Desc": "Extrai os shaders AGC e suas traduções SPIR-V para a pasta shader-dumps.\nUse ao reportar bugs de shader ou renderização.",
"Options.Env.LogDirectMemory.Desc": "Registra alocações de memória direta e falhas no console.\nUse quando um jogo aborta ou fecha durante a inicialização (boot).",
"Options.Env.LogNp.Desc": "Registra chamadas da biblioteca NP (PlayStation Network) no console.",
"Options.Section.Emulation": "EMULAÇÃO",
"Options.Section.Logging": "LOGS",
"Options.Section.Launcher": "INICIALIZADOR",
"Options.CpuEngine.Label": "Motor da CPU",
"Options.CpuEngine.Desc": "Motor de execução usado para executar o código do jogo.",
"Options.CpuEngine.Native": "Nativo",
"Options.Strict.Label": "Resolução estrita de bibliotecas dinâmicas",
"Options.Strict.Desc": "Interrompe a inicialização caso um símbolo importado não possa ser vinculado.",
"Options.LogLevel.Label": "Nível de log",
"Options.LogLevel.Desc": "Nível de detalhamento das mensagens exibidas no console do emulador.",
"Options.LogLevel.Trace": "Trace",
"Options.LogLevel.Debug": "Debug",
"Options.LogLevel.Info": "Info",
"Options.LogLevel.Warning": "Warning",
"Options.LogLevel.Error": "Error",
"Options.LogLevel.Critical": "Critical",
"Options.TraceImports.Label": "Limite de rastreamento de importações",
"Options.TraceImports.Desc": "Rastreia as primeiras N importações de cada módulo (0 = desativado).",
"Options.LogToFile.Label": "Salvar log em arquivo",
"Options.LogToFile.Desc": "Copia a saída do emulador para um arquivo de log.",
"Options.LogFilePath.Label": "Caminho do arquivo de log",
"Options.LogFilePath.Default": "Nenhum caminho definido — logs vão para user/logs na pasta do emulador.",
"Options.LogFilePath.Select": "Selecionar…",
"Options.OverrideLogFile.Label": "Sobrescrever arquivo de log",
"Options.OverrideLogFile.Desc": "Use o caminho exato do arquivo em vez de adicionar o ID do título e o log de data e hora.",
"Options.TitleMusic.Label": "Música de prévia",
"Options.TitleMusic.Desc": "Reproduz em loop a música de prévia do jogo selecionado na biblioteca.",
"Options.Discord.Label": "Status do Discord",
"Options.Discord.Desc": "Exibir o jogo em execução no seu perfil do Discord.",
"Options.Language.Label": "Idioma do emulador",
"Options.Language.Desc": "Idioma usado em toda a interface do emulador. A alteração é aplicada imediatamente.",
"Common.On": "Ativado",
"Common.Off": "Desativado",
"Console.Title": "CONSOLE",
"Console.SearchWatermark": "Pesquisar...",
"Console.AutoScroll": "Rolagem automática",
"Console.Split": "Recortar",
"Console.Copy": "Copiar",
"Console.Clear": "Limpar",
"Console.WindowTitle": "Console do SharpEmu",
"Launch.NoGameSelected": "Nenhum jogo selecionado",
"Launch.NoGameHint": "Selecione um jogo na biblioteca ou abra um arquivo eboot.bin diretamente.",
"Launch.Idle": "Ocioso",
"Launch.Console": "≡ Console",
"Launch.Launch": "▶ Iniciar",
"Launch.Stop": "■ Parar",
"Launch.Running": "Em execução — {0}",
"Launch.Stopping": "Encerrando…",
"Launch.Exited": "Encerrado com código {0} ({1})",
"Launch.ExeNotFound": "Executável do SharpEmu não encontrado. Compile primeiro o projeto SharpEmu.CLI (dotnet build).",
"Launch.LogFile": "Arquivo de log: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Falha ao iniciar o emulador: {0}",
"Launch.ProcessExited": "O processo foi encerrado com código {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "argumentos inválidos",
"Exit.EbootNotFound": "eboot.bin não encontrado",
"Exit.RuntimeException": "exceção em tempo de execução",
"Exit.EmulationError": "erro de emulação",
"Exit.Unknown": "desconhecido",
"Status.EmulatorLocating": "Emulador: localizando…",
"Status.EmulatorPath": "Emulador: {0}",
"Status.EmulatorNotFound": "Emulador: executável do SharpEmu não encontrado — compile o SharpEmu.CLI primeiro.",
"Status.ScanningLibrary": "Verificando biblioteca…",
"Status.AddFolderPrompt": "Adicione uma pasta de jogos para preencher a biblioteca.",
"Status.LibraryScanned": "Biblioteca verificada: {0} jogo(s) em {1} pasta(s).",
"Status.CouldNotOpenFolder": "Não foi possível abrir a pasta: {0}",
"Status.CopiedToClipboard": "{0} copiado para a área de transferência.",
"Status.RemovedFromLibrary": "“{0}” removido da biblioteca. Adicione novamente sua pasta para restaurá-lo.",
"Status.Running": "Executando {0}",
"Status.Stopping": "Encerrando…",
"Status.Idle": "Ocioso",
"Clipboard.Path": "Caminho",
"Clipboard.TitleId": "ID do título",
"Discord.Playing": "Jogando {0}",
"Discord.Browsing": "Navegando pela biblioteca",
"Dialog.ChooseGameFolder": "Escolha uma pasta contendo jogos",
"Dialog.OpenExecutable": "Abrir um executável para iniciar",
"Dialog.PsExecutables": "Executáveis de PS",
"Dialog.SaveLogFile": "Selecione onde salvar o arquivo de log",
"Dialog.PlainTextFiles": "Arquivos de texto simples",
"Dialog.LogFiles": "Arquivos de log"
}
-129
View File
@@ -1,129 +0,0 @@
{
"_languageName": "Deutsch",
"Page.Library": "Bibliothek",
"Page.Options": "Optionen",
"Page.GameCount.One": "1 Spiel",
"Page.GameCount.Other": "{0} Spiele",
"Library.SearchWatermark": "Bibliothek durchsuchen…",
"Library.AddFolder": " Spielordner hinzufügen",
"Library.Rescan": "⟳ Neu scannen",
"Library.OpenFile": "Datei öffnen…",
"Library.Context.Launch": "Starten",
"Library.Context.OpenFolder": "Spielordner öffnen",
"Library.Context.CopyPath": "Pfad kopieren",
"Library.Context.CopyTitleId": "Title ID kopieren",
"Library.Context.Remove": "Aus Bibliothek entfernen",
"Library.Empty.Title": "Deine Bibliothek ist leer",
"Library.Empty.Hint": "Füge einen Ordner mit deinen Spielen hinzu, um zu beginnen.",
"Library.Empty.SearchTitle": "Keine Spiele gefunden",
"Library.Empty.SearchHint": "Nichts in der Bibliothek entspricht “{0}”.",
"Library.Empty.AddFolder": " Spielordner hinzufügen",
"Library.Loading": "Bibliothek wird geladen…",
"Options.General": "Allgemein",
"Options.Section.Emulation": "EMULATION",
"Options.Section.Logging": "PROTOKOLLIERUNG",
"Options.Section.Launcher": "LAUNCHER",
"Options.CpuEngine.Label": "CPU-Engine",
"Options.CpuEngine.Desc": "Ausführungs-Engine, die zum Ausführen des Spiel-Codes verwendet wird.",
"Options.CpuEngine.Native": "Nativ",
"Options.Strict.Label": "Strikte dynlib-Auflösung",
"Options.Strict.Desc": "Starten abbrechen, wenn ein importiertes Symbol nicht aufgelöst werden kann.",
"Options.LogLevel.Label": "Protokollstufe",
"Options.LogLevel.Desc": "Ausführlichkeit der Emulator-Konsolenausgabe.",
"Options.LogLevel.Trace": "Trace",
"Options.LogLevel.Debug": "Debug",
"Options.LogLevel.Info": "Info",
"Options.LogLevel.Warning": "Warnung",
"Options.LogLevel.Error": "Fehler",
"Options.LogLevel.Critical": "Kritisch",
"Options.TraceImports.Label": "Import-Trace-Limit",
"Options.TraceImports.Desc": "Die ersten N Imports pro Modul verfolgen (0 = aus).",
"Options.LogToFile.Label": "In Datei protokollieren",
"Options.LogToFile.Desc": "Emulator-Ausgabe zusätzlich in eine Log-Datei schreiben.",
"Options.LogFilePath.Label": "Protokolldatei-Pfad",
"Options.LogFilePath.Default": "Kein benutzerdefinierter Pfad Logs werden im Ordner user/logs neben dem Emulator gespeichert.",
"Options.LogFilePath.Select": "Auswählen…",
"Options.OverrideLogFile.Label": "Protokolldatei überschreiben",
"Options.OverrideLogFile.Desc": "Genauen Dateipfad verwenden, statt Title-ID und Zeitstempel anzuhängen.",
"Options.TitleMusic.Label": "Titel-Musik",
"Options.TitleMusic.Desc": "Die Vorschau-Musik des ausgewählten Spiels in der Bibliothek loopend abspielen.",
"Options.Discord.Label": "Discord-Präsenz",
"Options.Discord.Desc": "Zeigt das aktuell gespielte Spiel in deinem Discord-Profil an.",
"Options.Language.Label": "Emulator-Sprache",
"Options.Language.Desc": "Sprache der Benutzeroberfläche. Wird sofort angewendet.",
"Common.On": "An",
"Common.Off": "Aus",
"Console.Title": "KONSOLE",
"Console.SearchWatermark": "Suchen...",
"Console.AutoScroll": "Auto-Scroll",
"Console.Split": "Teilen",
"Console.Copy": "Kopieren",
"Console.Clear": "Leeren",
"Console.WindowTitle": "SharpEmu Konsole",
"Launch.NoGameSelected": "Kein Spiel ausgewählt",
"Launch.NoGameHint": "Wähle ein Spiel aus der Bibliothek aus oder öffne eine eboot.bin direkt.",
"Launch.Idle": "Bereit",
"Launch.Console": "≡ Konsole",
"Launch.Launch": "▶ Starten",
"Launch.Stop": "■ Stoppen",
"Launch.Running": "Läuft -- {0}",
"Launch.Stopping": "Wird beendet…",
"Launch.Exited": "Beendet mit Code {0} ({1})",
"Launch.ExeNotFound": "SharpEmu-Executable wurde nicht gefunden. Baue zuerst das SharpEmu.CLI-Projekt (dotnet build).",
"Launch.LogFile": "Log-Datei: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Emulator konnte nicht gestartet werden: {0}",
"Launch.ProcessExited": "Prozess wurde mit Code {0} ({1}) beendet.",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "Ungültige Argumente",
"Exit.EbootNotFound": "eboot nicht gefunden",
"Exit.RuntimeException": "Laufzeitfehler",
"Exit.EmulationError": "Emulationsfehler",
"Exit.Unknown": "unbekannt",
"Status.EmulatorLocating": "Emulator: wird gesucht…",
"Status.EmulatorPath": "Emulator: {0}",
"Status.EmulatorNotFound": "Emulator: SharpEmu-Executable nicht gefunden -- baue zuerst SharpEmu.CLI.",
"Status.ScanningLibrary": "Bibliothek wird gescannt…",
"Status.AddFolderPrompt": "Füge einen Spielordner hinzu, um die Bibliothek zu füllen.",
"Status.LibraryScanned": "Bibliothek gescannt: {0} Spiel(e) in {1} Ordner(n).",
"Status.CouldNotOpenFolder": "Ordner konnte nicht geöffnet werden: {0}",
"Status.CopiedToClipboard": "{0} in die Zwischenablage kopiert.",
"Status.RemovedFromLibrary": "“{0}” wurde aus der Bibliothek entfernt. Füge den Ordner erneut hinzu, um es wiederherzustellen.",
"Status.Running": "Läuft {0}",
"Status.Stopping": "Wird gestoppt…",
"Status.Idle": "Bereit",
"Clipboard.Path": "Pfad",
"Clipboard.TitleId": "Title ID",
"Discord.Playing": "Spielt {0}",
"Discord.Browsing": "Durchsucht die Bibliothek",
"Dialog.ChooseGameFolder": "Spielordner auswählen",
"Dialog.OpenExecutable": "Ausführbare Datei zum Starten öffnen",
"Dialog.PsExecutables": "PS-Ausführbare Dateien",
"Dialog.SaveLogFile": "Protokolldatei speichern unter",
"Dialog.PlainTextFiles": "Textdateien",
"Dialog.LogFiles": "Protokolldateien"
}
-129
View File
@@ -1,129 +0,0 @@
{
"_languageName": "Dansk",
"Page.Library": "Bibliotek",
"Page.Options": "Indstillinger",
"Page.GameCount.One": "1 spil",
"Page.GameCount.Other": "{0} spil",
"Library.SearchWatermark": "Søg i biblioteket…",
"Library.AddFolder": " Tilføj mappe",
"Library.Rescan": "⟳ Genindlæs",
"Library.OpenFile": "Åbn fil…",
"Library.Context.Launch": "Start",
"Library.Context.OpenFolder": "Åbn spilmappe",
"Library.Context.CopyPath": "Kopiér sti",
"Library.Context.CopyTitleId": "Kopiér titel-ID",
"Library.Context.Remove": "Fjern fra bibliotek",
"Library.Empty.Title": "Dit bibliotek er tomt",
"Library.Empty.Hint": "Tilføj en mappe med dine spil for at komme i gang.",
"Library.Empty.SearchTitle": "Ingen spil matcher din søgning",
"Library.Empty.SearchHint": "Intet i biblioteket matcher “{0}”.",
"Library.Empty.AddFolder": " Tilføj spilmappe",
"Library.Loading": "Indlæser bibliotek…",
"Options.General": "Generelt",
"Options.Section.Emulation": "EMULERING",
"Options.Section.Logging": "LOGGING",
"Options.Section.Launcher": "LAUNCHER",
"Options.CpuEngine.Label": "CPU-engine",
"Options.CpuEngine.Desc": "Den eksekveringsengine der bruges til at køre spilkode.",
"Options.CpuEngine.Native": "Native",
"Options.Strict.Label": "Streng dynlib-opløsning",
"Options.Strict.Desc": "Afbryd opstarten, når et importeret symbol ikke kan findes.",
"Options.LogLevel.Label": "Logniveau",
"Options.LogLevel.Desc": "Detaljeringsgrad for emulatorens konsoloutput.",
"Options.LogLevel.Trace": "Trace",
"Options.LogLevel.Debug": "Debug",
"Options.LogLevel.Info": "Info",
"Options.LogLevel.Warning": "Advarsel",
"Options.LogLevel.Error": "Fejl",
"Options.LogLevel.Critical": "Kritisk",
"Options.TraceImports.Label": "Grænse for import-trace",
"Options.TraceImports.Desc": "Spor de første N imports pr. modul (0 = fra).",
"Options.LogToFile.Label": "Log til fil",
"Options.LogToFile.Desc": "Spejl emulatorens output til en logfil.",
"Options.LogFilePath.Label": "Sti til logfil",
"Options.LogFilePath.Default": "Ingen brugerdefineret sti — logs gemmes i user/logs ved siden af emulatoren.",
"Options.LogFilePath.Select": "Vælg…",
"Options.OverrideLogFile.Label": "Tilsidesæt logfil",
"Options.OverrideLogFile.Desc": "Brug den præcise filsti i stedet for at tilføje titel-ID og tidsstempel.",
"Options.TitleMusic.Label": "Titelmusik",
"Options.TitleMusic.Desc": "Gentag det valgte spils forhåndsvisningsmusik i biblioteket.",
"Options.Discord.Label": "Discord-tilstedeværelse",
"Options.Discord.Desc": "Vis det kørende spil på din Discord-profil.",
"Options.Language.Label": "Emulatorsprog",
"Options.Language.Desc": "Sprog der bruges i hele launcheren. Anvendes med det samme.",
"Common.On": "Til",
"Common.Off": "Fra",
"Console.Title": "KONSOL",
"Console.SearchWatermark": "Søg...",
"Console.AutoScroll": "Auto-scroll",
"Console.Split": "Opdel",
"Console.Copy": "Kopiér",
"Console.Clear": "Ryd",
"Console.WindowTitle": "SharpEmu-konsol",
"Launch.NoGameSelected": "Intet spil valgt",
"Launch.NoGameHint": "Vælg et spil fra biblioteket, eller åbn en eboot.bin direkte.",
"Launch.Idle": "Inaktiv",
"Launch.Console": "≡ Konsol",
"Launch.Launch": "▶ Start",
"Launch.Stop": "■ Stop",
"Launch.Running": "Kører — {0}",
"Launch.Stopping": "Stopper…",
"Launch.Exited": "Afsluttet med kode {0} ({1})",
"Launch.ExeNotFound": "SharpEmu-programmet blev ikke fundet. Byg SharpEmu.CLI-projektet først (dotnet build).",
"Launch.LogFile": "Logfil: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Kunne ikke starte emulatoren: {0}",
"Launch.ProcessExited": "Processen afsluttede med kode {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "ugyldige argumenter",
"Exit.EbootNotFound": "eboot ikke fundet",
"Exit.RuntimeException": "runtime-fejl",
"Exit.EmulationError": "emuleringsfejl",
"Exit.Unknown": "ukendt",
"Status.EmulatorLocating": "Emulator: lokaliserer…",
"Status.EmulatorPath": "Emulator: {0}",
"Status.EmulatorNotFound": "Emulator: SharpEmu-programmet blev ikke fundet — byg SharpEmu.CLI først.",
"Status.ScanningLibrary": "Skanner bibliotek…",
"Status.AddFolderPrompt": "Tilføj en spilmappe for at udfylde biblioteket.",
"Status.LibraryScanned": "Bibliotek skannet: {0} spil i {1} mappe(r).",
"Status.CouldNotOpenFolder": "Kunne ikke åbne mappe: {0}",
"Status.CopiedToClipboard": "{0} kopieret til udklipsholderen.",
"Status.RemovedFromLibrary": "“{0}” fjernet fra biblioteket. Tilføj mappen igen for at gendanne det.",
"Status.Running": "Kører {0}",
"Status.Stopping": "Stopper…",
"Status.Idle": "Inaktiv",
"Clipboard.Path": "Sti",
"Clipboard.TitleId": "Titel-ID",
"Discord.Playing": "Spiller {0}",
"Discord.Browsing": "Gennemser biblioteket",
"Dialog.ChooseGameFolder": "Vælg en mappe der indeholder spil",
"Dialog.OpenExecutable": "Åbn et program der skal startes",
"Dialog.PsExecutables": "PS-programmer",
"Dialog.SaveLogFile": "Vælg hvor logfilen skal gemmes",
"Dialog.PlainTextFiles": "Almindelige tekstfiler",
"Dialog.LogFiles": "Logfiler"
}
-146
View File
@@ -1,146 +0,0 @@
{
"_languageName": "English",
"Page.Library": "Library",
"Page.Options": "Options",
"Page.GameCount.One": "1 game",
"Page.GameCount.Other": "{0} games",
"Library.SearchWatermark": "Search library…",
"Library.AddFolder": " Add folder",
"Library.Rescan": "⟳ Rescan",
"Library.OpenFile": "Open file…",
"Library.Context.Launch": "Launch",
"Library.Context.OpenFolder": "Open game folder",
"Library.Context.CopyPath": "Copy path",
"Library.Context.CopyTitleId": "Copy title ID",
"Library.Context.Remove": "Remove from library",
"Library.Empty.Title": "Your library is empty",
"Library.Empty.Hint": "Add a folder containing your games to get started.",
"Library.Empty.SearchTitle": "No games match your search",
"Library.Empty.SearchHint": "Nothing in the library matches “{0}”.",
"Library.Empty.AddFolder": " Add game folder",
"Library.Loading": "Loading library…",
"Options.General": "General",
"Options.Env.Tab": "Environment",
"Options.Section.Environment": "ENVIRONMENT VARIABLES",
"Options.Env.Desc": "Switches passed to the emulator as environment variables at launch.",
"Options.Env.Bthid.Desc": "Report Bluetooth HID as unavailable for titles whose wheel/FFB middleware polls forever.\nLeave off normally. Some titles freeze when init fails.",
"Options.Env.LoopGuard.Desc": "Do not force quit titles that repeat the same call for too long.\nTry this when a game exits on its own while loading.",
"Options.Env.VkValidation.Desc": "Enable Vulkan validation layers for GPU debugging.\nSlow. Requires the Vulkan SDK to be installed.",
"Options.Env.DumpSpirv.Desc": "Dump AGC shaders and their SPIR-V translations to the shader-dumps folder.\nUse when reporting shader or rendering bugs.",
"Options.Env.LogDirectMemory.Desc": "Log direct memory allocations and failures to the console.\nUse when a game aborts or exits during boot.",
"Options.Env.LogNp.Desc": "Log NP (PlayStation Network) library calls to the console.",
"Options.Section.Emulation": "EMULATION",
"Options.Section.Logging": "LOGGING",
"Options.Section.Launcher": "LAUNCHER",
"Options.CpuEngine.Label": "CPU engine",
"Options.CpuEngine.Desc": "Execution engine used to run game code.",
"Options.CpuEngine.Native": "Native",
"Options.Strict.Label": "Strict dynlib resolution",
"Options.Strict.Desc": "Fail the launch when an imported symbol cannot be resolved.",
"Options.LogLevel.Label": "Log level",
"Options.LogLevel.Desc": "Verbosity of the emulator console output.",
"Options.LogLevel.Trace": "Trace",
"Options.LogLevel.Debug": "Debug",
"Options.LogLevel.Info": "Info",
"Options.LogLevel.Warning": "Warning",
"Options.LogLevel.Error": "Error",
"Options.LogLevel.Critical": "Critical",
"Options.TraceImports.Label": "Import trace limit",
"Options.TraceImports.Desc": "Trace the first N imports per module (0 = off).",
"Options.LogToFile.Label": "Log to file",
"Options.LogToFile.Desc": "Mirror emulator output to a log file.",
"Options.LogFilePath.Label": "Log file path",
"Options.LogFilePath.Default": "No custom path — logs go to user/logs next to the emulator.",
"Options.LogFilePath.Select": "Select…",
"Options.OverrideLogFile.Label": "Override log file",
"Options.OverrideLogFile.Desc": "Use the exact file path instead of appending title ID and timestamp.",
"Options.TitleMusic.Label": "Title music",
"Options.TitleMusic.Desc": "Loop the selected game's preview music in the library.",
"Options.Discord.Label": "Discord presence",
"Options.Discord.Desc": "Show the running game on your Discord profile.",
"Options.Language.Label": "Emulator language",
"Options.Language.Desc": "Language used throughout the launcher. Applies immediately.",
"Common.On": "On",
"Common.Off": "Off",
"Console.Title": "CONSOLE",
"Console.SearchWatermark": "Search...",
"Console.AutoScroll": "Auto-scroll",
"Console.Split": "Split",
"Console.Copy": "Copy",
"Console.Clear": "Clear",
"Console.WindowTitle": "SharpEmu Console",
"Launch.NoGameSelected": "No game selected",
"Launch.NoGameHint": "Pick a game from the library, or open an eboot.bin directly.",
"Launch.Idle": "Idle",
"Launch.Console": "≡ Console",
"Launch.Launch": "▶ Launch",
"Launch.Stop": "■ Stop",
"Launch.Running": "Running — {0}",
"Launch.Stopping": "Stopping…",
"Launch.Exited": "Exited with code {0} ({1})",
"Launch.ExeNotFound": "SharpEmu executable not found. Build the SharpEmu.CLI project first (dotnet build).",
"Launch.LogFile": "Log file: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Failed to start the emulator: {0}",
"Launch.ProcessExited": "Process exited with code {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "invalid arguments",
"Exit.EbootNotFound": "eboot not found",
"Exit.RuntimeException": "runtime exception",
"Exit.EmulationError": "emulation error",
"Exit.Unknown": "unknown",
"Status.EmulatorLocating": "Emulator: locating…",
"Status.EmulatorPath": "Emulator: {0}",
"Status.EmulatorNotFound": "Emulator: SharpEmu executable not found — build SharpEmu.CLI first.",
"Status.ScanningLibrary": "Scanning library…",
"Status.AddFolderPrompt": "Add a game folder to populate the library.",
"Status.LibraryScanned": "Library scanned: {0} game(s) in {1} folder(s).",
"Status.CouldNotOpenFolder": "Could not open folder: {0}",
"Status.CopiedToClipboard": "{0} copied to clipboard.",
"Status.RemovedFromLibrary": "Removed “{0}” from the library. Re-add its folder to restore it.",
"Status.Running": "Running {0}",
"Status.Stopping": "Stopping…",
"Status.Idle": "Idle",
"Clipboard.Path": "Path",
"Clipboard.TitleId": "Title ID",
"Discord.Playing": "Playing {0}",
"Discord.Browsing": "Browsing the library",
"Dialog.ChooseGameFolder": "Choose a folder containing games",
"Dialog.OpenExecutable": "Open an executable to launch",
"Dialog.PsExecutables": "PS executables",
"Dialog.SaveLogFile": "Select where to save the Log file",
"Dialog.PlainTextFiles": "Plain Text Files",
"Dialog.LogFiles": "Log Files",
"Options.About" : "About",
"About.Github.Label": "GitHub",
"About.Github.Desc": "Source code, issues and project development.",
"About.Discord.Label": "Discord",
"About.Discord.Desc": "Join the community, get support and follow development.",
"About.GithubButton": "Contribute in GitHub!",
"About.DiscordButton": "Join our Discord!"
}
-137
View File
@@ -1,137 +0,0 @@
{
"_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!"
}
-129
View File
@@ -1,129 +0,0 @@
{
"_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 lidentifiant 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 dexé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 lorsquun 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 dajouter lidentifiant du jeu et lhorodatage.",
"Options.TitleMusic.Label": "Musique du jeu",
"Options.TitleMusic.Desc": "Lire en boucle la musique daperçu du jeu sélectionné dans la bibliothèque.",
"Options.Discord.Label": "Présence Discord",
"Options.Discord.Desc": "Afficher le jeu en cours dexécution sur votre profil Discord.",
"Options.Language.Label": "Langue de l’émulateur",
"Options.Language.Desc": "Langue utilisée dans lensemble 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 dexécution — {0}",
"Launch.Stopping": "Arrêt en cours…",
"Launch.Exited": "Processus terminé avec le code {0} ({1})",
"Launch.ExeNotFound": "Lexécutable SharpEmu est introuvable. Compilez dabord 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 sest terminé avec le code {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "arguments non valides",
"Exit.EbootNotFound": "eboot introuvable",
"Exit.RuntimeException": "exception dexécution",
"Exit.EmulationError": "erreur d’émulation",
"Exit.Unknown": "inconnu",
"Status.EmulatorLocating": "Émulateur : recherche en cours…",
"Status.EmulatorPath": "Émulateur : {0}",
"Status.EmulatorNotFound": "Émulateur : lexécutable SharpEmu est introuvable — compilez dabord 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 douvrir 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 lemplacement du fichier journal",
"Dialog.PlainTextFiles": "Fichiers texte brut",
"Dialog.LogFiles": "Fichiers journaux"
}
-146
View File
@@ -1,146 +0,0 @@
{
"_languageName": "Hungarian",
"Page.Library": "Könyvtár",
"Page.Options": "Beállítások",
"Page.GameCount.One": "1 játék",
"Page.GameCount.Other": "{0} játékok",
"Library.SearchWatermark": "Keresés a könyvtárban",
"Library.AddFolder": " Mappa hozzáadása",
"Library.Rescan": "⟳ Újrakeresés",
"Library.OpenFile": "Fájl megnyitása…",
"Library.Context.Launch": "Inditás",
"Library.Context.OpenFolder": "Játékmappa megnyitása",
"Library.Context.CopyPath": "Elérési út másolása",
"Library.Context.CopyTitleId": "Cím ID másolása",
"Library.Context.Remove": "Eltávolítás a Könyvtárból",
"Library.Empty.Title": "A könyvtárad üres",
"Library.Empty.Hint": "Add meg a játékaidat tartalmazó mappát a kezdáshez.",
"Library.Empty.SearchTitle": "Nincs találat a elemre",
"Library.Empty.SearchHint": "A könyvtárban nincs olyan elem, amely egyezne a „{0}” kifejezéssel.",
"Library.Empty.AddFolder": " Játékmappa hozzáadása",
"Library.Loading": "Könyvtár betöltése",
"Options.General": "Általános",
"Options.Env.Tab": "Környezet",
"Options.Section.Environment": "KÖRNYEZETI VÁLTOZÓK",
"Options.Env.Desc": "Indításkor környezeti változóként az emulátorhoz átadott kapcsolók.",
"Options.Env.Bthid.Desc": "Jelenti, amely címeknél a Bluetooth HID nem elérhető, amelyeknél a kormány/FFB-közbenső szoftver végtelenül lekérdezi az adatokat.\nNormál esetben hagyja ki. Egyes címek lefagyanak, ha az inicializálás sikertelen.",
"Options.Env.LoopGuard.Desc": "Ne erőltesse a kilépést azoknál a címeknél, amelyek túl sokáig ismételnek ugyanazt a hívást.\nPróbálja ki ezt, ha egy játék betöltés közben magától kilép.",
"Options.Env.VkValidation.Desc": "Engedélyezze a Vulkan-érvényesítési rétegeket a GPU hibakereséshez.\nLassú. A Vulkan SDK telepítését igényli.",
"Options.Env.DumpSpirv.Desc": "Az AGC-shaderek és azok SPIR-V-fordításainak mentése a shader-dumps mappába.\nHasználd shader- vagy renderelési hibák jelentésekor.",
"Options.Env.LogDirectMemory.Desc": "A közvetlen memóriaallokációk és hibák naplózása a konzolra.\nHasználd, ha egy játék a rendszerindítás során megszakad vagy kilép.",
"Options.Env.LogNp.Desc": "Az NP (PlayStation Network) könyvtárhívásokat naplózza a konzolra.",
"Options.Section.Emulation": "EMULÁCIÓ",
"Options.Section.Logging": "LOGOLÁS",
"Options.Section.Launcher": "INDITÓ",
"Options.CpuEngine.Label": "CPU motor",
"Options.CpuEngine.Desc": "A játék kódjának futtatásához használt végrehajtó motor.",
"Options.CpuEngine.Native": "Natív",
"Options.Strict.Label": "Szigorú dynlib felbontás",
"Options.Strict.Desc": "Indítás megszakítása, ha egy importált szimbólum nem oldható fel.",
"Options.LogLevel.Label": "Naplózási szint",
"Options.LogLevel.Desc": "Az emulátor konzol kimenetének részletessége.",
"Options.LogLevel.Trace": "Trace",
"Options.LogLevel.Debug": "Debug",
"Options.LogLevel.Info": "Információ",
"Options.LogLevel.Warning": "Figyelmeztetés",
"Options.LogLevel.Error": "Hiba",
"Options.LogLevel.Critical": "Kritikus",
"Options.TraceImports.Label": "Import trace limit",
"Options.TraceImports.Desc": "Az első N darab import nyomon követése modulonként (0 = ki).",
"Options.LogToFile.Label": "Naplozás fájlba",
"Options.LogToFile.Desc": "Az emulátor kimenetének tükrözése egy log fájlba.",
"Options.LogFilePath.Label": "Naplófájl elérési útja",
"Options.LogFilePath.Default": "Nincs egyéni út — a logok az emulátor melletti user/logs mappába kerülnek.",
"Options.LogFilePath.Select": "Kiválasztás…",
"Options.OverrideLogFile.Label": "Naplófájl felülírása",
"Options.OverrideLogFile.Desc": "A pontos fájlútvonal használata a cím ID és időbélyeg hozzáfűzése helyett.",
"Options.TitleMusic.Label": "Címzene",
"Options.TitleMusic.Desc": "A kiválasztott játék előnézeti zenéjének ismétlése a könyvtárban.",
"Options.Discord.Label": "Discord jelenlét",
"Options.Discord.Desc": "A futó játék megjelenítése a Discord profilodon.",
"Options.Language.Label": "Emulátor nyelve",
"Options.Language.Desc": "Az indítóban használt nyelv. Azonnal érvénybe lép.",
"Common.On": "Be",
"Common.Off": "Ki",
"Console.Title": "KONZOL",
"Console.SearchWatermark": "Keresés...",
"Console.AutoScroll": "Automatikus görgetés",
"Console.Split": "Felosztás",
"Console.Copy": "Másolás",
"Console.Clear": "Törlés",
"Console.WindowTitle": "SharpEmu Konzol",
"Launch.NoGameSelected": "Nincs játék kiválasztva",
"Launch.NoGameHint": "Válassz egy játékot a könyvtárból, vagy nyiss meg közvetlenül egy eboot.bin fájlt.",
"Launch.Idle": "Tétlen",
"Launch.Console": "≡ Konzol",
"Launch.Launch": "▶ Inditás",
"Launch.Stop": "■ Leállítás",
"Launch.Running": "Fut — {0}",
"Launch.Stopping": "Leállítás…",
"Launch.Exited": "Kilépett a következő kóddal: {0} ({1})",
"Launch.ExeNotFound": "A SharpEmu futtatható fájl nem található. Előbb építsd fel a SharpEmu.CLI projektet (dotnet build).",
"Launch.LogFile": "Naplófájl: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Nem sikerült elindítani az emulátort: {0}",
"Launch.ProcessExited": "A folyamat kilépett a következő kóddal: {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "nemérvényes argumentumok",
"Exit.EbootNotFound": "eboot nem található",
"Exit.RuntimeException": "runtime exception",
"Exit.EmulationError": "emulációs hiba",
"Exit.Unknown": "ismeretlen",
"Status.EmulatorLocating": "Emulátor: keresés…",
"Status.EmulatorPath": "Emulátor: {0}",
"Status.EmulatorNotFound": "Emulátor: a SharpEmu futtatható fájl nem található — előbb építsd fel a SharpEmu.CLI-t.",
"Status.ScanningLibrary": "Könyvtár beolvasása…",
"Status.AddFolderPrompt": "Adj hozzá egy játékmappát a könyvtár feltöltéséhez.",
"Status.LibraryScanned": "Könyvtár beolvasva: {0} játék {1} mappában.",
"Status.CouldNotOpenFolder": "Nem sikerült megnyitni a mappát: {0}",
"Status.CopiedToClipboard": "{0} másolva a vágólapra.",
"Status.RemovedFromLibrary": "„{0}” eltávolítva a könyvtárból. A visszaállításához add hozzá újra a mappáját.",
"Status.Running": "Fut {0}",
"Status.Stopping": "Leállítás…",
"Status.Idle": "Nyugodt",
"Clipboard.Path": "Út",
"Clipboard.TitleId": "Cím ID",
"Discord.Playing": "Játékban {0}",
"Discord.Browsing": "Böngéssz a könyvtárban",
"Dialog.ChooseGameFolder": "Válassz egy mappát ami a játékaidat tartalmazza",
"Dialog.OpenExecutable": "Futtatható fájl megnyitása az indításhoz",
"Dialog.PsExecutables": "PS futtatható fájlok",
"Dialog.SaveLogFile": "Válaszd ki, hogy hova szeretnéd menteni a napló fájlokat",
"Dialog.PlainTextFiles": "Egyszerű szöveges fájlok",
"Dialog.LogFiles": "Naplózási fájlok",
"Options.About" : "Erről",
"About.Github.Label": "GitHub",
"About.Github.Desc": "Forrás kód, hibajelentések és a projekt fejlesztése.",
"About.Discord.Label": "Discord",
"About.Discord.Desc": "Csatlakozz a közösséghe, kérj segítéget és kövesd nyomon a fejlesztést.",
"About.GithubButton": "Járulj hozzá GitHubon!",
"About.DiscordButton": "Csatlakozz a Discordunhoz!"
}
-134
View File
@@ -1,134 +0,0 @@
{
"_languageName": "Italiano",
"Page.Library": "Libreria",
"Page.Options": "Opzioni",
"Page.GameCount.One": "1 gioco",
"Page.GameCount.Other": "{0} giochi",
"Library.SearchWatermark": "Cerca nella libreria…",
"Library.AddFolder": " Aggiungi cartella",
"Library.Rescan": "⟳ Riscansiona",
"Library.OpenFile": "Apri file…",
"Library.Context.Launch": "Avvia",
"Library.Context.OpenFolder": "Apri cartella gioco",
"Library.Context.CopyPath": "Copia percorso",
"Library.Context.CopyTitleId": "Copia ID titolo",
"Library.Context.Remove": "Rimuovi dalla libreria",
"Library.Empty.Title": "La tua libreria è vuota",
"Library.Empty.Hint": "Aggiungi la cartella che contiene i tuoi giochi per partire.",
"Library.Empty.SearchTitle": "Nessun gioco corrisponde alla ricerca",
"Library.Empty.SearchHint": "Nulla nella libreria corrisponde a “{0}”.",
"Library.Empty.AddFolder": " Aggiungi cartella giochi",
"Library.Loading": "Caricamento libreria…",
"Options.General": "Generale",
"Options.Section.Emulation": "EMULAZIONE",
"Options.Section.Logging": "LOG",
"Options.Section.Launcher": "LAUNCHER",
"Options.CpuEngine.Label": "CPU engine",
"Options.CpuEngine.Desc": "Motore di esecuzione utilizzato per eseguire il codice del gioco.",
"Options.CpuEngine.Native": "Nativo",
"Options.Strict.Label": "Risoluzione rigorosa dynlib",
"Options.Strict.Desc": "Interrompi l'avvio quando un simbolo importato non può essere trovato.",
"Options.LogLevel.Label": "Livello Log",
"Options.LogLevel.Desc": "Livello di dettaglio dell'output console dell'emulatore.",
"Options.LogLevel.Trace": "Trace",
"Options.LogLevel.Debug": "Debug",
"Options.LogLevel.Info": "Info",
"Options.LogLevel.Warning": "Warning",
"Options.LogLevel.Error": "Error",
"Options.LogLevel.Critical": "Critical",
"Options.TraceImports.Label": "Limite tracciamento import",
"Options.TraceImports.Desc": "Traccia i primi N import per modulo (0 = disattivato).",
"Options.LogToFile.Label": "Salva log su file",
"Options.LogToFile.Desc": "Duplica l'output dell'emulatore in un file di log.",
"Options.LogFilePath.Label": "Percorso file di log",
"Options.LogFilePath.Default": "Nessun percorso personalizzato — i log vengono salvati in user/logs accanto all'emulatore.",
"Options.LogFilePath.Select": "Seleziona…",
"Options.OverrideLogFile.Label": "Sovrascrivi file di log",
"Options.OverrideLogFile.Desc": "Usa esattamente il percorso specificato invece di aggiungere Title ID e timestamp.",
"Options.TitleMusic.Label": "Musica del titolo",
"Options.TitleMusic.Desc": "Riproduci in loop la musica di anteprima del gioco selezionato nella libreria.",
"Options.Discord.Label": "Presenza Discord",
"Options.Discord.Desc": "Mostra il gioco in esecuzione sul tuo profilo Discord.",
"Options.Language.Label": "Lingua dell'emulatore",
"Options.Language.Desc": "Lingua utilizzata in tutto il launcher. Viene applicata immediatamente.",
"Common.On": "On",
"Common.Off": "Off",
"Console.Title": "CONSOLE",
"Console.SearchWatermark": "Cerca...",
"Console.AutoScroll": "Scorrimento automatico",
"Console.Split": "Dividi",
"Console.Copy": "Copia",
"Console.Clear": "Cancella",
"Console.WindowTitle": "Console SharpEmu",
"Launch.NoGameSelected": "Nessun gioco selezionato",
"Launch.NoGameHint": "Scegli un gioco dalla libreria, oppure apri direttamente un eboot.bin.",
"Launch.Idle": "Inattivo",
"Launch.Console": "≡ Console",
"Launch.Launch": "▶ Avvia",
"Launch.Stop": "■ Ferma",
"Launch.Running": "In esecuzione — {0}",
"Launch.Stopping": "Arresto in corso…",
"Launch.Exited": "Terminato con codice {0} ({1})",
"Launch.ExeNotFound": "Eseguibile SharpEmu non trovato. Compila prima il progetto SharpEmu.CLI (dotnet build).",
"Launch.LogFile": "File di log: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Impossibile avviare l'emulatore: {0}",
"Launch.ProcessExited": "Processo terminato con codice {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "argomenti non validi",
"Exit.EbootNotFound": "eboot non trovato",
"Exit.RuntimeException": "errore di runtime",
"Exit.EmulationError": "errore di emulazione",
"Exit.Unknown": "sconosciuto",
"Status.EmulatorLocating": "Emulatore: ricerca in corso…",
"Status.EmulatorPath": "Emulatore: {0}",
"Status.EmulatorNotFound": "Emulatore: eseguibile SharpEmu non trovato — compila prima SharpEmu.CLI.",
"Status.ScanningLibrary": "Scansione della libreria…",
"Status.AddFolderPrompt": "Aggiungi una cartella di giochi per popolare la libreria.",
"Status.LibraryScanned": "Libreria scansionata: {0} gioco/giochi in {1} cartella/e.",
"Status.CouldNotOpenFolder": "Impossibile aprire la cartella: {0}",
"Status.CopiedToClipboard": "{0} copiato negli appunti.",
"Status.RemovedFromLibrary": "“{0}” rimosso dalla libreria. Riaggiungi la sua cartella per ripristinarlo.",
"Status.Running": "In esecuzione: {0}",
"Status.Stopping": "Arresto in corso…",
"Status.Idle": "Inattivo",
"Clipboard.Path": "Percorso",
"Clipboard.TitleId": "ID Titolo",
"Discord.Playing": "Sta giocando a {0}",
"Discord.Browsing": "Sta esplorando la libreria",
"Dialog.ChooseGameFolder": "Scegli una cartella contenente giochi",
"Dialog.OpenExecutable": "Apri un eseguibile da avviare",
"Dialog.PsExecutables": "Eseguibili PS",
"Dialog.SaveLogFile": "Scegli dove salvare il file di log",
"Dialog.PlainTextFiles": "File di testo semplice",
"Dialog.LogFiles": "File di log"
}
-129
View File
@@ -1,129 +0,0 @@
{
"_languageName": "日本語",
"Page.Library": "ライブラリ",
"Page.Options": "オプション",
"Page.GameCount.One": "ゲーム 1本",
"Page.GameCount.Other": "ゲーム {0}本",
"Library.SearchWatermark": "ライブラリを検索…",
"Library.AddFolder": " フォルダーを追加",
"Library.Rescan": "⟳ 再スキャン",
"Library.OpenFile": "ファイルを開く…",
"Library.Context.Launch": "起動",
"Library.Context.OpenFolder": "ゲームフォルダーを開く",
"Library.Context.CopyPath": "パスをコピー",
"Library.Context.CopyTitleId": "ゲームIDをコピー",
"Library.Context.Remove": "ライブラリから削除",
"Library.Empty.Title": "ライブラリが空です",
"Library.Empty.Hint": "開始するには、ゲームが含まれるフォルダーを追加してください。",
"Library.Empty.SearchTitle": "検索条件に一致するゲームが見つかりません",
"Library.Empty.SearchHint": "ライブラリに「{0}」と一致する項目はありません。",
"Library.Empty.AddFolder": "+ ゲームフォルダーを追加",
"Library.Loading": "ライブラリを読み込み中…",
"Options.General": "一般",
"Options.Section.Emulation": "エミュレーション",
"Options.Section.Logging": "ロギング",
"Options.Section.Launcher": "ランチャー",
"Options.CpuEngine.Label": "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": "ログファイル"
}
-129
View File
@@ -1,129 +0,0 @@
{
"_languageName": "한국어",
"Page.Library": "라이브러리",
"Page.Options": "옵션",
"Page.GameCount.One": "게임 1개",
"Page.GameCount.Other": "게임 {0}개",
"Library.SearchWatermark": "라이브러리 검색…",
"Library.AddFolder": " 폴더 추가",
"Library.Rescan": "⟳ 다시 스캔",
"Library.OpenFile": "파일 열기…",
"Library.Context.Launch": "실행",
"Library.Context.OpenFolder": "게임 폴더 열기",
"Library.Context.CopyPath": "경로 복사",
"Library.Context.CopyTitleId": "게임 ID 복사",
"Library.Context.Remove": "라이브러리에서 제거",
"Library.Empty.Title": "라이브러리가 비어 있습니다",
"Library.Empty.Hint": "시작하려면 게임이 포함된 폴더를 추가하세요.",
"Library.Empty.SearchTitle": "검색 결과와 일치하는 게임이 없습니다",
"Library.Empty.SearchHint": "라이브러리에 '{0}'와(과) 일치하는 항목이 없습니다.",
"Library.Empty.AddFolder": " 게임 폴더 추가",
"Library.Loading": "라이브러리 불러오는 중…",
"Options.General": "일반",
"Options.Section.Emulation": "에뮬레이션",
"Options.Section.Logging": "로깅",
"Options.Section.Launcher": "런처",
"Options.CpuEngine.Label": "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": "로그 파일"
}
-129
View File
@@ -1,129 +0,0 @@
{
"_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"
}
-146
View File
@@ -1,146 +0,0 @@
{
"_languageName": "Português (Portugal)",
"Page.Library": "Biblioteca",
"Page.Options": "Opções",
"Page.GameCount.One": "1 jogo",
"Page.GameCount.Other": "{0} jogos",
"Library.SearchWatermark": "Pesquisar biblioteca…",
"Library.AddFolder": " Adicionar pasta",
"Library.Rescan": "⟳ Reanalisar",
"Library.OpenFile": "Abrir ficheiro…",
"Library.Context.Launch": "Iniciar",
"Library.Context.OpenFolder": "Abrir pasta do jogo",
"Library.Context.CopyPath": "Copiar caminho",
"Library.Context.CopyTitleId": "Copiar ID do título",
"Library.Context.Remove": "Remover da biblioteca",
"Library.Empty.Title": "A sua biblioteca está vazia",
"Library.Empty.Hint": "Adicione uma pasta com os seus jogos para começar.",
"Library.Empty.SearchTitle": "Nenhum jogo corresponde à sua pesquisa",
"Library.Empty.SearchHint": "Nada na biblioteca corresponde a “{0}”.",
"Library.Empty.AddFolder": " Adicionar pasta de jogos",
"Library.Loading": "A carregar biblioteca…",
"Options.General": "Geral",
"Options.Env.Tab": "Ambiente",
"Options.Section.Environment": "VARIÁVEIS DE AMBIENTE",
"Options.Env.Desc": "Switches passados ao emulador como variáveis de ambiente no arranque.",
"Options.Env.Bthid.Desc": "Reporta o Bluetooth HID como indisponível para títulos cujo middleware de volante/FFB fica à espera indefinidamente.\nDeixe desativado normalmente. Alguns títulos bloqueiam quando a inicialização falha.",
"Options.Env.LoopGuard.Desc": "Não force o encerramento de títulos que repetem a mesma chamada durante demasiado tempo.\nExperimente isto quando um jogo fecha sozinho durante o carregamento.",
"Options.Env.VkValidation.Desc": "Ativa as camadas de validação do Vulkan para depuração da GPU.\nLento. Requer que o Vulkan SDK esteja instalado.",
"Options.Env.DumpSpirv.Desc": "Extrai os shaders AGC e as respetivas traduções SPIR-V para a pasta shader-dumps.\nUtilize ao reportar problemas de shaders ou renderização.",
"Options.Env.LogDirectMemory.Desc": "Regista alocações de memória direta e falhas na consola.\nUtilize quando um jogo aborta ou fecha durante o arranque.",
"Options.Env.LogNp.Desc": "Regista chamadas da biblioteca NP (PlayStation Network) na consola.",
"Options.Section.Emulation": "EMULAÇÃO",
"Options.Section.Logging": "REGISTOS",
"Options.Section.Launcher": "LANÇADOR",
"Options.CpuEngine.Label": "Motor de CPU",
"Options.CpuEngine.Desc": "Motor de execução utilizado para correr o código do jogo.",
"Options.CpuEngine.Native": "Nativo",
"Options.Strict.Label": "Resolução estrita de dynlib",
"Options.Strict.Desc": "Falha o arranque quando um símbolo importado não pode ser resolvido.",
"Options.LogLevel.Label": "Nível de registo",
"Options.LogLevel.Desc": "Nível de detalhe da saída da consola do emulador.",
"Options.LogLevel.Trace": "Rastreio",
"Options.LogLevel.Debug": "Depuração",
"Options.LogLevel.Info": "Informação",
"Options.LogLevel.Warning": "Aviso",
"Options.LogLevel.Error": "Erro",
"Options.LogLevel.Critical": "Crítico",
"Options.TraceImports.Label": "Limite de rastreio de importações",
"Options.TraceImports.Desc": "Rastreia as primeiras N importações por módulo (0 = desativado).",
"Options.LogToFile.Label": "Registar para ficheiro",
"Options.LogToFile.Desc": "Duplicar a saída do emulador para um ficheiro de registo.",
"Options.LogFilePath.Label": "Caminho do ficheiro de registo",
"Options.LogFilePath.Default": "Sem caminho personalizado — os registos vão para user/logs junto ao emulador.",
"Options.LogFilePath.Select": "Selecionar…",
"Options.OverrideLogFile.Label": "Substituir ficheiro de registo",
"Options.OverrideLogFile.Desc": "Utilizar o caminho de ficheiro exato em vez de acrescentar o ID do título e a hora.",
"Options.TitleMusic.Label": "Música do título",
"Options.TitleMusic.Desc": "Repetir em loop a música de pré-visualização do jogo selecionado na biblioteca.",
"Options.Discord.Label": "Presença no Discord",
"Options.Discord.Desc": "Mostrar o jogo em execução no seu perfil do Discord.",
"Options.Language.Label": "Idioma do emulador",
"Options.Language.Desc": "Idioma utilizado em todo o lançador. Aplica-se de imediato.",
"Common.On": "Ativado",
"Common.Off": "Desativado",
"Console.Title": "CONSOLA",
"Console.SearchWatermark": "Pesquisar...",
"Console.AutoScroll": "Deslocamento automático",
"Console.Split": "Dividir",
"Console.Copy": "Copiar",
"Console.Clear": "Limpar",
"Console.WindowTitle": "Consola do SharpEmu",
"Launch.NoGameSelected": "Nenhum jogo selecionado",
"Launch.NoGameHint": "Escolha um jogo da biblioteca ou abra um eboot.bin diretamente.",
"Launch.Idle": "Inativo",
"Launch.Console": "≡ Consola",
"Launch.Launch": "▶ Iniciar",
"Launch.Stop": "■ Parar",
"Launch.Running": "Em execução — {0}",
"Launch.Stopping": "A parar…",
"Launch.Exited": "Terminou com o código {0} ({1})",
"Launch.ExeNotFound": "Executável do SharpEmu não encontrado. Compile primeiro o projeto SharpEmu.CLI (dotnet build).",
"Launch.LogFile": "Ficheiro de registo: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Falha ao iniciar o emulador: {0}",
"Launch.ProcessExited": "O processo terminou com o código {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "argumentos inválidos",
"Exit.EbootNotFound": "eboot não encontrado",
"Exit.RuntimeException": "exceção em tempo de execução",
"Exit.EmulationError": "erro de emulação",
"Exit.Unknown": "desconhecido",
"Status.EmulatorLocating": "Emulador: a localizar…",
"Status.EmulatorPath": "Emulador: {0}",
"Status.EmulatorNotFound": "Emulador: executável do SharpEmu não encontrado — compile primeiro o SharpEmu.CLI.",
"Status.ScanningLibrary": "A analisar biblioteca…",
"Status.AddFolderPrompt": "Adicione uma pasta de jogos para preencher a biblioteca.",
"Status.LibraryScanned": "Biblioteca analisada: {0} jogo(s) em {1} pasta(s).",
"Status.CouldNotOpenFolder": "Não foi possível abrir a pasta: {0}",
"Status.CopiedToClipboard": "{0} copiado para a área de transferência.",
"Status.RemovedFromLibrary": "“{0}” removido da biblioteca. Adicione novamente a pasta para o restaurar.",
"Status.Running": "A executar {0}",
"Status.Stopping": "A parar…",
"Status.Idle": "Inativo",
"Clipboard.Path": "Caminho",
"Clipboard.TitleId": "ID do título",
"Discord.Playing": "A jogar {0}",
"Discord.Browsing": "A navegar na biblioteca",
"Dialog.ChooseGameFolder": "Escolha uma pasta com jogos",
"Dialog.OpenExecutable": "Abrir um executável para iniciar",
"Dialog.PsExecutables": "Executáveis PS",
"Dialog.SaveLogFile": "Selecione onde guardar o ficheiro de registo",
"Dialog.PlainTextFiles": "Ficheiros de Texto Simples",
"Dialog.LogFiles": "Ficheiros de Registo",
"Options.About" : "Sobre",
"About.Github.Label": "GitHub",
"About.Github.Desc": "Código-fonte, problemas e desenvolvimento do projeto.",
"About.Discord.Label": "Discord",
"About.Discord.Desc": "Junte-se à comunidade, obtenha suporte e acompanhe o desenvolvimento.",
"About.GithubButton": "Contribua no GitHub!",
"About.DiscordButton": "Junte-se ao nosso Discord!"
}
-129
View File
@@ -1,129 +0,0 @@
{
"_languageName": "Русский",
"Page.Library": "Библиотека",
"Page.Options": "Настройки",
"Page.GameCount.One": "1 игра",
"Page.GameCount.Other": "Игр: {0}",
"Library.SearchWatermark": "Поиск…",
"Library.AddFolder": "+ Добавить папку",
"Library.Rescan": "⟳ Сканировать",
"Library.OpenFile": "Открыть файл…",
"Library.Context.Launch": "Запустить",
"Library.Context.OpenFolder": "Открыть папку с игрой",
"Library.Context.CopyPath": "Скопировать путь",
"Library.Context.CopyTitleId": "Скопировать ID игры",
"Library.Context.Remove": "Удалить из библиотеки",
"Library.Empty.Title": "Ваша библиотека пуста",
"Library.Empty.Hint": "Добавьте папку с играми, чтобы начать.",
"Library.Empty.SearchTitle": "По вашему запросу ничего не найдено",
"Library.Empty.SearchHint": "В библиотеке нет игр, соответствующих запросу «{0}».",
"Library.Empty.AddFolder": "+ Добавить папку с играми",
"Library.Loading": "Загрузка библиотеки…",
"Options.General": "Основные",
"Options.Section.Emulation": "ЭМУЛЯЦИЯ",
"Options.Section.Logging": "ЛОГГИРОВАНИЕ",
"Options.Section.Launcher": "ЛАУНЧЕР",
"Options.CpuEngine.Label": "Движок ЦП",
"Options.CpuEngine.Desc": "Движок выполнения, используемый для запуска игрового кода.",
"Options.CpuEngine.Native": "Нативный",
"Options.Strict.Label": "Строгое разрешение динамических библиотек",
"Options.Strict.Desc": "Не запускать игру, если не удается найти или связать импортируемую функцию/переменную.",
"Options.LogLevel.Label": "Уровень логгирования",
"Options.LogLevel.Desc": "Подробность вывода в консоль эмулятора.",
"Options.LogLevel.Trace": "Trace",
"Options.LogLevel.Debug": "Debug",
"Options.LogLevel.Info": "Info",
"Options.LogLevel.Warning": "Warning",
"Options.LogLevel.Error": "Error",
"Options.LogLevel.Critical": "Critical",
"Options.TraceImports.Label": "Лимит трассировки импортов",
"Options.TraceImports.Desc": "Трассировать первые N импортов в каждом модуле (0 - выключено).",
"Options.LogToFile.Label": "Запись лога в файл",
"Options.LogToFile.Desc": "Дублировать вывод эмулятора в файл лога.",
"Options.LogFilePath.Label": "Путь к файлу лога",
"Options.LogFilePath.Default": "Пользовательский путь не задан: логи сохраняются в user/logs рядом с эмулятором.",
"Options.LogFilePath.Select": "Выбрать…",
"Options.OverrideLogFile.Label": "Переопределить файл лога",
"Options.OverrideLogFile.Desc": "Использовать указанный путь к файлу без добавления ID игры и метки времени.",
"Options.TitleMusic.Label": "Музыка игры",
"Options.TitleMusic.Desc": "Зацикленно воспроизводить музыку предпросмотра выбранной игры в библиотеке.",
"Options.Discord.Label": "Статус Discord",
"Options.Discord.Desc": "Показывать запущенную игру в профиле Discord.",
"Options.Language.Label": "Язык эмулятора",
"Options.Language.Desc": "Язык интерфейса лаунчера. Изменение применяется сразу.",
"Common.On": "Включено",
"Common.Off": "Выключено",
"Console.Title": "КОНСОЛЬ",
"Console.SearchWatermark": "Поиск...",
"Console.AutoScroll": "Авто-прокрутка",
"Console.Split": "Разделить",
"Console.Copy": "Скопировать",
"Console.Clear": "Очистить",
"Console.WindowTitle": "Консоль SharpEmu",
"Launch.NoGameSelected": "Ничего не выбрано",
"Launch.NoGameHint": "Выберите игру из библиотеки или откройте eboot.bin напрямую.",
"Launch.Idle": "Ожидание",
"Launch.Console": "≡ Консоль",
"Launch.Launch": "▶ Запустить",
"Launch.Stop": "■ Остановить",
"Launch.Running": "Запущено - {0}",
"Launch.Stopping": "Остановка…",
"Launch.Exited": "Завершено с кодом {0} ({1})",
"Launch.ExeNotFound": "Исполняемый файл SharpEmu не найден. Скомпилируйте сначала проект SharpEmu.CLI (dotnet build).",
"Launch.LogFile": "Лог: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Не удалось запустить эмулятор: {0}",
"Launch.ProcessExited": "Процесс завершился с кодом {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "некорректные аргументы",
"Exit.EbootNotFound": "eboot не найден",
"Exit.RuntimeException": "ошибка выполнения",
"Exit.EmulationError": "ошибка эмуляции",
"Exit.Unknown": "неизвестная ошибка",
"Status.EmulatorLocating": "Эмулятор: поиск…",
"Status.EmulatorPath": "Эмулятор: {0}",
"Status.EmulatorNotFound": "Эмулятор: исполняемый файл SharpEmu не был найден - скомпилируйте сначала SharpEmu.CLI.",
"Status.ScanningLibrary": "Сканирование библиотеки…",
"Status.AddFolderPrompt": "Добавьте папку с играми, чтобы заполнить библиотеку.",
"Status.LibraryScanned": "Сканирование библиотеки завершено: игр: {0}, папок: {1}.",
"Status.CouldNotOpenFolder": "Не удалось открыть папку: {0}",
"Status.CopiedToClipboard": "{0}: скопировано в буфер обмена.",
"Status.RemovedFromLibrary": "Игра «{0}» удалена из библиотеки. Добавьте её папку заново, чтобы вернуть.",
"Status.Running": "Запущено: {0}",
"Status.Stopping": "Остановка…",
"Status.Idle": "Ожидание",
"Clipboard.Path": "Путь",
"Clipboard.TitleId": "ID игры",
"Discord.Playing": "Играет в {0}",
"Discord.Browsing": "Просматривает библиотеку",
"Dialog.ChooseGameFolder": "Выберите папку, содержащую игры",
"Dialog.OpenExecutable": "Открыть исполняемый файл для запуска",
"Dialog.PsExecutables": "Исполняемые файлы PS",
"Dialog.SaveLogFile": "Выберите, куда сохранить файл с логами",
"Dialog.PlainTextFiles": "Текстовые файлы",
"Dialog.LogFiles": "Логи"
}
-129
View File
@@ -1,129 +0,0 @@
{
"_languageName": "Türkçe",
"Page.Library": "Kütüphane",
"Page.Options": "Ayarlar",
"Page.GameCount.One": "1 oyun",
"Page.GameCount.Other": "{0} oyun",
"Library.SearchWatermark": "Kütüphanede ara…",
"Library.AddFolder": " Klasör ekle",
"Library.Rescan": "⟳ Yeniden tara",
"Library.OpenFile": "Dosya aç…",
"Library.Context.Launch": "Başlat",
"Library.Context.OpenFolder": "Oyun klasörünü aç",
"Library.Context.CopyPath": "Yolu kopyala",
"Library.Context.CopyTitleId": "Title ID'yi kopyala",
"Library.Context.Remove": "Kütüphaneden kaldır",
"Library.Empty.Title": "Kütüphaneniz boş",
"Library.Empty.Hint": "Başlamak için oyunlarınızı içeren bir klasör ekleyin.",
"Library.Empty.SearchTitle": "Aramanızla eşleşen oyun yok",
"Library.Empty.SearchHint": "Kütüphanede “{0}” ile eşleşen bir şey yok.",
"Library.Empty.AddFolder": " Oyun klasörü ekle",
"Library.Loading": "Kütüphane yükleniyor…",
"Options.General": "Genel",
"Options.Section.Emulation": "EMÜLASYON",
"Options.Section.Logging": "GÜNLÜKLEME",
"Options.Section.Launcher": "BAŞLATICI",
"Options.CpuEngine.Label": "CPU motoru",
"Options.CpuEngine.Desc": "Oyun kodunu çalıştırmak için kullanılan yürütme motoru.",
"Options.CpuEngine.Native": "Native",
"Options.Strict.Label": "Katı dynlib çözümü",
"Options.Strict.Desc": "İçe aktarılan bir sembol çözümlenemediğinde başlatmayı başarısız kılar.",
"Options.LogLevel.Label": "Günlük seviyesi",
"Options.LogLevel.Desc": "Emülatör konsol çıktısının ayrıntı düzeyi.",
"Options.LogLevel.Trace": "Trace",
"Options.LogLevel.Debug": "Debug",
"Options.LogLevel.Info": "Info",
"Options.LogLevel.Warning": "Warning",
"Options.LogLevel.Error": "Error",
"Options.LogLevel.Critical": "Critical",
"Options.TraceImports.Label": "Import izleme sınırı",
"Options.TraceImports.Desc": "Modül başına ilk N import'u izle (0 = kapalı).",
"Options.LogToFile.Label": "Dosyaya günlükle",
"Options.LogToFile.Desc": "Emülatör çıktısını bir günlük dosyasına yansıt.",
"Options.LogFilePath.Label": "Günlük dosyası yolu",
"Options.LogFilePath.Default": "Özel yol yok — günlükler emülatörün yanındaki user/logs klasörüne yazılır.",
"Options.LogFilePath.Select": "Seç…",
"Options.OverrideLogFile.Label": "Günlük dosyasının üzerine yaz",
"Options.OverrideLogFile.Desc": "Title ID ve zaman damgası eklemek yerine tam dosya yolunu kullan.",
"Options.TitleMusic.Label": "Oyun müziği",
"Options.TitleMusic.Desc": "Kütüphanede seçili oyunun önizleme müziğini döngüye al.",
"Options.Discord.Label": "Discord durumu",
"Options.Discord.Desc": "Discord profilinde çalışan oyunu göster.",
"Options.Language.Label": "Emülatör dili",
"Options.Language.Desc": "Başlatıcı genelinde kullanılan dil. Hemen uygulanır.",
"Common.On": "Açık",
"Common.Off": "Kapalı",
"Console.Title": "KONSOL",
"Console.SearchWatermark": "Ara...",
"Console.AutoScroll": "Otomatik kaydır",
"Console.Split": "Ayır",
"Console.Copy": "Kopyala",
"Console.Clear": "Temizle",
"Console.WindowTitle": "SharpEmu Konsol",
"Launch.NoGameSelected": "Oyun seçilmedi",
"Launch.NoGameHint": "Kütüphaneden bir oyun seçin veya doğrudan bir eboot.bin açın.",
"Launch.Idle": "Boşta",
"Launch.Console": "≡ Konsol",
"Launch.Launch": "▶ Başlat",
"Launch.Stop": "■ Durdur",
"Launch.Running": "Çalışıyor — {0}",
"Launch.Stopping": "Durduruluyor…",
"Launch.Exited": "Çıkış kodu {0} ({1})",
"Launch.ExeNotFound": "SharpEmu çalıştırılabilir dosyası bulunamadı. Önce SharpEmu.CLI projesini derleyin (dotnet build).",
"Launch.LogFile": "Günlük dosyası: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Emülatör başlatılamadı: {0}",
"Launch.ProcessExited": "İşlem {0} kodla sonlandı ({1}).",
"Exit.Ok": "Tamam",
"Exit.InvalidArguments": "geçersiz argümanlar",
"Exit.EbootNotFound": "eboot bulunamadı",
"Exit.RuntimeException": "çalışma zamanı hatası",
"Exit.EmulationError": "emülasyon hatası",
"Exit.Unknown": "bilinmiyor",
"Status.EmulatorLocating": "Emülatör: bulunuyor…",
"Status.EmulatorPath": "Emülatör: {0}",
"Status.EmulatorNotFound": "Emülatör: SharpEmu çalıştırılabilir dosyası bulunamadı — önce SharpEmu.CLI'yi derleyin.",
"Status.ScanningLibrary": "Kütüphane taranıyor…",
"Status.AddFolderPrompt": "Kütüphaneyi doldurmak için bir oyun klasörü ekleyin.",
"Status.LibraryScanned": "Kütüphane tarandı: {1} klasörde {0} oyun.",
"Status.CouldNotOpenFolder": "Klasör açılamadı: {0}",
"Status.CopiedToClipboard": "{0} panoya kopyalandı.",
"Status.RemovedFromLibrary": "“{0}” kütüphaneden kaldırıldı. Geri getirmek için klasörünü yeniden ekleyin.",
"Status.Running": "{0} çalışıyor",
"Status.Stopping": "Durduruluyor…",
"Status.Idle": "Boşta",
"Clipboard.Path": "Yol",
"Clipboard.TitleId": "Title ID",
"Discord.Playing": "{0} oynuyor",
"Discord.Browsing": "Kütüphaneye göz atıyor",
"Dialog.ChooseGameFolder": "Oyunlarınızı içeren bir klasör seçin",
"Dialog.OpenExecutable": "Başlatılacak bir çalıştırılabilir dosya aç",
"Dialog.PsExecutables": "PS çalıştırılabilirleri",
"Dialog.SaveLogFile": "Günlük dosyasının kaydedileceği yeri seçin",
"Dialog.PlainTextFiles": "Düz Metin Dosyaları",
"Dialog.LogFiles": "Günlük Dosyaları"
}
-256
View File
@@ -1,256 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Text.Json;
namespace SharpEmu.GUI;
/// <summary>
/// Loads UI strings for the launcher. Every language ships embedded in the
/// assembly (see SharpEmu.GUI.csproj) so a release build is fully
/// self-contained; an optional Languages/&lt;code&gt;.json file next to the
/// executable overrides the embedded copy for that code, so a translation
/// fix or a brand-new language never needs a rebuild.
/// </summary>
public sealed class Localization
{
public static Localization Instance { get; } = new();
public sealed record LanguageInfo(string Code, string NativeName);
private const string EmbeddedResourcePrefix = "Languages.";
private const string EmbeddedResourceSuffix = ".json";
private Dictionary<string, string> _strings = new();
private 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;
}
}
+154 -443
View File
@@ -9,7 +9,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
Title="SharpEmu"
Width="1280" Height="820"
MinWidth="980" MinHeight="640"
WindowState="Maximized"
WindowStartupLocation="CenterScreen"
Background="{StaticResource BgBrush}"
ExtendClientAreaToDecorationsHint="True"
@@ -18,7 +17,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
Icon="avares://SharpEmu.GUI/Assets/SharpEmu.ico"
KeyDown="OnKeyDown">
<Grid RowDefinitions="Auto,*,Auto">
<Grid RowDefinitions="44,*,32">
<!-- Selected-game backdrop: key art behind the main content, dimmed by
a scrim so tiles and text stay readable. Fades on selection. -->
@@ -42,9 +41,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
</Border>
</Panel>
<!-- Title bar; hidden in fullscreen (F11) along with the status bar, so
the game gets the whole screen. -->
<Grid x:Name="TitleBar" Grid.Row="0" Height="44" Background="{StaticResource ChromeBrush}">
<!-- Title bar -->
<Grid x:Name="TitleBar" Grid.Row="0" Background="{StaticResource ChromeBrush}">
<StackPanel Orientation="Horizontal" Spacing="10" Margin="16,0" VerticalAlignment="Center">
<Image Source="avares://SharpEmu.GUI/Assets/SharpEmu.ico" Width="20" Height="20"
RenderOptions.BitmapInterpolationMode="HighQuality" />
@@ -56,421 +54,106 @@ SPDX-License-Identifier: GPL-2.0-or-later
</Grid>
<!-- Main content -->
<Grid Grid.Row="1" Margin="32,24,32,20" RowDefinitions="Auto,*,Auto,Auto">
<Grid Grid.Row="1" Margin="18,14,18,14" RowDefinitions="Auto,*,Auto,Auto">
<!-- Library / Options page switcher, with the library toolbar sharing
the same row on the right. Plain buttons (not TabItem) so there is
no underline; LB/RB hint chips flank the pair and the gamepad's
shoulder buttons actually switch pages from anywhere. -->
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto" Margin="0,0,0,20">
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="14" VerticalAlignment="Center">
<Border Classes="padHint" VerticalAlignment="Center">
<TextBlock Text="LB" FontSize="11" FontWeight="Bold" Foreground="{StaticResource MutedBrush}" />
</Border>
<Button x:Name="LibraryTabButton" Classes="segment active" Content="Library" />
<Button x:Name="OptionsTabButton" Classes="segment" Content="Options" />
<Border Classes="padHint" VerticalAlignment="Center">
<TextBlock Text="RB" FontSize="11" FontWeight="Bold" Foreground="{StaticResource MutedBrush}" />
</Border>
</StackPanel>
<StackPanel Grid.Column="2" x:Name="LibraryToolbar" Orientation="Horizontal" Spacing="8"
VerticalAlignment="Center">
<TextBox x:Name="SearchBox" Watermark="Search library…" Width="240" VerticalAlignment="Center" />
<Button x:Name="AddFolderButton" Classes="ghost" Content=" Add folder" VerticalAlignment="Center" />
<Button x:Name="RescanButton" Classes="ghost" Content="⟳ Rescan" VerticalAlignment="Center" />
<Button x:Name="OpenFileButton" Classes="ghost" Content="Open file…" VerticalAlignment="Center" />
</StackPanel>
<!-- Library toolbar -->
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,*,Auto,Auto,Auto,Auto" Margin="6,0,6,12">
<TextBlock Grid.Column="0" Text="Library" FontSize="22" FontWeight="Bold" VerticalAlignment="Center" />
<Border Grid.Column="1" Classes="pill" Margin="12,2,0,0" VerticalAlignment="Center">
<TextBlock x:Name="GameCountText" Text="0 games" FontSize="11" Foreground="{StaticResource MutedBrush}" />
</Border>
<TextBox Grid.Column="3" x:Name="SearchBox" Watermark="Search library…" Width="280"
VerticalAlignment="Center" Margin="0,0,12,0" />
<Button Grid.Column="4" x:Name="AddFolderButton" Classes="ghost" Content=" Add folder"
VerticalAlignment="Center" Margin="0,0,8,0" />
<Button Grid.Column="5" x:Name="RescanButton" Classes="ghost" Content="⟳ Rescan"
VerticalAlignment="Center" Margin="0,0,8,0" />
<Button Grid.Column="6" x:Name="OpenFileButton" Classes="ghost" Content="Open file…"
VerticalAlignment="Center" />
</Grid>
<!-- Cover-art grid -->
<Panel Grid.Row="1">
<!-- Library page. The tile row gets extra top margin so it sits
closer to eye level (PS5 home-screen style) instead of hugging
the Library/Options switcher above it. -->
<Panel x:Name="LibraryPage" Margin="0,64,0,0">
<ListBox x:Name="GameList" Classes="tileGrid" Background="Transparent"
SelectionMode="Single" Padding="0">
<ListBox.ContextMenu>
<ContextMenu x:Name="GameContextMenu" Placement="Pointer">
<MenuItem x:Name="CtxLaunch" Header="Launch" FontWeight="SemiBold">
<MenuItem.Icon>
<TextBlock Text="▶" FontSize="11" Foreground="{StaticResource AccentHoverBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
<MenuItem x:Name="CtxOpenFolder" Header="Open game folder">
<MenuItem.Icon>
<TextBlock Text="📂" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem x:Name="CtxCopyPath" Header="Copy path">
<MenuItem.Icon>
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
<MenuItem x:Name="CtxCopyTitleId" Header="Copy title ID">
<MenuItem.Icon>
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem x:Name="CtxRemove" Header="Remove from library"
Foreground="{StaticResource DangerHoverBrush}">
<MenuItem.Icon>
<TextBlock Text="✕" FontSize="12" Foreground="{StaticResource DangerHoverBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
</ContextMenu>
</ListBox.ContextMenu>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Width="128" Height="172" Spacing="7">
<Border Classes="coverShadow" Width="128" Height="128">
<Border Classes="coverClip">
<Panel>
<Border Background="{Binding PlaceholderBrush}" IsVisible="{Binding !HasCover}">
<TextBlock Text="{Binding Initials}" FontSize="36" FontWeight="Bold"
Foreground="#E8ECF4" Opacity="0.85"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<Image Source="{Binding Cover}" Stretch="UniformToFill" IsVisible="{Binding HasCover}" />
</Panel>
</Border>
<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"
TextAlignment="Center" HorizontalAlignment="Center" />
TextWrapping="Wrap" MaxLines="2" TextTrimming="CharacterEllipsis" />
<TextBlock Text="{Binding Detail}" FontSize="11" Foreground="{StaticResource MutedBrush}"
TextTrimming="CharacterEllipsis" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<!-- Empty state -->
<StackPanel x:Name="EmptyState" Spacing="10" HorizontalAlignment="Center" VerticalAlignment="Center"
IsVisible="False">
<TextBlock Text="🎮" FontSize="44" HorizontalAlignment="Center" Opacity="0.7" />
<TextBlock x:Name="EmptyStateTitle" Text="Your library is empty" FontSize="18" FontWeight="SemiBold"
HorizontalAlignment="Center" />
<TextBlock x:Name="EmptyStateHint" Text="Add a folder containing your games to get started."
FontSize="13" Foreground="{StaticResource MutedBrush}" HorizontalAlignment="Center" />
<Button x:Name="EmptyAddFolderButton" Classes="accent" Content=" Add game folder"
HorizontalAlignment="Center" Margin="0,8,0,0" />
</StackPanel>
<!-- Loading state: covers the grid while a scan (initial load,
rescan, or a just-added folder) is in flight, so the screen
never looks blank mid-scan. -->
<StackPanel x:Name="LoadingState" Spacing="14" HorizontalAlignment="Center" VerticalAlignment="Center"
IsVisible="False">
<ProgressBar IsIndeterminate="True" Width="180" Height="3" />
<TextBlock x:Name="LoadingStateText" Text="Loading library…" FontSize="13"
Foreground="{StaticResource MutedBrush}" HorizontalAlignment="Center" />
</StackPanel>
</Panel>
<!-- Options page: sub-tabs so future categories (Graphics, …) slot
in next to General. Card-grouped sections match the visual
language used by the console panel and launch bar below. -->
<Grid x:Name="OptionsPage" IsVisible="False">
<TabControl>
<TabItem x:Name="GeneralTabItem" Header="General" FontSize="15">
<ScrollViewer>
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
<Border Classes="card">
<StackPanel Spacing="14">
<TextBlock x:Name="EmulationSectionTitle" Classes="sectionTitle" Text="EMULATION" />
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="CpuEngineLabel" Text="CPU engine" FontSize="13" />
<TextBlock x:Name="CpuEngineDesc" Text="Execution engine used to run game code."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ComboBox Grid.Column="1" x:Name="CpuEngineBox" Width="160" SelectedIndex="0"
VerticalAlignment="Center" CornerRadius="8">
<ComboBoxItem x:Name="CpuEngineNativeItem" Content="Native" />
</ComboBox>
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="StrictLabel" Text="Strict dynlib resolution" FontSize="13" />
<TextBlock x:Name="StrictDesc" Text="Fail the launch when an imported symbol cannot be resolved."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="StrictToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
</StackPanel>
</Border>
<Border Classes="card">
<StackPanel Spacing="14">
<TextBlock x:Name="LoggingSectionTitle" Classes="sectionTitle" Text="LOGGING" />
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="LogLevelLabel" Text="Log level" FontSize="13" />
<TextBlock x:Name="LogLevelDesc" Text="Verbosity of the emulator console output."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ComboBox Grid.Column="1" x:Name="LogLevelBox" Width="160" SelectedIndex="2"
VerticalAlignment="Center" CornerRadius="8">
<ComboBoxItem x:Name="LogLevelTraceItem" Content="Trace" />
<ComboBoxItem x:Name="LogLevelDebugItem" Content="Debug" />
<ComboBoxItem x:Name="LogLevelInfoItem" Content="Info" />
<ComboBoxItem x:Name="LogLevelWarningItem" Content="Warning" />
<ComboBoxItem x:Name="LogLevelErrorItem" Content="Error" />
<ComboBoxItem x:Name="LogLevelCriticalItem" Content="Critical" />
</ComboBox>
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="TraceImportsLabel" Text="Import trace limit" FontSize="13" />
<TextBlock x:Name="TraceImportsDesc" Text="Trace the first N imports per module (0 = off)."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<NumericUpDown Grid.Column="1" x:Name="TraceImportsBox" Width="160" Minimum="0"
Maximum="4096" Increment="16" Value="0" FormatString="0"
VerticalAlignment="Center" CornerRadius="8" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="LogToFileLabel" Text="Log to file" FontSize="13" />
<TextBlock x:Name="LogToFileDesc" Text="Mirror emulator output to a log file."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="LogToFileToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="LogFilePathLabel" Text="Log file path" FontSize="13" />
<TextBlock x:Name="LogFilePathText" Text="No custom path"
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<Button Grid.Column="1" x:Name="SelectLogFilePathButton" Classes="ghost" Content="Select…"
VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="OverrideLogFileLabel" Text="Override log file" FontSize="13" />
<TextBlock x:Name="OverrideLogFileDesc"
Text="Use the exact file path instead of appending title ID and timestamp."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="OverrideLogFileToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
</StackPanel>
</Border>
<Border Classes="card">
<StackPanel Spacing="14">
<TextBlock x:Name="LauncherSectionTitle" Classes="sectionTitle" Text="LAUNCHER" />
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="LanguageLabel" Text="Emulator language" FontSize="13" />
<TextBlock x:Name="LanguageDesc"
Text="Language used throughout the launcher. Applies immediately."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ComboBox Grid.Column="1" x:Name="LanguageBox" Width="160"
VerticalAlignment="Center" CornerRadius="8"
DisplayMemberBinding="{Binding NativeName}" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="TitleMusicLabel" Text="Title music" FontSize="13" />
<TextBlock x:Name="TitleMusicDesc" Text="Loop the selected game's preview music in the library."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="TitleMusicToggle" OnContent="On" OffContent="Off"
IsChecked="True" VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="DiscordLabel" Text="Discord presence" FontSize="13" />
<TextBlock x:Name="DiscordDesc" Text="Show the running game on your Discord profile."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="DiscordToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
</StackPanel>
</Border>
<Border Classes="card">
<StackPanel Spacing="14">
<TextBlock x:Name="AboutSectionTitle"
Classes="sectionTitle"
Text="ABOUT" />
<!--Github-->
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0"
Orientation="Horizontal"
Spacing="8"
VerticalAlignment="Center">
<Image Source="avares://SharpEmu.GUI/Assets/github.png"
Width="20"
Height="20"
VerticalAlignment="Center" />
<StackPanel VerticalAlignment="Center"
Spacing="2">
<TextBlock x:Name="GithubLabel"
Text="GitHub"
FontSize="13" />
<TextBlock x:Name="GithubDesc"
Text="Source code, issues and project development."
FontSize="11"
Foreground="{StaticResource MutedBrush}"
TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
<Button Grid.Column="1"
x:Name="GithubButton"
Classes="ghost"
Content="Open"
VerticalAlignment="Center" />
</Grid>
<!--Discord-->
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0"
Orientation="Horizontal"
Spacing="8"
VerticalAlignment="Center">
<Image Source="avares://SharpEmu.GUI/Assets/discord.png"
Width="20"
Height="20"
VerticalAlignment="Center" />
<StackPanel VerticalAlignment="Center"
Spacing="2">
<TextBlock x:Name="DiscordServerLabel"
Text="Discord"
FontSize="13" />
<TextBlock x:Name="DiscordServerDesc"
Text="Join the community, get support and follow development."
FontSize="11"
Foreground="{StaticResource MutedBrush}"
TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
<Button Grid.Column="1"
x:Name="DiscordButton"
Classes="ghost"
Content="Join"
VerticalAlignment="Center" />
</Grid>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</TabItem>
<TabItem x:Name="EnvTabItem" Header="Environment" FontSize="15">
<ScrollViewer>
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
<Border Classes="card">
<StackPanel Spacing="14">
<TextBlock x:Name="EnvSectionTitle" Classes="sectionTitle" Text="ENVIRONMENT VARIABLES" />
<TextBlock x:Name="EnvDesc"
Text="Switches passed to the emulator as environment variables at launch."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock Text="SHARPEMU_BTHID_UNAVAILABLE" FontSize="13" FontFamily="Consolas,monospace" />
<TextBlock x:Name="EnvBthidDesc"
Text="Report Bluetooth HID as unavailable for titles whose wheel/FFB middleware polls forever.&#10;Leave off normally. Some titles freeze when init fails."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="EnvBthidToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock Text="SHARPEMU_DISABLE_IMPORT_LOOP_GUARD" FontSize="13" FontFamily="Consolas,monospace" />
<TextBlock x:Name="EnvLoopGuardDesc"
Text="Do not force quit titles that repeat the same call for too long.&#10;Try this when a game exits on its own while loading."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="EnvLoopGuardToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock Text="SHARPEMU_VK_VALIDATION" FontSize="13" FontFamily="Consolas,monospace" />
<TextBlock x:Name="EnvVkValidationDesc"
Text="Enable Vulkan validation layers for GPU debugging.&#10;Slow. Requires the Vulkan SDK to be installed."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="EnvVkValidationToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock Text="SHARPEMU_DUMP_SPIRV" FontSize="13" FontFamily="Consolas,monospace" />
<TextBlock x:Name="EnvDumpSpirvDesc"
Text="Dump AGC shaders and their SPIR-V translations to the shader-dumps folder.&#10;Use when reporting shader or rendering bugs."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="EnvDumpSpirvToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock Text="SHARPEMU_LOG_DIRECT_MEMORY" FontSize="13" FontFamily="Consolas,monospace" />
<TextBlock x:Name="EnvLogDirectMemoryDesc"
Text="Log direct memory allocations and failures to the console.&#10;Use when a game aborts or exits during boot."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="EnvLogDirectMemoryToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock Text="SHARPEMU_LOG_NP" FontSize="13" FontFamily="Consolas,monospace" />
<TextBlock x:Name="EnvLogNpDesc"
Text="Log NP (PlayStation Network) library calls to the console."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="EnvLogNpToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</TabItem>
</TabControl>
</Grid>
<!-- 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>
</Panel>
<!-- Console (collapsible) -->
@@ -478,7 +161,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
Margin="0,12,0,0" IsVisible="False">
<Grid RowDefinitions="Auto,*">
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto,Auto,Auto" Margin="16,12,16,8">
<TextBlock x:Name="ConsoleSectionTitle" Classes="sectionTitle" Text="CONSOLE" VerticalAlignment="Center" />
<TextBlock Classes="sectionTitle" Text="CONSOLE" VerticalAlignment="Center" />
<TextBox Grid.Column="1" FontSize="12" Margin="0,0,12,0" x:Name="ConsoleSearchBox"
Watermark="Search..." Width="320" />
<CheckBox Grid.Column="2" x:Name="AutoScrollCheck" Content="Auto-scroll" IsChecked="True"
@@ -501,8 +184,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
</Grid>
</Border>
<!-- Launch bar; capped so it does not sprawl on a maximized window. -->
<Border Grid.Row="3" Classes="card" Margin="0,12,0,0" Padding="14" MaxWidth="1280">
<!-- Launch bar -->
<Border Grid.Row="3" Classes="card" Margin="0,12,0,0" Padding="14">
<StackPanel Spacing="14">
<Grid ColumnDefinitions="Auto,*,Auto">
@@ -521,32 +204,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
</Panel>
</Border>
<StackPanel Grid.Column="1" Spacing="4" VerticalAlignment="Center" Margin="14,0,14,0">
<Grid ColumnDefinitions="Auto,Auto,*">
<TextBlock Grid.Column="0" x:Name="SelectedGameTitle" Text="No game selected" FontSize="16"
FontWeight="Bold" TextTrimming="CharacterEllipsis" VerticalAlignment="Center"
MaxWidth="340" Margin="0,0,10,0" />
<!-- Title id / version / size badges, right next to the
title. The title's own MaxWidth (not a "*" column) is
what keeps them from drifting to the far right. -->
<StackPanel Grid.Column="1" x:Name="SelectedBadgesRow" Orientation="Horizontal" Spacing="6"
IsVisible="False" VerticalAlignment="Center">
<Border Classes="pill" IsVisible="{Binding HasTitleId, FallbackValue=False}">
<TextBlock Text="{Binding TitleId}" FontSize="10" FontWeight="SemiBold"
Foreground="{StaticResource MutedBrush}" />
</Border>
<Border Classes="pill" IsVisible="{Binding HasVersion, FallbackValue=False}">
<TextBlock Text="{Binding VersionText}" FontSize="10" FontWeight="SemiBold"
Foreground="{StaticResource MutedBrush}" />
</Border>
<Border Classes="pill">
<TextBlock Text="{Binding SizeText, FallbackValue=''}" FontSize="10" FontWeight="SemiBold"
Foreground="{StaticResource MutedBrush}" />
</Border>
</StackPanel>
</Grid>
<StackPanel Grid.Column="1" Spacing="3" VerticalAlignment="Center" Margin="14,0,14,0">
<TextBlock x:Name="SelectedGameTitle" Text="No game selected" FontSize="16" FontWeight="Bold"
TextTrimming="CharacterEllipsis" />
<TextBlock x:Name="SelectedGamePath" Text="Pick a game from the library, or open an eboot.bin directly."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextTrimming="CharacterEllipsis" />
<StackPanel Orientation="Horizontal" Spacing="7">
@@ -558,18 +218,69 @@ SPDX-License-Identifier: GPL-2.0-or-later
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
<ToggleButton x:Name="OptionsToggle" Classes="ghost" Content="⚙ Options" />
<ToggleButton x:Name="ConsoleToggle" Classes="ghost" Content="≡ Console" />
<Button x:Name="LaunchButton" Classes="accent" Content="▶ Launch" IsEnabled="False" />
<Button x:Name="StopButton" Classes="danger" Content="■ Stop" IsEnabled="False" />
</StackPanel>
</Grid>
<!-- Launch options (collapsible) -->
<WrapPanel x:Name="OptionsPanel" IsVisible="False">
<StackPanel Margin="0,0,24,0">
<TextBlock Classes="fieldLabel" Text="CPU engine" />
<ComboBox x:Name="CpuEngineBox" Width="150" SelectedIndex="0" CornerRadius="8">
<ComboBoxItem Content="Native" />
</ComboBox>
</StackPanel>
<StackPanel Margin="0,0,24,0">
<TextBlock Classes="fieldLabel" Text="Log level" />
<ComboBox x:Name="LogLevelBox" Width="150" SelectedIndex="2" CornerRadius="8">
<ComboBoxItem Content="Trace" />
<ComboBoxItem Content="Debug" />
<ComboBoxItem Content="Info" />
<ComboBoxItem Content="Warning" />
<ComboBoxItem Content="Error" />
<ComboBoxItem Content="Critical" />
</ComboBox>
</StackPanel>
<StackPanel Margin="0,0,24,0">
<TextBlock Classes="fieldLabel" Text="Import trace limit (0 = off)" />
<NumericUpDown x:Name="TraceImportsBox" Width="170" Minimum="0" Maximum="4096" Increment="16"
Value="0" FormatString="0" CornerRadius="8" />
</StackPanel>
<StackPanel Margin="0,0,24,0">
<TextBlock Classes="fieldLabel" Text="Strict dynlib resolution" />
<ToggleSwitch x:Name="StrictToggle" OnContent="On" OffContent="Off" />
</StackPanel>
<StackPanel Margin="0,0,24,0">
<TextBlock Classes="fieldLabel" Text="Log to file" />
<ToggleSwitch x:Name="LogToFileToggle" OnContent="On" OffContent="Off" />
</StackPanel>
<StackPanel Margin="0,0,24,0">
<TextBlock Classes="fieldLabel" Text="Log file path" />
<Button x:Name="SelectLogFilePathButton" Classes="ghost" Content="Select…"
VerticalAlignment="Center" />
</StackPanel>
<StackPanel Margin="0,0,24,0">
<TextBlock Classes="fieldLabel" Text="Override log file" />
<ToggleSwitch x:Name="OverrideLogFileToggle" OnContent="On" OffContent="Off" />
</StackPanel>
<StackPanel Margin="0,0,24,0">
<TextBlock Classes="fieldLabel" Text="Title music" />
<ToggleSwitch x:Name="TitleMusicToggle" OnContent="On" OffContent="Off" IsChecked="True" />
</StackPanel>
<StackPanel>
<TextBlock Classes="fieldLabel" Text="Discord presence" />
<ToggleSwitch x:Name="DiscordToggle" OnContent="On" OffContent="Off" />
</StackPanel>
</WrapPanel>
</StackPanel>
</Border>
</Grid>
<!-- Status bar -->
<Grid x:Name="StatusBar" Grid.Row="2" Height="32" Background="{StaticResource ChromeBrush}"
ColumnDefinitions="*,Auto">
<Grid Grid.Row="2" Background="{StaticResource ChromeBrush}" ColumnDefinitions="*,Auto">
<TextBlock x:Name="EmulatorPathText" Grid.Column="0" Text="Emulator: locating…" FontSize="11"
Foreground="{StaticResource FaintBrush}" VerticalAlignment="Center" Margin="16,0"
TextTrimming="CharacterEllipsis" />
+113 -449
View File
@@ -12,8 +12,7 @@ using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using Avalonia.VisualTree;
using SharpEmu.HLE.Host;
using SharpEmu.HLE.Host.Windows;
using SharpEmu.Libs.Pad;
using SharpEmu.Logging;
using System.Collections.Concurrent;
using System.Collections.ObjectModel;
@@ -26,7 +25,6 @@ namespace SharpEmu.GUI;
public partial class MainWindow : Window
{
private const int MaxConsoleLines = 4000;
private const int MaxConsoleLinesPerFlush = 500;
private static readonly IBrush DefaultLineBrush = new SolidColorBrush(Color.Parse("#C7CFDE"));
private static readonly IBrush DimLineBrush = new SolidColorBrush(Color.Parse("#6B7488"));
@@ -50,7 +48,6 @@ public partial class MainWindow : Window
private string? _emulatorExePath;
private bool _isRunning;
private int _autoScrollTicks;
private int _activePageIndex;
// Discord Rich Presence state.
private readonly long _launcherStartUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
@@ -63,7 +60,7 @@ public partial class MainWindow : Window
// Controller navigation state.
private readonly DispatcherTimer _gamepadTimer;
private HostGamepadButtons _previousPadButtons;
private uint _previousPadButtons;
private long _navLeftNextAt;
private long _navRightNextAt;
private long _navUpNextAt;
@@ -103,121 +100,36 @@ public partial class MainWindow : Window
ClearLogButton.Click += (_, _) => _consoleLines.Clear();
CopyLogButton.Click += async (_, _) => await CopyConsoleAsync();
DetachConsoleButton.Click += (_, _) => ShowConsoleWindow();
LibraryTabButton.Click += (_, _) => SetActivePage(0);
OptionsTabButton.Click += (_, _) => SetActivePage(1);
OptionsToggle.IsCheckedChanged += (_, _) => OptionsPanel.IsVisible = OptionsToggle.IsChecked == true;
ConsoleToggle.IsCheckedChanged += (_, _) => ConsolePanel.IsVisible = ConsoleToggle.IsChecked == true && _consoleWindow is null;
// 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();
};
SelectLogFilePathButton.Click += async (_, _) => await SelectFilePathAsync();
TitleMusicToggle.IsCheckedChanged += (_, _) => OnTitleMusicToggled();
DiscordToggle.IsCheckedChanged += (_, _) =>
{
_settings.DiscordRichPresence = DiscordToggle.IsChecked == true;
UpdateDiscordPresence();
};
SelectLogFilePathButton.Click += async (_, _) => await SelectLogFilePathAsync();
EnvBthidToggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle("SHARPEMU_BTHID_UNAVAILABLE", EnvBthidToggle.IsChecked == true);
EnvLoopGuardToggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD", EnvLoopGuardToggle.IsChecked == true);
EnvVkValidationToggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle("SHARPEMU_VK_VALIDATION", EnvVkValidationToggle.IsChecked == true);
EnvDumpSpirvToggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle("SHARPEMU_DUMP_SPIRV", EnvDumpSpirvToggle.IsChecked == true);
EnvLogDirectMemoryToggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle("SHARPEMU_LOG_DIRECT_MEMORY", EnvLogDirectMemoryToggle.IsChecked == true);
EnvLogNpToggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle("SHARPEMU_LOG_NP", EnvLogNpToggle.IsChecked == true);
LanguageBox.SelectionChanged += (_, _) => OnLanguageChanged();
GameList.AddHandler(ContextRequestedEvent, OnGameContextRequested, RoutingStrategies.Tunnel);
CtxLaunch.Click += (_, _) => LaunchSelected();
CtxOpenFolder.Click += (_, _) => OpenSelectedGameFolder();
CtxCopyPath.Click += async (_, _) =>
await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.Path, "Clipboard.Path");
await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.Path, "Path");
CtxCopyTitleId.Click += async (_, _) =>
await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.TitleId, "Clipboard.TitleId");
await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.TitleId, "Title ID");
CtxRemove.Click += (_, _) => RemoveSelectedFromLibrary();
Opened += async (_, _) => await OnOpenedAsync();
Closing += (_, _) => OnWindowClosing();
WindowsDualSenseReader.EnsureStarted();
WindowsXInputReader.EnsureStarted();
DualSenseReader.EnsureStarted();
XInputReader.EnsureStarted();
_gamepadTimer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(50),
};
_gamepadTimer.Tick += (_, _) => PollGamepad();
_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 ----
@@ -225,42 +137,25 @@ public partial class MainWindow : Window
private void PollGamepad()
{
// DualSense wins when both are connected; XInput covers Xbox pads.
if (!WindowsDualSenseReader.TryGetState(out var pad) && !WindowsXInputReader.TryGetState(out pad))
if (!DualSenseReader.TryGetState(out var pad) && !XInputReader.TryGetState(out pad))
{
_previousPadButtons = HostGamepadButtons.None;
_previousPadButtons = 0;
return;
}
if (!IsActive)
{
// Ignore input while the launcher is in the background, e.g. the
// game window is focused and using the same controller.
_previousPadButtons = pad.Buttons;
return;
}
var shoulderPressed = pad.Buttons & ~_previousPadButtons;
if ((shoulderPressed & HostGamepadButtons.L1) != 0)
{
SetActivePage(0);
}
if ((shoulderPressed & HostGamepadButtons.R1) != 0)
{
SetActivePage(1);
}
if (_activePageIndex != 0)
{
// Ignore input while the launcher is in the background (e.g. the
// game window is focused and using the same controller).
_previousPadButtons = pad.Buttons;
return;
}
var now = Environment.TickCount64;
var left = (pad.Buttons & HostGamepadButtons.Left) != 0 || pad.LeftX < 64;
var right = (pad.Buttons & HostGamepadButtons.Right) != 0 || pad.LeftX > 192;
var up = (pad.Buttons & HostGamepadButtons.Up) != 0 || pad.LeftY < 64;
var down = (pad.Buttons & HostGamepadButtons.Down) != 0 || pad.LeftY > 192;
var left = (pad.Buttons & 0x0080) != 0 || pad.LeftX < 64;
var right = (pad.Buttons & 0x0020) != 0 || pad.LeftX > 192;
var up = (pad.Buttons & 0x0010) != 0 || pad.LeftY < 64;
var down = (pad.Buttons & 0x0040) != 0 || pad.LeftY > 192;
if (ShouldNavigate(left, ref _navLeftNextAt, now))
{
@@ -283,12 +178,12 @@ public partial class MainWindow : Window
}
var pressed = pad.Buttons & ~_previousPadButtons;
if ((pressed & HostGamepadButtons.Cross) != 0)
if ((pressed & 0x4000) != 0) // Cross
{
LaunchSelected();
}
if ((pressed & HostGamepadButtons.Circle) != 0)
if ((pressed & 0x2000) != 0) // Circle
{
StopEmulator();
}
@@ -359,143 +254,12 @@ public partial class MainWindow : Window
ToolTip.SetTip(VersionText, BuildInfo.Banner);
_settings = GuiSettings.Load();
Localization.Instance.Load(_settings.Language);
PopulateLanguageBox();
ApplyLocalization();
ApplySettingsToControls();
LocateEmulator();
UpdateDiscordPresence();
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");
EnvTabItem.Header = loc.Get("Options.Env.Tab");
EnvSectionTitle.Text = loc.Get("Options.Section.Environment");
EnvDesc.Text = loc.Get("Options.Env.Desc");
EnvBthidDesc.Text = loc.Get("Options.Env.Bthid.Desc");
EnvLoopGuardDesc.Text = loc.Get("Options.Env.LoopGuard.Desc");
EnvVkValidationDesc.Text = loc.Get("Options.Env.VkValidation.Desc");
EnvDumpSpirvDesc.Text = loc.Get("Options.Env.DumpSpirv.Desc");
EnvLogDirectMemoryDesc.Text = loc.Get("Options.Env.LogDirectMemory.Desc");
EnvLogNpDesc.Text = loc.Get("Options.Env.LogNp.Desc");
EmulationSectionTitle.Text = loc.Get("Options.Section.Emulation");
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 ----
/// <summary>
@@ -516,7 +280,7 @@ public partial class MainWindow : Window
if (_isRunning && _runningGameName is { } gameName)
{
_discord.SetPresence(
Localization.Instance.Format("Discord.Playing", gameName),
$"Playing {gameName}",
_runningGameTitleId,
_runningSinceUnixSeconds);
}
@@ -524,12 +288,9 @@ public partial class MainWindow : Window
{
// Discord does not render activities without timestamps, so the
// 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(
Localization.Instance.Get("Discord.Browsing"),
count,
"Browsing the library",
$"{_allGames.Count} game(s)",
_launcherStartUnixSeconds);
}
}
@@ -554,20 +315,17 @@ public partial class MainWindow : Window
{
WindowState = WindowState.Normal;
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.PreferSystemChrome;
TitleBar.IsVisible = true;
StatusBar.IsVisible = true;
}
else
{
WindowState = WindowState.FullScreen;
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.NoChrome;
TitleBar.IsVisible = false;
StatusBar.IsVisible = false;
}
}
private void OnWindowClosing()
{
ReadControlsIntoSettings();
_settings.Save();
_consoleFlushTimer.Stop();
_gamepadTimer.Stop();
@@ -586,7 +344,7 @@ public partial class MainWindow : Window
}
}
// ---- Settings ----
// ---- Settings <-> controls ----
private void ApplySettingsToControls()
{
@@ -605,33 +363,19 @@ public partial class MainWindow : Window
LogToFileToggle.IsChecked = _settings.LogToFile;
OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile;
TitleMusicToggle.IsChecked = _settings.PlayTitleMusic;
ToolTip.SetTip(SelectLogFilePathButton, string.IsNullOrWhiteSpace(_settings.LogFilePath) ? "No path selected" : _settings.LogFilePath);
DiscordToggle.IsChecked = _settings.DiscordRichPresence;
EnvBthidToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_BTHID_UNAVAILABLE");
EnvLoopGuardToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD");
EnvVkValidationToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_VK_VALIDATION");
EnvDumpSpirvToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_DUMP_SPIRV");
EnvLogDirectMemoryToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_DIRECT_MEMORY");
EnvLogNpToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_NP");
UpdateLogFilePathText();
}
// Environment variables set on this process at the previous launch; children
// inherit the process environment, so stale names must be cleared explicitly.
private readonly HashSet<string> _appliedEnvironmentVariables = new(StringComparer.OrdinalIgnoreCase);
private void SetEnvironmentToggle(string name, bool enabled)
private void ReadControlsIntoSettings()
{
if (enabled)
{
if (!_settings.EnvironmentToggles.Contains(name))
{
_settings.EnvironmentToggles.Add(name);
}
}
else
{
_settings.EnvironmentToggles.Remove(name);
}
_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()
@@ -648,35 +392,6 @@ 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 ----
private void LocateEmulator()
@@ -706,8 +421,8 @@ public partial class MainWindow : Window
: null;
EmulatorPathText.Text = _emulatorExePath is not null
? Localization.Instance.Format("Status.EmulatorPath", _emulatorExePath)
: Localization.Instance.Get("Status.EmulatorNotFound");
? $"Emulator: {_emulatorExePath}"
: "Emulator: SharpEmu executable not found — build SharpEmu.CLI first.";
}
// ---- Game library ----
@@ -716,7 +431,7 @@ public partial class MainWindow : Window
{
var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{
Title = Localization.Instance.Get("Dialog.ChooseGameFolder"),
Title = "Choose a folder containing games",
AllowMultiple = false,
});
@@ -751,21 +466,18 @@ public partial class MainWindow : Window
{
var folders = _settings.GameFolders.ToArray();
var excluded = new HashSet<string>(_settings.ExcludedGames, StringComparer.OrdinalIgnoreCase);
StatusBarRight.Text = Localization.Instance.Get("Status.ScanningLibrary");
EmptyState.IsVisible = false;
LoadingState.IsVisible = true;
StatusBarRight.Text = "Scanning library";
var games = await Task.Run(() => ScanFolders(folders, excluded));
_allGames.Clear();
_allGames.AddRange(games);
RefreshVisibleGames();
LoadingState.IsVisible = false;
LoadGameDetailsInBackground(games);
UpdateDiscordPresence();
StatusBarRight.Text = folders.Length == 0
? Localization.Instance.Get("Status.AddFolderPrompt")
: Localization.Instance.Format("Status.LibraryScanned", games.Count, folders.Length);
? "Add a game folder to populate the library."
: $"Library scanned: {games.Count} game(s) in {folders.Length} folder(s).";
}
/// <summary>
@@ -902,9 +614,9 @@ public partial class MainWindow : Window
{
}
var (title, titleId, version) = TryReadParamJson(fullPath);
var (title, titleId) = TryReadParamJson(fullPath);
games.Add(new GameEntry(
title ?? GameNameFor(fullPath), titleId, version, fullPath, size,
title ?? GameNameFor(fullPath), titleId, fullPath, size,
FindCoverFor(fullPath), FindBackgroundFor(fullPath)));
}
}
@@ -919,23 +631,23 @@ public partial class MainWindow : Window
}
/// <summary>
/// Reads the game title, title id and content version from
/// sce_sys/param.json next to the executable, when present.
/// Reads the game title and title id from sce_sys/param.json next to the
/// executable, when present.
/// </summary>
private static (string? Title, string? TitleId, string? Version) TryReadParamJson(string ebootPath)
private static (string? Title, string? TitleId) TryReadParamJson(string ebootPath)
{
try
{
var directory = Path.GetDirectoryName(ebootPath);
if (directory is null)
{
return (null, null, null);
return (null, null);
}
var paramPath = Path.Combine(directory, "sce_sys", "param.json");
if (!File.Exists(paramPath))
{
return (null, null, null);
return (null, null);
}
// ReadAllText handles a UTF-8 BOM, which JsonDocument rejects in
@@ -949,20 +661,6 @@ public partial class MainWindow : Window
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;
if (root.TryGetProperty("localizedParameters", out var localized) &&
localized.ValueKind == JsonValueKind.Object)
@@ -993,12 +691,11 @@ public partial class MainWindow : Window
return (
string.IsNullOrWhiteSpace(title) ? null : title,
string.IsNullOrWhiteSpace(titleId) ? null : titleId,
string.IsNullOrWhiteSpace(version) ? null : version.Trim());
string.IsNullOrWhiteSpace(titleId) ? null : titleId);
}
catch (Exception)
{
return (null, null, null);
return (null, null);
}
}
@@ -1109,12 +806,11 @@ public partial class MainWindow : Window
}
catch (Exception ex)
{
StatusBarRight.Text = Localization.Instance.Format("Status.CouldNotOpenFolder", ex.Message);
StatusBarRight.Text = $"Could not open folder: {ex.Message}";
}
}
/// <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)
private async Task CopyToClipboardAsync(string? text, string what)
{
if (string.IsNullOrEmpty(text) || Clipboard is null)
{
@@ -1122,7 +818,7 @@ public partial class MainWindow : Window
}
await Clipboard.SetTextAsync(text);
StatusBarRight.Text = Localization.Instance.Format("Status.CopiedToClipboard", Localization.Instance.Get(whatKey));
StatusBarRight.Text = $"{what} copied to clipboard.";
}
private void RemoveSelectedFromLibrary()
@@ -1141,7 +837,7 @@ public partial class MainWindow : Window
_allGames.RemoveAll(g => string.Equals(g.Path, game.Path, StringComparison.OrdinalIgnoreCase));
GameList.SelectedItem = null;
RefreshVisibleGames();
StatusBarRight.Text = Localization.Instance.Format("Status.RemovedFromLibrary", game.Name);
StatusBarRight.Text = $"Removed “{game.Name}” from the library. Re-add its folder to restore it.";
}
private void RefreshVisibleGames()
@@ -1161,6 +857,8 @@ public partial class MainWindow : Window
}
}
GameCountText.Text = _visibleGames.Count == 1 ? "1 game" : $"{_visibleGames.Count} games";
if (selectedPath is not null &&
_visibleGames.FirstOrDefault(g => g.Path.Equals(selectedPath, StringComparison.OrdinalIgnoreCase))
is { } reselected)
@@ -1169,50 +867,34 @@ public partial class MainWindow : Window
}
EmptyState.IsVisible = _visibleGames.Count == 0;
UpdateEmptyStateTexts();
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)
if (_visibleGames.Count == 0)
{
return;
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;
}
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;
UpdateSelectedGame();
}
private void UpdateSelectedGame()
{
if (GameList.SelectedItem is GameEntry game)
{
UpdateSelectedGameTexts();
SelectedGameTitle.Text = game.Name;
SelectedGamePath.Text = game.Path;
SelectedCoverPanel.DataContext = game;
SelectedBadgesRow.DataContext = game;
SelectedBadgesRow.IsVisible = true;
_ = UpdateBackdropAsync(game);
PlaySelectedGamePreview(game);
}
else
{
UpdateSelectedGameTexts();
SelectedGameTitle.Text = "No game selected";
SelectedGamePath.Text = "Pick a game from the library, or open an eboot.bin directly.";
SelectedCoverPanel.DataContext = null;
SelectedBadgesRow.DataContext = null;
SelectedBadgesRow.IsVisible = false;
_ = UpdateBackdropAsync(null);
_sndPreview.Stop();
}
@@ -1220,25 +902,6 @@ public partial class MainWindow : Window
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>
/// Loops the selected game's sce_sys/snd0.at9 preview music, console
/// home screen style. Silent while a game is running or when disabled
@@ -1263,8 +926,9 @@ public partial class MainWindow : Window
}
}
private void OnTitleMusicSettingChanged()
private void OnTitleMusicToggled()
{
_settings.PlayTitleMusic = TitleMusicToggle.IsChecked == true;
if (!_settings.PlayTitleMusic)
{
_sndPreview.Stop();
@@ -1337,12 +1001,11 @@ public partial class MainWindow : Window
{
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = Localization.Instance.Get("Dialog.OpenExecutable"),
Title = "Open an executable to launch",
AllowMultiple = false,
FileTypeFilter = new[]
{
new FilePickerFileType(Localization.Instance.Get("Dialog.PsExecutables"))
{ Patterns = new[] { "eboot.bin", "*.bin", "*.self", "*.elf" } },
new FilePickerFileType("PS executables") { Patterns = new[] { "eboot.bin", "*.bin", "*.self", "*.elf" } },
FilePickerFileTypes.All,
},
});
@@ -1374,12 +1037,14 @@ public partial class MainWindow : Window
LocateEmulator();
if (_emulatorExePath is null)
{
AppendConsoleLine(Localization.Instance.Get("Launch.ExeNotFound"), ErrorLineBrush);
AppendConsoleLine("SharpEmu executable not found. Build the SharpEmu.CLI project first (dotnet build).", ErrorLineBrush);
return;
}
}
_sndPreview.Stop();
ReadControlsIntoSettings();
_settings.Save();
var arguments = new List<string>
{
@@ -1397,6 +1062,7 @@ public partial class MainWindow : Window
}
_consoleLines.Clear();
ConsoleToggle.IsChecked = true;
// Let the CLI mirror stdout/stderr itself; it sees loader/native
// diagnostics before the GUI pipe reader can filter or batch them.
@@ -1439,33 +1105,13 @@ public partial class MainWindow : Window
{
arguments.Add("--log-file");
arguments.Add(filePath);
AppendConsoleLine(Localization.Instance.Format("Launch.LogFile", filePath), DimLineBrush);
AppendConsoleLine($"Log file: {filePath}", DimLineBrush);
}
}
arguments.Add(ebootPath);
AppendConsoleLine(
Localization.Instance.Format("Launch.Command", string.Join(' ', arguments)),
DimLineBrush);
// Apply the enabled switches to this process; both emulator launch paths
// (CreateProcessW and Process.Start) inherit it. Clear switches turned
// off since the previous launch.
foreach (var staleName in _appliedEnvironmentVariables)
{
if (!_settings.EnvironmentToggles.Contains(staleName))
{
Environment.SetEnvironmentVariable(staleName, null);
}
}
_appliedEnvironmentVariables.Clear();
foreach (var name in _settings.EnvironmentToggles)
{
Environment.SetEnvironmentVariable(name, "1");
_appliedEnvironmentVariables.Add(name);
}
AppendConsoleLine($"$ SharpEmu {string.Join(' ', arguments)}", DimLineBrush);
var emulator = new EmulatorProcess();
emulator.OutputReceived += (line, isError) => _pendingLines.Enqueue((line, isError));
@@ -1478,7 +1124,7 @@ public partial class MainWindow : Window
catch (Exception ex)
{
emulator.Dispose();
AppendConsoleLine(Localization.Instance.Format("Launch.StartFailed", ex.Message), ErrorLineBrush);
AppendConsoleLine($"Failed to start the emulator: {ex.Message}", ErrorLineBrush);
DropFileLog();
return;
}
@@ -1491,8 +1137,8 @@ public partial class MainWindow : Window
.TitleId;
_runningSinceUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
StatusDot.Fill = SuccessLineBrush;
StatusText.Text = Localization.Instance.Format("Launch.Running", displayName);
StatusBarRight.Text = Localization.Instance.Format("Status.Running", displayName);
StatusText.Text = $"Running — {displayName}";
StatusBarRight.Text = $"Running {displayName}";
UpdateRunButtons();
UpdateDiscordPresence();
}
@@ -1514,8 +1160,8 @@ public partial class MainWindow : Window
_emulator?.Stop();
_runningGameName = null;
_runningGameTitleId = null;
StatusText.Text = Localization.Instance.Get("Launch.Stopping");
StatusBarRight.Text = Localization.Instance.Get("Status.Stopping");
StatusText.Text = "Stopping";
StatusBarRight.Text = "Stopping";
UpdateDiscordPresence();
}
@@ -1557,24 +1203,22 @@ public partial class MainWindow : Window
_emulator?.Dispose();
_emulator = null;
var meaningKey = exitCode switch
var meaning = exitCode switch
{
0 => "Exit.Ok",
1 => "Exit.InvalidArguments",
2 => "Exit.EbootNotFound",
3 => "Exit.RuntimeException",
4 => "Exit.EmulationError",
-1073741819 => "Exit.EmulationError",
_ => "Exit.Unknown",
0 => "OK",
1 => "invalid arguments",
2 => "eboot not found",
3 => "runtime exception",
4 => "emulation error",
_ => "unknown",
};
var meaning = Localization.Instance.Get(meaningKey);
var brush = exitCode == 0 ? SuccessLineBrush : ErrorLineBrush;
AppendConsoleLine(Localization.Instance.Format("Launch.ProcessExited", exitCode, meaning), brush);
AppendConsoleLine($"Process exited with code {exitCode} ({meaning}).", brush);
CloseFileLogSoon();
StatusDot.Fill = exitCode == 0 ? (IBrush)SuccessLineBrush : ErrorLineBrush;
StatusText.Text = Localization.Instance.Format("Launch.Exited", exitCode, meaning);
StatusBarRight.Text = Localization.Instance.Get("Status.Idle");
StatusText.Text = $"Exited with code {exitCode} ({meaning})";
StatusBarRight.Text = "Idle";
_runningGameName = null;
_runningGameTitleId = null;
UpdateRunButtons();
@@ -1588,6 +1232,27 @@ public partial class MainWindow : Window
OpenFileButton.IsEnabled = !_isRunning;
}
private async Task SelectFilePathAsync()
{
SaveFilePickerResult result = await StorageProvider.SaveFilePickerWithResultAsync(new FilePickerSaveOptions
{
Title = "Select where to save the Log file",
SuggestedFileName = "SharpEmuLog",
DefaultExtension = "log",
FileTypeChoices =
[
new FilePickerFileType("Plain Text Files") { Patterns = ["*.txt"] },
new FilePickerFileType("Log Files") { Patterns = ["*.log"] }
]
});
if (result.File is not null)
{
_settings.LogFilePath = result.File.Path.LocalPath;
ToolTip.SetTip(SelectLogFilePathButton, _settings.LogFilePath);
}
}
// ---- Console ----
private void FlushPendingConsoleLines()
@@ -1598,8 +1263,7 @@ public partial class MainWindow : Window
}
var incoming = new List<LogLine>();
while (incoming.Count < MaxConsoleLinesPerFlush &&
_pendingLines.TryDequeue(out var pending))
while (_pendingLines.TryDequeue(out var pending))
{
WriteFileLog(pending.Line);
incoming.Add(new LogLine(pending.Line, BrushForLine(pending.Line)));
+7 -20
View File
@@ -10,9 +10,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
<PropertyGroup>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<Version>0.0.1</Version>
<!-- Required by the source-generated LibraryImport stubs in the linked
controller readers below. -->
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<!-- Dependency-free; provides the BuildInfo provenance shown in the
@@ -31,27 +28,17 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ItemGroup>
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
<AvaloniaResource Include="..\..\assets\images\github.png" Link="Assets/github.png" />
<AvaloniaResource Include="..\..\assets\images\discord.png" Link="Assets/discord.png" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Languages\*.json">
<LogicalName>Languages.%(Filename)%(Extension)</LogicalName>
</EmbeddedResource>
</ItemGroup>
<!-- The controller readers (DualSense raw HID + Xbox XInput) are shared
with the emulator's host input backend. They are dependency-free, so
they are compiled in directly rather than pulling a reference to all
of SharpEmu.HLE into the launcher. -->
with the emulator's pad HLE. They are dependency-free, so they are
compiled in directly rather than pulling a reference to all of
SharpEmu.Libs into the launcher. -->
<ItemGroup>
<Compile Include="..\SharpEmu.HLE\Host\HostGamepadState.cs" Link="Input/HostGamepadState.cs" />
<Compile Include="..\SharpEmu.HLE\Host\Windows\WindowsHidNative.cs" Link="Input/WindowsHidNative.cs" />
<Compile Include="..\SharpEmu.HLE\Host\Windows\WindowsDualSenseReader.cs" Link="Input/WindowsDualSenseReader.cs" />
<Compile Include="..\SharpEmu.HLE\Host\Windows\WindowsXInputReader.cs" Link="Input/WindowsXInputReader.cs" />
<Compile Include="..\SharpEmu.Libs\Pad\PadState.cs" Link="Input/PadState.cs" />
<Compile Include="..\SharpEmu.Libs\Pad\HidNative.cs" Link="Input/HidNative.cs" />
<Compile Include="..\SharpEmu.Libs\Pad\DualSenseReader.cs" Link="Input/DualSenseReader.cs" />
<Compile Include="..\SharpEmu.Libs\Pad\XInputReader.cs" Link="Input/XInputReader.cs" />
</ItemGroup>
</Project>
+1 -1
View File
@@ -47,7 +47,7 @@ internal sealed class SndPreviewPlayer
{
// Debounce so skimming through the library does not decode (or
// start) a preview per tile.
await Task.Delay(120).ConfigureAwait(false);
await Task.Delay(300).ConfigureAwait(false);
byte[]? wav;
lock (_sync)
Binary file not shown.
+10 -51
View File
@@ -1,7 +1,6 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers;
using System.Buffers.Binary;
using System.Text;
@@ -239,63 +238,23 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
return false;
}
const int StackBufferLength = 512;
const int ReadChunkLength = 128;
var rented = capacity > StackBufferLength ? ArrayPool<byte>.Shared.Rent(capacity) : null;
Span<byte> bytes = rented is null ? stackalloc byte[StackBufferLength] : rented;
try
var bytes = new byte[capacity];
for (var index = 0; index < bytes.Length; index++)
{
var length = 0;
while (length < capacity)
if (!Memory.TryRead(address + (ulong)index, bytes.AsSpan(index, 1)))
{
// Bulk-read in bounded chunks rather than the full capacity: the string
// may end just before unmapped memory, and overreading past the
// terminator by more than a chunk could fault where the old
// byte-by-byte loop succeeded.
var chunk = Math.Min(ReadChunkLength, capacity - length);
var span = bytes.Slice(length, chunk);
if (Memory.TryRead(address + (ulong)length, span))
{
var terminator = span.IndexOf((byte)0);
if (terminator >= 0)
{
value = Encoding.UTF8.GetString(bytes[..(length + terminator)]);
return true;
}
length += chunk;
continue;
}
// The chunk touches an unreadable range; fall back to per-byte reads so a
// terminator sitting before the bad byte still yields the string.
for (var i = 0; i < chunk; i++)
{
if (!Memory.TryRead(address + (ulong)(length + i), bytes.Slice(length + i, 1)))
{
return false;
}
if (bytes[length + i] == 0)
{
value = Encoding.UTF8.GetString(bytes[..(length + i)]);
return true;
}
}
length += chunk;
return false;
}
value = Encoding.UTF8.GetString(bytes[..capacity]);
return true;
}
finally
{
if (rented is not null)
if (bytes[index] == 0)
{
ArrayPool<byte>.Shared.Return(rented);
value = Encoding.UTF8.GetString(bytes, 0, index);
return true;
}
}
value = Encoding.UTF8.GetString(bytes);
return true;
}
public bool PushUInt64(ulong value)
-13
View File
@@ -1,13 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE;
[Flags]
public enum GuestPageProtection
{
None = 0,
Read = 1,
Write = 2,
Execute = 4,
}
+25 -25
View File
@@ -23,20 +23,6 @@ public readonly record struct GuestThreadSnapshot(
ulong LastReturnRip,
string? BlockReason);
/// <summary>
/// Continuation state for a blocked guest thread, replacing the closure pair a blocking
/// wait used to allocate. TryWake runs under the scheduler's guest-thread gate and
/// returns true when the waiter has a final result and the thread should be re-readied;
/// false leaves it parked. Resume runs later on the woken thread outside that gate, and
/// its return value becomes the guest's RAX for the resumed call.
/// </summary>
public interface IGuestThreadBlockWaiter
{
int Resume();
bool TryWake();
}
public interface IGuestThreadScheduler
{
bool SupportsGuestContextTransfer { get; }
@@ -120,7 +106,10 @@ public static class GuestThreadExecution
private static string? _pendingBlockWakeKey;
[ThreadStatic]
private static IGuestThreadBlockWaiter? _pendingBlockWaiter;
private static Func<int>? _pendingBlockResumeHandler;
[ThreadStatic]
private static Func<bool>? _pendingBlockWakeHandler;
[ThreadStatic]
private static long _pendingBlockDeadlineTimestamp;
@@ -168,7 +157,8 @@ public static class GuestThreadExecution
_pendingBlockContinuationValid = false;
_pendingBlockContinuation = default;
_pendingBlockWakeKey = null;
_pendingBlockWaiter = null;
_pendingBlockResumeHandler = null;
_pendingBlockWakeHandler = null;
_pendingBlockDeadlineTimestamp = 0;
_pendingEntryExit = false;
_pendingEntryExitValue = 0;
@@ -189,7 +179,8 @@ public static class GuestThreadExecution
_pendingBlockContinuationValid = false;
_pendingBlockContinuation = default;
_pendingBlockWakeKey = null;
_pendingBlockWaiter = null;
_pendingBlockResumeHandler = null;
_pendingBlockWakeHandler = null;
_pendingBlockDeadlineTimestamp = 0;
_pendingEntryExit = false;
_pendingEntryExitValue = 0;
@@ -220,7 +211,8 @@ public static class GuestThreadExecution
CpuContext? context,
string reason,
string? wakeKey = null,
IGuestThreadBlockWaiter? waiter = null,
Func<int>? resumeHandler = null,
Func<bool>? wakeHandler = null,
long blockDeadlineTimestamp = 0)
{
if (!IsGuestThread)
@@ -230,7 +222,8 @@ public static class GuestThreadExecution
_pendingBlockReason = string.IsNullOrWhiteSpace(reason) ? "guest_thread_blocked" : reason;
_pendingBlockWakeKey = string.IsNullOrWhiteSpace(wakeKey) ? _pendingBlockReason : wakeKey;
_pendingBlockWaiter = waiter;
_pendingBlockResumeHandler = resumeHandler;
_pendingBlockWakeHandler = wakeHandler;
_pendingBlockDeadlineTimestamp = blockDeadlineTimestamp;
if (context is not null && TryCaptureCurrentBlockContinuation(context, out var continuation))
{
@@ -262,6 +255,7 @@ public static class GuestThreadExecution
out hasContinuation,
out _,
out _,
out _,
out _);
}
@@ -270,14 +264,16 @@ public static class GuestThreadExecution
out GuestCpuContinuation continuation,
out bool hasContinuation,
out string wakeKey,
out IGuestThreadBlockWaiter? waiter)
out Func<int>? resumeHandler,
out Func<bool>? wakeHandler)
{
return TryConsumeCurrentThreadBlock(
out reason,
out continuation,
out hasContinuation,
out wakeKey,
out waiter,
out resumeHandler,
out wakeHandler,
out _);
}
@@ -286,7 +282,8 @@ public static class GuestThreadExecution
out GuestCpuContinuation continuation,
out bool hasContinuation,
out string wakeKey,
out IGuestThreadBlockWaiter? waiter,
out Func<int>? resumeHandler,
out Func<bool>? wakeHandler,
out long blockDeadlineTimestamp)
{
reason = _pendingBlockReason ?? string.Empty;
@@ -295,7 +292,8 @@ public static class GuestThreadExecution
continuation = default;
hasContinuation = false;
wakeKey = string.Empty;
waiter = null;
resumeHandler = null;
wakeHandler = null;
blockDeadlineTimestamp = 0;
return false;
}
@@ -303,13 +301,15 @@ public static class GuestThreadExecution
continuation = _pendingBlockContinuation;
hasContinuation = _pendingBlockContinuationValid;
wakeKey = _pendingBlockWakeKey ?? reason;
waiter = _pendingBlockWaiter;
resumeHandler = _pendingBlockResumeHandler;
wakeHandler = _pendingBlockWakeHandler;
blockDeadlineTimestamp = _pendingBlockDeadlineTimestamp;
_pendingBlockReason = null;
_pendingBlockContinuation = default;
_pendingBlockContinuationValid = false;
_pendingBlockWakeKey = null;
_pendingBlockWaiter = null;
_pendingBlockResumeHandler = null;
_pendingBlockWakeHandler = null;
_pendingBlockDeadlineTimestamp = 0;
return true;
}
@@ -1,18 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// General-purpose register snapshot of a suspended thread, produced by
/// <see cref="IHostThreading.TryCaptureThreadRegisters"/>. Registers are named
/// after the guest ISA (x86-64), which every supported host executes natively.
/// </summary>
public readonly record struct HostCapturedRegisters(
ulong Rip,
ulong Rsp,
ulong Rbp,
ulong Rax,
ulong Rbx,
ulong Rcx,
ulong Rdx);
-46
View File
@@ -1,46 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// Host-neutral gamepad button flags. Named after the PlayStation layout the guest API
/// exposes, but the numeric values are the seam's own — the HLE pad exports translate
/// them to SCE_PAD_BUTTON bits, so guest ABI values never leak into host backends.
/// </summary>
[Flags]
public enum HostGamepadButtons : uint
{
None = 0,
Up = 1 << 0,
Down = 1 << 1,
Left = 1 << 2,
Right = 1 << 3,
Cross = 1 << 4,
Circle = 1 << 5,
Square = 1 << 6,
Triangle = 1 << 7,
L1 = 1 << 8,
R1 = 1 << 9,
L2 = 1 << 10,
R2 = 1 << 11,
L3 = 1 << 12,
R3 = 1 << 13,
Options = 1 << 14,
TouchPad = 1 << 15,
}
/// <summary>
/// Snapshot of one host gamepad: sticks are 0..255 with 128 centered and Y growing
/// downward; triggers 0..255. Unmanaged on purpose so per-frame polls can stackalloc
/// snapshot buffers.
/// </summary>
public readonly record struct HostGamepadState(
bool Connected,
HostGamepadButtons Buttons,
byte LeftX,
byte LeftY,
byte RightX,
byte RightY,
byte LeftTrigger,
byte RightTrigger);
@@ -1,20 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// Platform-neutral page protection. Values intentionally enumerate the exact
/// combinations the emulator uses today so each maps 1:1 onto a single native
/// protection constant (PAGE_* on Windows, PROT_* elsewhere).
/// </summary>
public enum HostPageProtection
{
NoAccess,
ReadOnly,
ReadWrite,
Execute,
ReadExecute,
ReadWriteExecute,
ExecuteWriteCopy,
}
-34
View File
@@ -1,34 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
using SharpEmu.HLE.Host.Windows;
namespace SharpEmu.HLE.Host;
/// <summary>
/// Process-wide access point for the host platform backend. Static HLE export
/// classes (which cannot receive constructor injection) resolve host primitives
/// through <see cref="Current"/>; injectable components should instead accept an
/// <see cref="IHostPlatform"/> and merely default to this.
/// </summary>
public static class HostPlatform
{
private static readonly Lazy<IHostPlatform> Instance = new(Create);
public static IHostPlatform Current => Instance.Value;
private static IHostPlatform Create()
{
// The Windows backend executes guest x86-64 natively and emits x86-64
// stubs, so a native ARM64 process must be rejected here rather than
// crash undefined later (x64 processes under emulation report X64).
if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.X64)
{
return new WindowsHostPlatform();
}
throw new PlatformNotSupportedException(
"SharpEmu native guest execution requires a host platform backend and none exists for this OS/architecture yet (currently Windows x64 only).");
}
}
-20
View File
@@ -1,20 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// Result of <see cref="IHostMemory.Query"/>. The Raw* fields carry the
/// untranslated OS values so call sites migrated from direct VirtualQuery use
/// keep comparing (and logging) the exact native words they did before;
/// <see cref="State"/> and <see cref="Protection"/> are neutral views.
/// </summary>
public readonly record struct HostRegionInfo(
ulong BaseAddress,
ulong AllocationBase,
ulong RegionSize,
HostRegionState State,
uint RawState,
HostPageProtection Protection,
uint RawProtection,
uint RawAllocationProtection);
-11
View File
@@ -1,11 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
public enum HostRegionState
{
Free,
Reserved,
Committed,
}
@@ -1,21 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// Host functions whose addresses the execution engine bakes into emitted
/// stubs (spin-waits, worker run loops, TLS reads). Enum-keyed rather than a
/// free-form name lookup: each platform's emitters need their own specific
/// functions, and this set is exactly what the current emitters consume.
/// </summary>
public enum HostRuntimeFunction
{
TlsGetValue,
QueryPerformanceCounter,
SwitchToThread,
Sleep,
WaitForSingleObject,
SetEvent,
ExitThread,
}
-23
View File
@@ -1,23 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// Host audio-output device access. The HLE audio exports convert guest submissions to
/// interleaved stereo 16-bit PCM (the format every backend accepts) and feed them through
/// streams opened here; everything device-specific — queueing, backpressure, native
/// buffer lifetime — lives behind <see cref="IHostAudioStream"/>.
/// </summary>
public interface IHostAudioOutput
{
/// <summary>Backend identifier for diagnostics (e.g. "winmm").</summary>
string BackendName { get; }
/// <summary>
/// Opens an interleaved stereo 16-bit PCM output stream at the given sample rate.
/// Throws when the host has no usable output device; callers degrade to a silent
/// port and pace the guest instead.
/// </summary>
IHostAudioStream OpenStereoPcm16Stream(uint sampleRate);
}
-18
View File
@@ -1,18 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// One open host audio output stream. Submissions are interleaved stereo 16-bit PCM at
/// the sample rate the stream was opened with.
/// </summary>
public interface IHostAudioStream : IDisposable
{
/// <summary>
/// Submits one buffer. May block briefly while the device drains its queue (this is
/// what paces the guest's audio loop); returns false when the stream cannot accept
/// audio, in which case the caller paces the guest itself.
/// </summary>
bool Submit(ReadOnlySpan<byte> stereoPcm16);
}
@@ -1,32 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// Installation mechanics for the process-wide fault interception the execution
/// engine relies on to catch guest faults. Deliberately thin: the managed
/// handlers keep receiving the platform's raw exception data, and the emitted
/// pre-filter thunk is an opaque per-platform unit. Implementations live next
/// to the execution backend (SharpEmu.Core), not behind HostPlatform.Current.
/// </summary>
public interface IHostFaultHandling
{
/// <summary>
/// Emits the native thunk that wraps a managed fault handler: it pre-filters
/// exception codes that must never enter managed code and, when the fault
/// happened on a guest stack, switches to the host stack saved in
/// <paramref name="hostRspSwitchTlsSlot"/> before the call. Returns 0 on failure.
/// </summary>
nint CreateHandlerThunk(nint managedCallback, uint hostRspSwitchTlsSlot, nint tlsGetValueAddress);
void FreeThunk(nint thunk);
/// <summary>Installs a first-chance handler ahead of existing ones; returns a removal handle (0 on failure).</summary>
nint AddFirstChanceHandler(nint thunk);
void RemoveHandler(nint handle);
/// <summary>Installs the last-resort filter; pass 0 to clear.</summary>
void SetUnhandledFilter(nint thunk);
}
-44
View File
@@ -1,44 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// Host input devices: gamepad state snapshots, force-feedback/lightbar sinks, and the
/// keyboard-fallback queries. Which physical readers exist (DualSense over raw HID,
/// XInput, evdev, ...) is a backend detail; merge policy between devices and the
/// keyboard lives in the HLE pad exports.
/// </summary>
public interface IHostInput
{
/// <summary>Starts the background device readers once; safe to call repeatedly.</summary>
void EnsureStarted();
/// <summary>
/// Fills <paramref name="destination"/> with snapshots of currently connected
/// gamepads and returns how many were written (0 when none are connected).
/// </summary>
int GetGamepadStates(Span<HostGamepadState> destination);
/// <summary>Human-readable name of the first connected gamepad, or null.</summary>
string? DescribeConnectedGamepad();
/// <summary>Sets rumble on all connected gamepads; large = strong/left motor.</summary>
void SetRumble(byte largeMotor, byte smallMotor);
/// <summary>
/// Approximates per-trigger vibration on gamepads without independent trigger
/// actuators; null leaves that trigger's current value unchanged.
/// </summary>
void SetTriggerRumble(byte? leftTrigger, byte? rightTrigger);
void SetLightbar(byte red, byte green, byte blue);
void ResetLightbar();
/// <summary>True when a window of this process has keyboard focus.</summary>
bool IsHostWindowFocused();
/// <summary>Windows virtual-key code semantics; other backends translate.</summary>
bool IsKeyDown(int virtualKey);
}
-48
View File
@@ -1,48 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// Host page-allocation primitives used by the native execution engine.
/// Allocate/Reserve/Commit are deliberately separate members (rather than a
/// flags parameter) so every call site maps 1:1 onto the exact native call it
/// replaced, keeping the Windows behavior byte-for-byte identical.
/// </summary>
public interface IHostMemory
{
/// <summary>
/// Reserves and commits pages in one step. <paramref name="desiredAddress"/> of 0
/// lets the OS choose the address. Returns the base address, or 0 on failure.
/// The OS may satisfy the request at a different address than desired; callers
/// that require an exact placement must check the result themselves.
/// </summary>
ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection);
/// <summary>Reserves address space without committing pages (lazy regions).</summary>
ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection);
/// <summary>Commits pages inside a previously reserved range (fault-path lazy commit).</summary>
bool Commit(ulong address, ulong size, HostPageProtection protection);
/// <summary>Releases an entire allocation or reservation by its base address.</summary>
bool Free(ulong address);
/// <summary>
/// Changes protection on committed pages. <paramref name="rawOldProtection"/> is the
/// untranslated previous OS protection value (see <see cref="HostRegionInfo.RawProtection"/>).
/// </summary>
bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection);
/// <summary>
/// Restores a raw protection value previously returned by <see cref="Protect"/> or
/// <see cref="Query"/> on this same platform. Raw values are opaque to callers and
/// must never cross platforms; this exists so save/restore protection sequences
/// round-trip OS-specific modifier bits the neutral enum cannot represent.
/// </summary>
bool ProtectRaw(ulong address, ulong size, uint rawProtection, out uint rawOldProtection);
bool Query(ulong address, out HostRegionInfo info);
void FlushInstructionCache(ulong address, ulong size);
}
-23
View File
@@ -1,23 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// Aggregates the host-OS primitives the native execution engine depends on.
/// Each supported platform provides one implementation; consumers reach the
/// process-wide instance through <see cref="HostPlatform.Current"/> or accept
/// one by injection.
/// </summary>
public interface IHostPlatform
{
IHostMemory Memory { get; }
IHostThreading Threading { get; }
IHostSymbolResolver Symbols { get; }
IHostAudioOutput Audio { get; }
IHostInput Input { get; }
}
@@ -1,10 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
public interface IHostSymbolResolver
{
/// <summary>Returns the native address of the function, or 0 if unavailable.</summary>
nint GetAddress(HostRuntimeFunction function);
}
-51
View File
@@ -1,51 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// Raw host thread and native-TLS primitives for the execution engine. Guest
/// code must run on threads the CLR did not create (no managed frames below
/// guest frames), so thread creation takes a native entry point and is not
/// expressible with managed threads.
/// </summary>
public interface IHostThreading
{
/// <summary>Allocates a native TLS slot; returns <see cref="uint.MaxValue"/> on failure.</summary>
uint AllocateTlsSlot();
bool FreeTlsSlot(uint slot);
bool SetTlsValue(uint slot, nint value);
nint GetTlsValue(uint slot);
uint CurrentThreadId { get; }
bool TrySetCurrentThreadAffinity(nuint affinityMask);
/// <summary>
/// Asks the OS for ~1 ms timed-wait granularity for the life of the process
/// (idempotent; best-effort). No-op on platforms whose default is already fine.
/// </summary>
void RequestTimerResolution();
/// <summary>
/// Creates a raw OS thread executing native code at <paramref name="entry"/> with
/// <paramref name="stackReserveBytes"/> of reserved (not committed) stack.
/// Returns the thread handle, or 0 on failure.
/// </summary>
nint CreateNativeThread(nint entry, nint parameter, nuint stackReserveBytes, out uint threadId);
/// <summary>Waits for the thread to exit; true when it did within the timeout.</summary>
bool WaitForThreadExit(nint threadHandle, uint timeoutMilliseconds);
void CloseThreadHandle(nint threadHandle);
/// <summary>
/// Suspends the thread, snapshots its general-purpose registers, and resumes it —
/// one indivisible operation (diagnostics only). The caller must not pass the
/// current thread. Returns false if the thread cannot be opened or suspended.
/// </summary>
bool TryCaptureThreadRegisters(uint threadId, out HostCapturedRegisters registers);
}
@@ -1,84 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
namespace SharpEmu.HLE.Host.Windows;
/// <summary>
/// Windows input backend: DualSense over raw HID plus XInput controllers for gamepads,
/// user32 for the keyboard-fallback queries. Rumble fans out to every reader; lightbar
/// only exists on the DualSense.
/// </summary>
internal sealed partial class WindowsHostInput : IHostInput
{
public void EnsureStarted()
{
WindowsDualSenseReader.EnsureStarted();
WindowsXInputReader.EnsureStarted();
}
public int GetGamepadStates(Span<HostGamepadState> destination)
{
var count = 0;
if (count < destination.Length && WindowsDualSenseReader.TryGetState(out var dualSense))
{
destination[count++] = dualSense;
}
if (count < destination.Length && WindowsXInputReader.TryGetState(out var xinput))
{
destination[count++] = xinput;
}
return count;
}
public string? DescribeConnectedGamepad()
{
if (WindowsDualSenseReader.TryGetState(out _))
{
return "DualSense";
}
return WindowsXInputReader.TryGetState(out _) ? "Xbox controller" : null;
}
public void SetRumble(byte largeMotor, byte smallMotor)
{
WindowsDualSenseReader.SetRumble(largeMotor, smallMotor);
WindowsXInputReader.SetRumble(largeMotor, smallMotor);
}
public void SetTriggerRumble(byte? leftTrigger, byte? rightTrigger) =>
WindowsXInputReader.SetTriggerRumble(leftTrigger, rightTrigger);
public void SetLightbar(byte red, byte green, byte blue) =>
WindowsDualSenseReader.SetLightbar(red, green, blue);
public void ResetLightbar() => WindowsDualSenseReader.ResetLightbar();
public bool IsHostWindowFocused()
{
var foregroundWindow = GetForegroundWindow();
if (foregroundWindow == 0)
{
return false;
}
GetWindowThreadProcessId(foregroundWindow, out var processId);
return processId == (uint)Environment.ProcessId;
}
public bool IsKeyDown(int virtualKey) =>
(GetAsyncKeyState(virtualKey) & 0x8000) != 0;
[LibraryImport("user32.dll")]
private static partial short GetAsyncKeyState(int vKey);
[LibraryImport("user32.dll")]
private static partial nint GetForegroundWindow();
[LibraryImport("user32.dll")]
private static partial uint GetWindowThreadProcessId(nint hWnd, out uint processId);
}
@@ -1,153 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
namespace SharpEmu.HLE.Host.Windows;
/// <summary>
/// Windows implementation over VirtualAlloc/VirtualFree/VirtualProtect/VirtualQuery.
/// Sealed so the JIT can devirtualize interface calls on fault-handling hot paths.
/// </summary>
internal sealed unsafe partial class WindowsHostMemory : IHostMemory
{
private const uint MEM_COMMIT = 0x1000;
private const uint MEM_RESERVE = 0x2000;
private const uint MEM_RELEASE = 0x8000;
private const uint MEM_FREE = 0x10000;
private const uint PAGE_NOACCESS = 0x01;
private const uint PAGE_READONLY = 0x02;
private const uint PAGE_READWRITE = 0x04;
private const uint PAGE_WRITECOPY = 0x08;
private const uint PAGE_EXECUTE = 0x10;
private const uint PAGE_EXECUTE_READ = 0x20;
private const uint PAGE_EXECUTE_READWRITE = 0x40;
private const uint PAGE_EXECUTE_WRITECOPY = 0x80;
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection)
{
return (ulong)VirtualAlloc((void*)desiredAddress, (nuint)size, MEM_COMMIT | MEM_RESERVE, ToNativeProtection(protection));
}
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection)
{
return (ulong)VirtualAlloc((void*)desiredAddress, (nuint)size, MEM_RESERVE, ToNativeProtection(protection));
}
public bool Commit(ulong address, ulong size, HostPageProtection protection)
{
return VirtualAlloc((void*)address, (nuint)size, MEM_COMMIT, ToNativeProtection(protection)) != null;
}
public bool Free(ulong address)
{
return VirtualFree((void*)address, 0, MEM_RELEASE);
}
public bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection)
{
return VirtualProtect((void*)address, (nuint)size, ToNativeProtection(protection), out rawOldProtection);
}
public bool ProtectRaw(ulong address, ulong size, uint rawProtection, out uint rawOldProtection)
{
return VirtualProtect((void*)address, (nuint)size, rawProtection, out rawOldProtection);
}
public bool Query(ulong address, out HostRegionInfo info)
{
if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MemoryBasicInformation64)) == 0)
{
info = default;
return false;
}
info = new HostRegionInfo(
mbi.BaseAddress,
mbi.AllocationBase,
mbi.RegionSize,
ToRegionState(mbi.State),
mbi.State,
ToHostProtection(mbi.Protect),
mbi.Protect,
mbi.AllocationProtect);
return true;
}
public void FlushInstructionCache(ulong address, ulong size)
{
FlushInstructionCache(GetCurrentProcess(), (void*)address, (nuint)size);
}
private static uint ToNativeProtection(HostPageProtection protection) => protection switch
{
HostPageProtection.NoAccess => PAGE_NOACCESS,
HostPageProtection.ReadOnly => PAGE_READONLY,
HostPageProtection.ReadWrite => PAGE_READWRITE,
HostPageProtection.Execute => PAGE_EXECUTE,
HostPageProtection.ReadExecute => PAGE_EXECUTE_READ,
HostPageProtection.ReadWriteExecute => PAGE_EXECUTE_READWRITE,
HostPageProtection.ExecuteWriteCopy => PAGE_EXECUTE_WRITECOPY,
_ => throw new ArgumentOutOfRangeException(nameof(protection), protection, null),
};
private static HostRegionState ToRegionState(uint state) => state switch
{
MEM_COMMIT => HostRegionState.Committed,
MEM_RESERVE => HostRegionState.Reserved,
MEM_FREE => HostRegionState.Free,
_ => HostRegionState.Free,
};
private static HostPageProtection ToHostProtection(uint rawProtection)
{
// Strip PAGE_GUARD/PAGE_NOCACHE/PAGE_WRITECOMBINE modifiers; callers needing
// them compare HostRegionInfo.RawProtection directly.
return (rawProtection & 0xFF) switch
{
PAGE_READONLY => HostPageProtection.ReadOnly,
PAGE_READWRITE => HostPageProtection.ReadWrite,
PAGE_WRITECOPY => HostPageProtection.ReadWrite,
PAGE_EXECUTE => HostPageProtection.Execute,
PAGE_EXECUTE_READ => HostPageProtection.ReadExecute,
PAGE_EXECUTE_READWRITE => HostPageProtection.ReadWriteExecute,
PAGE_EXECUTE_WRITECOPY => HostPageProtection.ExecuteWriteCopy,
_ => HostPageProtection.NoAccess,
};
}
[LibraryImport("kernel32.dll", SetLastError = true)]
private static partial void* VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool VirtualFree(void* lpAddress, nuint dwSize, uint dwFreeType);
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool VirtualProtect(void* lpAddress, nuint dwSize, uint flNewProtect, out uint lpflOldProtect);
[LibraryImport("kernel32.dll")]
private static partial nuint VirtualQuery(void* lpAddress, out MemoryBasicInformation64 lpBuffer, nuint dwLength);
[LibraryImport("kernel32.dll")]
private static partial void* GetCurrentProcess();
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
private struct MemoryBasicInformation64
{
public ulong BaseAddress;
public ulong AllocationBase;
public uint AllocationProtect;
public uint Alignment1;
public ulong RegionSize;
public uint State;
public uint Protect;
public uint Type;
public uint Alignment2;
}
}
@@ -1,17 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host.Windows;
internal sealed class WindowsHostPlatform : IHostPlatform
{
public IHostMemory Memory { get; } = new WindowsHostMemory();
public IHostThreading Threading { get; } = new WindowsHostThreading();
public IHostSymbolResolver Symbols { get; } = new WindowsHostSymbolResolver();
public IHostAudioOutput Audio { get; } = new WindowsWaveOutAudio();
public IHostInput Input { get; } = new WindowsHostInput();
}
@@ -1,39 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
namespace SharpEmu.HLE.Host.Windows;
internal sealed partial class WindowsHostSymbolResolver : IHostSymbolResolver
{
public nint GetAddress(HostRuntimeFunction function)
{
var kernel32 = GetModuleHandle("kernel32.dll");
if (kernel32 == 0)
{
return 0;
}
return GetProcAddress(kernel32, function switch
{
HostRuntimeFunction.TlsGetValue => "TlsGetValue",
HostRuntimeFunction.QueryPerformanceCounter => "QueryPerformanceCounter",
HostRuntimeFunction.SwitchToThread => "SwitchToThread",
HostRuntimeFunction.Sleep => "Sleep",
HostRuntimeFunction.WaitForSingleObject => "WaitForSingleObject",
HostRuntimeFunction.SetEvent => "SetEvent",
HostRuntimeFunction.ExitThread => "ExitThread",
_ => throw new ArgumentOutOfRangeException(nameof(function), function, null),
});
}
// Utf16 marshalling pins the managed string and passes its address directly
// (no copy); Utf8 stack-allocates the transient buffer for these short
// ASCII export names. LibraryImport is exact-spelling, hence the W entry point.
[LibraryImport("kernel32.dll", EntryPoint = "GetModuleHandleW", StringMarshalling = StringMarshalling.Utf16)]
private static partial nint GetModuleHandle(string lpModuleName);
[LibraryImport("kernel32.dll", StringMarshalling = StringMarshalling.Utf8)]
private static partial nint GetProcAddress(nint hModule, string procName);
}
@@ -1,193 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
namespace SharpEmu.HLE.Host.Windows;
internal sealed unsafe partial class WindowsHostThreading : IHostThreading
{
private const uint StackSizeParamIsAReservation = 0x00010000u;
private const uint ThreadGetContext = 0x0008u;
private const uint ThreadSuspendResume = 0x0002u;
// Win64 CONTEXT layout (CONTROL | INTEGER only — no XMM state is requested).
private const int Win64ContextSize = 0x4D0;
private const int Win64ContextFlagsOffset = 0x30;
private const uint ContextAmd64ControlInteger = 0x00100003u;
private const int CtxRax = 120;
private const int CtxRcx = 128;
private const int CtxRdx = 136;
private const int CtxRbx = 144;
private const int CtxRsp = 152;
private const int CtxRbp = 160;
private const int CtxRip = 248;
private static int _timerResolutionRequested;
public void RequestTimerResolution()
{
if (Interlocked.Exchange(ref _timerResolutionRequested, 1) != 0)
{
return;
}
try
{
if (TimeBeginPeriod(1) != 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}");
}
}
public uint AllocateTlsSlot() => TlsAlloc();
public bool FreeTlsSlot(uint slot) => TlsFree(slot);
public bool SetTlsValue(uint slot, nint value) => TlsSetValue(slot, value);
public nint GetTlsValue(uint slot) => TlsGetValue(slot);
public uint CurrentThreadId => GetCurrentThreadId();
public bool TrySetCurrentThreadAffinity(nuint affinityMask)
{
return SetThreadAffinityMask(GetCurrentThread(), affinityMask) != 0;
}
public nint CreateNativeThread(nint entry, nint parameter, nuint stackReserveBytes, out uint threadId)
{
return CreateThread(0, stackReserveBytes, entry, parameter, StackSizeParamIsAReservation, out threadId);
}
public bool WaitForThreadExit(nint threadHandle, uint timeoutMilliseconds)
{
return WaitForSingleObject(threadHandle, timeoutMilliseconds) == 0u;
}
public void CloseThreadHandle(nint threadHandle)
{
_ = CloseHandle(threadHandle);
}
public bool TryCaptureThreadRegisters(uint threadId, out HostCapturedRegisters registers)
{
registers = default;
var threadHandle = OpenThread(ThreadGetContext | ThreadSuspendResume, false, threadId);
if (threadHandle == 0)
{
return false;
}
void* contextRecord = null;
var suspended = false;
try
{
if (SuspendThread(threadHandle) == uint.MaxValue)
{
return false;
}
suspended = true;
// CONTEXT requires 16-byte alignment (it embeds M128A fields);
// NativeMemory.AllocZeroed guarantees max_align_t, stackalloc only
// pointer-size — so this stays a native allocation.
contextRecord = NativeMemory.AllocZeroed((nuint)Win64ContextSize);
*(uint*)((byte*)contextRecord + Win64ContextFlagsOffset) = ContextAmd64ControlInteger;
if (!GetThreadContext(threadHandle, contextRecord))
{
return false;
}
registers = new HostCapturedRegisters(
ReadU64(contextRecord, CtxRip),
ReadU64(contextRecord, CtxRsp),
ReadU64(contextRecord, CtxRbp),
ReadU64(contextRecord, CtxRax),
ReadU64(contextRecord, CtxRbx),
ReadU64(contextRecord, CtxRcx),
ReadU64(contextRecord, CtxRdx));
return true;
}
finally
{
if (contextRecord != null)
{
NativeMemory.Free(contextRecord);
}
if (suspended)
{
_ = ResumeThread(threadHandle);
}
_ = CloseHandle(threadHandle);
}
}
private static ulong ReadU64(void* contextRecord, int offset)
{
return *(ulong*)((byte*)contextRecord + offset);
}
[LibraryImport("kernel32.dll")]
private static partial uint TlsAlloc();
[LibraryImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool TlsFree(uint dwTlsIndex);
[LibraryImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool TlsSetValue(uint dwTlsIndex, nint lpTlsValue);
[LibraryImport("kernel32.dll")]
private static partial nint TlsGetValue(uint dwTlsIndex);
[LibraryImport("kernel32.dll")]
private static partial uint GetCurrentThreadId();
[LibraryImport("kernel32.dll")]
private static partial nint GetCurrentThread();
[LibraryImport("kernel32.dll", SetLastError = true)]
private static partial nuint SetThreadAffinityMask(nint hThread, nuint dwThreadAffinityMask);
[LibraryImport("kernel32.dll", SetLastError = true)]
private static partial nint CreateThread(
nint lpThreadAttributes,
nuint dwStackSize,
nint lpStartAddress,
nint lpParameter,
uint dwCreationFlags,
out uint lpThreadId);
[LibraryImport("kernel32.dll", SetLastError = true)]
private static partial uint WaitForSingleObject(nint hHandle, uint dwMilliseconds);
[LibraryImport("kernel32.dll", SetLastError = true)]
private static partial nint OpenThread(uint dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, uint dwThreadId);
[LibraryImport("kernel32.dll", SetLastError = true)]
private static partial uint SuspendThread(nint hThread);
[LibraryImport("kernel32.dll", SetLastError = true)]
private static partial uint ResumeThread(nint hThread);
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool GetThreadContext(nint hThread, void* lpContext);
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool CloseHandle(nint hObject);
[LibraryImport("winmm.dll", EntryPoint = "timeBeginPeriod")]
private static partial uint TimeBeginPeriod(uint uPeriod);
}
@@ -1,243 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
namespace SharpEmu.HLE.Host.Windows;
internal sealed partial class WindowsWaveOutAudio : IHostAudioOutput
{
public string BackendName => "winmm";
public IHostAudioStream OpenStereoPcm16Stream(uint sampleRate) => new WaveOutStream(sampleRate);
private sealed partial class WaveOutStream : IHostAudioStream
{
private const uint WaveMapper = uint.MaxValue;
private const uint CallbackEvent = 0x0005_0000;
private const ushort WaveFormatPcm = 1;
private const uint WaveHeaderDone = 0x0000_0001;
private const int MaximumQueuedPcmBytes = 32 * 1024;
private readonly object _gate = new();
private readonly AutoResetEvent _completion = new(false);
private readonly Queue<NativeBuffer> _buffers = new();
private IntPtr _device;
private int _queuedPcmBytes;
private bool _disposed;
public WaveOutStream(uint sampleRate)
{
var format = new WaveFormat
{
FormatTag = WaveFormatPcm,
Channels = 2,
SamplesPerSecond = sampleRate,
AverageBytesPerSecond = checked(sampleRate * 4),
BlockAlign = 4,
BitsPerSample = 16,
ExtraSize = 0,
};
var result = WaveOutOpen(
out _device,
WaveMapper,
ref format,
_completion.SafeWaitHandle.DangerousGetHandle(),
IntPtr.Zero,
CallbackEvent);
if (result != 0)
{
throw new InvalidOperationException($"waveOutOpen failed with MMRESULT {result}.");
}
}
public bool Submit(ReadOnlySpan<byte> stereoPcm16)
{
lock (_gate)
{
if (_disposed)
{
return false;
}
ReapCompletedBuffers();
while (_queuedPcmBytes != 0 &&
_queuedPcmBytes + stereoPcm16.Length > MaximumQueuedPcmBytes)
{
if (!_completion.WaitOne(TimeSpan.FromSeconds(1)))
{
return false;
}
ReapCompletedBuffers();
}
return QueueBuffer(stereoPcm16);
}
}
public void Dispose()
{
lock (_gate)
{
if (_disposed)
{
return;
}
_disposed = true;
if (_device != IntPtr.Zero)
{
WaveOutReset(_device);
while (_buffers.TryDequeue(out var buffer))
{
ReleaseBuffer(buffer);
}
WaveOutClose(_device);
_device = IntPtr.Zero;
}
_completion.Dispose();
}
}
private bool QueueBuffer(ReadOnlySpan<byte> data)
{
var dataAddress = Marshal.AllocHGlobal(data.Length);
var headerAddress = IntPtr.Zero;
try
{
unsafe
{
data.CopyTo(new Span<byte>((void*)dataAddress, data.Length));
}
var header = new WaveHeader
{
Data = dataAddress,
BufferLength = checked((uint)data.Length),
};
headerAddress = Marshal.AllocHGlobal(Marshal.SizeOf<WaveHeader>());
Marshal.StructureToPtr(header, headerAddress, false);
var result = WaveOutPrepareHeader(
_device,
headerAddress,
checked((uint)Marshal.SizeOf<WaveHeader>()));
if (result != 0)
{
return false;
}
result = WaveOutWrite(
_device,
headerAddress,
checked((uint)Marshal.SizeOf<WaveHeader>()));
if (result != 0)
{
WaveOutUnprepareHeader(
_device,
headerAddress,
checked((uint)Marshal.SizeOf<WaveHeader>()));
return false;
}
_buffers.Enqueue(new NativeBuffer(dataAddress, headerAddress, data.Length));
_queuedPcmBytes += data.Length;
dataAddress = IntPtr.Zero;
headerAddress = IntPtr.Zero;
return true;
}
finally
{
if (headerAddress != IntPtr.Zero)
{
Marshal.FreeHGlobal(headerAddress);
}
if (dataAddress != IntPtr.Zero)
{
Marshal.FreeHGlobal(dataAddress);
}
}
}
private void ReapCompletedBuffers()
{
while (_buffers.TryPeek(out var buffer))
{
var header = Marshal.PtrToStructure<WaveHeader>(buffer.Header);
if ((header.Flags & WaveHeaderDone) == 0)
{
return;
}
_buffers.Dequeue();
ReleaseBuffer(buffer);
}
}
private void ReleaseBuffer(NativeBuffer buffer)
{
WaveOutUnprepareHeader(
_device,
buffer.Header,
checked((uint)Marshal.SizeOf<WaveHeader>()));
_queuedPcmBytes -= buffer.Length;
Marshal.FreeHGlobal(buffer.Header);
Marshal.FreeHGlobal(buffer.Data);
}
private readonly record struct NativeBuffer(IntPtr Data, IntPtr Header, int Length);
[StructLayout(LayoutKind.Sequential, Pack = 2)]
private struct WaveFormat
{
public ushort FormatTag;
public ushort Channels;
public uint SamplesPerSecond;
public uint AverageBytesPerSecond;
public ushort BlockAlign;
public ushort BitsPerSample;
public ushort ExtraSize;
}
[StructLayout(LayoutKind.Sequential)]
private struct WaveHeader
{
public IntPtr Data;
public uint BufferLength;
public uint BytesRecorded;
public nuint User;
public uint Flags;
public uint Loops;
public IntPtr Next;
public nuint Reserved;
}
[LibraryImport("winmm.dll", EntryPoint = "waveOutOpen")]
private static partial uint WaveOutOpen(
out IntPtr device,
uint deviceId,
ref WaveFormat format,
IntPtr callback,
IntPtr instance,
uint flags);
[LibraryImport("winmm.dll", EntryPoint = "waveOutPrepareHeader")]
private static partial uint WaveOutPrepareHeader(IntPtr device, IntPtr header, uint headerSize);
[LibraryImport("winmm.dll", EntryPoint = "waveOutWrite")]
private static partial uint WaveOutWrite(IntPtr device, IntPtr header, uint headerSize);
[LibraryImport("winmm.dll", EntryPoint = "waveOutUnprepareHeader")]
private static partial uint WaveOutUnprepareHeader(IntPtr device, IntPtr header, uint headerSize);
[LibraryImport("winmm.dll", EntryPoint = "waveOutReset")]
private static partial uint WaveOutReset(IntPtr device);
[LibraryImport("winmm.dll", EntryPoint = "waveOutClose")]
private static partial uint WaveOutClose(IntPtr device);
}
}
-31
View File
@@ -1,31 +0,0 @@
// 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}");
}
}
}
-2
View File
@@ -8,6 +8,4 @@ public interface ICpuMemory
bool TryRead(ulong virtualAddress, Span<byte> destination);
bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source);
bool TryCompare(ulong virtualAddress, ReadOnlySpan<byte> expected) => false;
}
-14
View File
@@ -1,14 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE;
/// <summary>
/// Implemented by memories that decorate another <see cref="ICpuMemory"/>
/// (e.g. access trackers) so capability lookups can unwrap to the real
/// implementation without reflection.
/// </summary>
public interface ICpuMemoryWrapper
{
ICpuMemory Inner { get; }
}
-21
View File
@@ -1,21 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE;
/// <summary>
/// Guest address-space manipulation beyond plain allocation: fixed-address
/// mapping and page-protection changes. Guest addresses are identity-mapped
/// onto host pages by the implementing memory, so HLE exports (mmap, mprotect)
/// reach these operations through <c>ctx.Memory</c> instead of calling host
/// APIs directly. Member signatures deliberately mirror the implementation in
/// SharpEmu.Core so existing call sites migrate call-for-call.
/// </summary>
public interface IGuestAddressSpace : IGuestMemoryAllocator
{
ulong AllocateAt(ulong desiredAddress, ulong size, bool executable = true, bool allowAlternative = true);
bool TryAllocateAtOrAbove(ulong desiredAddress, ulong size, bool executable, ulong alignment, out ulong actualAddress);
bool TryProtect(ulong address, ulong size, GuestPageProtection protection);
}
@@ -6,6 +6,4 @@ namespace SharpEmu.HLE;
public interface IGuestMemoryAllocator
{
bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address);
bool TryFreeGuestMemory(ulong address);
}
+6 -198
View File
@@ -3,17 +3,18 @@
using System.Collections.Concurrent;
using System.Reflection;
using System.Runtime.CompilerServices;
using SharpEmu.Logging;
namespace SharpEmu.HLE;
public sealed class ModuleManager : IModuleManager
{
private static readonly SharpEmuLogger Log = SharpEmuLog.For("HLE");
private readonly ConcurrentDictionary<string, Delegate> _dispatchTable = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, ExportedFunction> _exportTable = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, ExportedFunction> _exportNameTable = new(StringComparer.Ordinal);
private readonly object _registrationGate = new();
private readonly HashSet<(Assembly Assembly, Generation Generation)> _scannedAssemblies = new();
private bool _isFrozen;
public int RegisterFromAssembly(Assembly assembly, Generation generation, ISymbolCatalog? symbolCatalog = null)
@@ -27,12 +28,6 @@ public sealed class ModuleManager : IModuleManager
throw new InvalidOperationException("Module registration is frozen.");
}
// Deduplicated: one assembly is reached through many types.
if (!_scannedAssemblies.Add((assembly, generation)))
{
return 0;
}
var registeredCount = 0;
var instances = new Dictionary<Type, object>();
@@ -55,7 +50,7 @@ public sealed class ModuleManager : IModuleManager
var handler = CreateHandler(type, method, instances);
if (!_dispatchTable.TryAdd(exportInfo.Value.Nid, handler))
{
Console.Error.WriteLine($"[HLE] Duplicate NID '{exportInfo.Value.Nid}' ({exportInfo.Value.ExportName}) — already registered, skipping.");
Log.Warning($"Duplicate NID '{exportInfo.Value.Nid}' ({exportInfo.Value.ExportName}) — already registered, skipping.");
continue;
}
@@ -81,195 +76,8 @@ public sealed class ModuleManager : IModuleManager
{
_isFrozen = true;
}
WarmHleTypeInitializers();
}
// A .cctor or first JIT running on a guest thread's hijacked stack fail-fasts the CLR.
// Run every HLE type's initializer and JIT its methods here first, on a host thread.
private void WarmHleTypeInitializers()
{
Assembly[] assemblies;
lock (_registrationGate)
{
assemblies = _scannedAssemblies.Select(entry => entry.Assembly).Distinct().ToArray();
}
assemblies = WithGuestReachableDependencies(assemblies);
var bclWarmed = WarmFrameworkTypeInitializers();
var warmed = 0;
var failed = 0;
var jitted = 0;
var jitFailed = 0;
foreach (var assembly in assemblies)
{
Type[] types;
try
{
types = assembly.GetTypes();
}
catch (ReflectionTypeLoadException ex)
{
types = ex.Types.Where(t => t is not null).ToArray()!;
}
const BindingFlags allMembers = BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
foreach (var type in types)
{
if (type is null || type.ContainsGenericParameters)
{
continue;
}
try
{
RuntimeHelpers.RunClassConstructor(type.TypeHandle);
warmed++;
}
catch
{
// A throw here beats a guest-thread fail-fast later; swallow and continue.
failed++;
}
// Force-JIT (not execute) every method so no guest thread compiles one first.
MethodBase[] members;
try
{
members = type.GetConstructors(allMembers)
.Concat<MethodBase>(type.GetMethods(allMembers))
.ToArray();
}
catch
{
continue;
}
foreach (var member in members)
{
if (member.ContainsGenericParameters || member.IsAbstract || member.MethodImplementationFlags.HasFlag(MethodImplAttributes.InternalCall))
{
continue;
}
try
{
RuntimeHelpers.PrepareMethod(member.MethodHandle);
jitted++;
}
catch
{
jitFailed++;
}
}
}
}
Console.Error.WriteLine(
$"[HLE] Warmed {warmed} type initializers ({failed} threw) + JIT-compiled {jitted} methods ({jitFailed} skipped) across {assemblies.Length} HLE assemblies, plus {bclWarmed} framework type initializers.");
}
// Framework .cctors too (but not JIT — the BCL is too large).
private static int WarmFrameworkTypeInitializers()
{
var warmed = 0;
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
var name = assembly.GetName().Name;
if (name is null || !IsFrameworkAssembly(name))
{
continue;
}
Type[] types;
try
{
types = assembly.GetTypes();
}
catch (ReflectionTypeLoadException ex)
{
types = ex.Types.Where(t => t is not null).ToArray()!;
}
catch
{
continue;
}
foreach (var type in types)
{
if (type is null || type.ContainsGenericParameters)
{
continue;
}
try
{
RuntimeHelpers.RunClassConstructor(type.TypeHandle);
warmed++;
}
catch
{
}
}
}
return warmed;
}
private static bool IsFrameworkAssembly(string assemblyName) =>
assemblyName.StartsWith("System", StringComparison.Ordinal) ||
string.Equals(assemblyName, "netstandard", StringComparison.Ordinal);
// Warm the interop assemblies guest threads reach (e.g. Silk.NET via the flip path).
private static Assembly[] WithGuestReachableDependencies(Assembly[] scanned)
{
var result = new Dictionary<string, Assembly>(StringComparer.Ordinal);
foreach (var assembly in scanned)
{
result[assembly.FullName ?? assembly.GetName().Name ?? string.Empty] = assembly;
}
foreach (var assembly in scanned)
{
AssemblyName[] references;
try
{
references = assembly.GetReferencedAssemblies();
}
catch
{
continue;
}
foreach (var reference in references)
{
var name = reference.Name;
if (name is null || !IsGuestReachableInterop(name))
{
continue;
}
try
{
var loaded = Assembly.Load(reference);
result[loaded.FullName ?? name] = loaded;
}
catch
{
}
}
}
return result.Values.ToArray();
}
private static bool IsGuestReachableInterop(string assemblyName) =>
assemblyName.StartsWith("Silk.NET", StringComparison.Ordinal) ||
assemblyName.StartsWith("SharpEmu", StringComparison.Ordinal);
public bool TryGetFunction(string nid, out Delegate function)
{
ArgumentException.ThrowIfNullOrWhiteSpace(nid);
@@ -301,7 +109,7 @@ public sealed class ModuleManager : IModuleManager
if (!_dispatchTable.TryGetValue(nid, out var function) || !_exportTable.TryGetValue(nid, out var export))
{
Console.Error.WriteLine($"[HLE] NID '{nid}' not found in dispatch table.");
Log.Warning($"NID '{nid}' not found in dispatch table.");
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
return false;
@@ -309,7 +117,7 @@ public sealed class ModuleManager : IModuleManager
if ((export.Target & context.TargetGeneration) == 0)
{
Console.Error.WriteLine($"[HLE] NID '{nid}' ({export.Name}) found but not implemented for generation {context.TargetGeneration} (targets: {export.Target}).");
Log.Warning($"NID '{nid}' ({export.Name}) found but not implemented for generation {context.TargetGeneration} (targets: {export.Target}).");
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED;
return false;
+82 -578
View File
@@ -29,7 +29,6 @@ public static class AgcExports
private const uint ItDispatchDirect = 0x15;
private const uint ItDispatchIndirect = 0x16;
private const uint ItWaitRegMem = 0x3C;
private const uint ItIndirectBuffer = 0x3F;
private const uint ItEventWrite = 0x46;
private const uint ItDmaData = 0x50;
private const uint ItSetContextReg = 0x69;
@@ -121,9 +120,6 @@ public static class AgcExports
private const uint RegisterDefaultsVersion13 = 13;
private const int RegisterDefaultsSize = 0x40;
private const int RegisterDefaultBlockSize = 16 * 8;
private const ulong ResourceRegistrationBytesPerResource = 0x118;
private const ulong ResourceRegistrationBytesPerOwner = 0x1E0;
private const int ResourceRegistrationMaxNameLength = 256;
private const ulong ShaderUserDataOffset = 0x08;
private const ulong ShaderCodeOffset = 0x10;
@@ -158,7 +154,7 @@ public static class AgcExports
private static readonly HashSet<uint> _tracedSubmittedDrawOpcodes = new();
private static readonly Dictionary<(ulong Ps, ulong State, Gen5PixelOutputKind Output), byte[]> _pixelSpirvCache = new();
private static readonly Dictionary<
(ulong Es, ulong EsState, ulong Ps, ulong PsState, string OutputLayout, uint Attributes),
(ulong Es, ulong EsState, ulong Ps, ulong PsState, Gen5PixelOutputKind Output),
(byte[] Vertex, byte[] Pixel)> _graphicsSpirvCache = new();
private static readonly Dictionary<
(ulong Cs, ulong State, uint LocalX, uint LocalY, uint LocalZ),
@@ -392,26 +388,9 @@ public static class AgcExports
public SubmittedDcbState Graphics { get; } = new();
public Dictionary<uint, SubmittedDcbState> ComputeQueues { get; } = new();
public Dictionary<ulong, ComputeImageWriter> ComputeImageWriters { get; } = new();
public Dictionary<uint, string> ResourceOwners { get; } = new();
public Dictionary<uint, RegisteredAgcResource> RegisteredResources { get; } = new();
public bool ResourceRegistrationInitialized { get; set; }
public ulong ResourceRegistrationMemory { get; set; }
public ulong ResourceRegistrationMemorySize { get; set; }
public uint ResourceRegistrationMaxOwners { get; set; }
public uint DefaultOwner { get; set; } = DefaultAgcOwner;
public uint NextOwner { get; set; } = 1;
public uint NextResource { get; set; } = 1;
public ulong WorkSequence { get; set; }
}
private readonly record struct RegisteredAgcResource(
uint Owner,
ulong Address,
ulong Size,
string Name,
uint Type,
uint Flags);
private readonly record struct RegisterDefaultValue(uint Offset, uint Value);
private readonly record struct RegisterDefaultGroup(
@@ -1944,96 +1923,6 @@ public static class AgcExports
return ReturnPointer(ctx, commandAddress);
}
// Guest draw-command builders: emit valid (skippable) packets and return a live
// command pointer so the guest's command-buffer build succeeds; full draw processing is TODO.
[SysAbiExport(
Nid = "8N2tmT3jmC8",
ExportName = "sceAgcDcbSetIndexCount",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int DcbSetIndexCount(CpuContext ctx)
{
var dcb = ctx[CpuRegister.Rdi];
var indexCount = (uint)ctx[CpuRegister.Rsi];
if (dcb == 0)
{
return ReturnPointer(ctx, 0);
}
if (!TryAllocateCommandDwords(ctx, dcb, 2, out var cmd) ||
!ctx.TryWriteUInt32(cmd, Pm4(2, ItNop, RZero)) ||
!ctx.TryWriteUInt32(cmd + 4, indexCount))
{
return ReturnPointer(ctx, 0);
}
return ReturnPointer(ctx, cmd);
}
[SysAbiExport(
Nid = "xSAR0LTcRKM",
ExportName = "sceAgcDcbJump",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int DcbJump(CpuContext ctx)
{
var dcb = ctx[CpuRegister.Rdi];
var target = ctx[CpuRegister.Rsi];
var sizeDwords = (uint)ctx[CpuRegister.Rdx];
if (dcb == 0)
{
return ReturnPointer(ctx, 0);
}
if (!TryAllocateCommandDwords(ctx, dcb, 4, out var cmd) ||
!ctx.TryWriteUInt32(cmd, Pm4(4, ItIndirectBuffer, RZero)) ||
!ctx.TryWriteUInt32(cmd + 4, (uint)(target & 0xFFFF_FFFFUL)) ||
!ctx.TryWriteUInt32(cmd + 8, (uint)((target >> 32) & 0xFFFFUL)) ||
!ctx.TryWriteUInt32(cmd + 12, sizeDwords & 0xFFFFF))
{
return ReturnPointer(ctx, 0);
}
return ReturnPointer(ctx, cmd);
}
[SysAbiExport(
Nid = "bbFueFP+J4k",
ExportName = "sceAgcDcbSetPredication",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int DcbSetPredication(CpuContext ctx)
{
var dcb = ctx[CpuRegister.Rdi];
var address = ctx[CpuRegister.Rsi];
if (dcb == 0)
{
return ReturnPointer(ctx, 0);
}
if (!TryAllocateCommandDwords(ctx, dcb, 3, out var cmd) ||
!ctx.TryWriteUInt32(cmd, Pm4(3, ItNop, RZero)) ||
!ctx.TryWriteUInt32(cmd + 4, (uint)(address & 0xFFFF_FFFFUL)) ||
!ctx.TryWriteUInt32(cmd + 8, (uint)(address >> 32)))
{
return ReturnPointer(ctx, 0);
}
return ReturnPointer(ctx, cmd);
}
[SysAbiExport(
Nid = "w6Dj1VJt5qY",
ExportName = "sceAgcSetPacketPredication",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int SetPacketPredication(CpuContext ctx)
{
// Global predication toggle on a packet; a no-op is safe for rendering.
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "w2rJhmD+dsE",
ExportName = "sceAgcDriverAddEqEvent",
@@ -2110,16 +1999,8 @@ public static class AgcExports
var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
lock (gpuState.Gate)
{
Gen5ShaderScalarEvaluator.BeginGlobalMemoryReadScope();
try
{
ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets);
DrainResumableDcbs(ctx, gpuState, tracePackets);
}
finally
{
Gen5ShaderScalarEvaluator.EndGlobalMemoryReadScope();
}
ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets);
DrainResumableDcbs(ctx, gpuState, tracePackets);
}
ctx[CpuRegister.Rax] = 0;
@@ -2149,35 +2030,27 @@ public static class AgcExports
var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
lock (gpuState.Gate)
{
Gen5ShaderScalarEvaluator.BeginGlobalMemoryReadScope();
try
for (uint i = 0; i < bufferCount; i++)
{
for (uint i = 0; i < bufferCount; i++)
if (!ctx.TryReadUInt64(addressArray + i * 8, out var commandAddress) ||
commandAddress == 0 ||
!ctx.TryReadUInt32(sizeArray + i * 4, out var dwordCount) ||
dwordCount == 0)
{
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);
continue;
}
DrainResumableDcbs(ctx, gpuState, tracePackets);
}
finally
{
Gen5ShaderScalarEvaluator.EndGlobalMemoryReadScope();
if (tracePackets)
{
TraceAgc(
$"agc.driver_submit_multi_dcbs index={i}/{bufferCount} " +
$"addr=0x{commandAddress:X16} dwords={dwordCount}");
}
ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets);
}
DrainResumableDcbs(ctx, gpuState, tracePackets);
}
ctx[CpuRegister.Rax] = 0;
@@ -2225,16 +2098,8 @@ public static class AgcExports
gpuState.ComputeQueues.Add(ownerHandle, queueState);
}
Gen5ShaderScalarEvaluator.BeginGlobalMemoryReadScope();
try
{
ParseSubmittedDcb(ctx, gpuState, queueState, commandAddress, dwordCount, tracePackets);
DrainResumableDcbs(ctx, gpuState, tracePackets);
}
finally
{
Gen5ShaderScalarEvaluator.EndGlobalMemoryReadScope();
}
ParseSubmittedDcb(ctx, gpuState, queueState, commandAddress, dwordCount, tracePackets);
DrainResumableDcbs(ctx, gpuState, tracePackets);
}
ctx[CpuRegister.Rax] = 0;
@@ -2255,87 +2120,12 @@ public static class AgcExports
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (!ctx.TryWriteUInt32(outAddress, ResourceRegistrationMaxNameLength))
if (!ctx.TryWriteUInt32(outAddress, 256))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceAgc(
$"agc.driver_get_resource_registration_max_name_length " +
$"out=0x{outAddress:X16} value={ResourceRegistrationMaxNameLength}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "AOLcoIkQDgM",
ExportName = "sceAgcDriverQueryResourceRegistrationUserMemoryRequirements",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int DriverQueryResourceRegistrationUserMemoryRequirements(CpuContext ctx)
{
var sizeAddress = ctx[CpuRegister.Rdi];
var resourceCount = ctx[CpuRegister.Rsi];
var ownerCount = ctx[CpuRegister.Rdx];
if (sizeAddress == 0 || resourceCount == 0 || ownerCount == 0)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
ulong requiredSize;
try
{
requiredSize = checked(
resourceCount * ResourceRegistrationBytesPerResource +
ownerCount * ResourceRegistrationBytesPerOwner);
}
catch (OverflowException)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (!ctx.TryWriteUInt64(sizeAddress, requiredSize))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceAgc(
$"agc.driver_query_resource_registration_memory resources={resourceCount} " +
$"owners={ownerCount} bytes=0x{requiredSize:X}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "F0Y42t-3e18",
ExportName = "sceAgcDriverInitResourceRegistration",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int DriverInitResourceRegistration(CpuContext ctx)
{
var memoryAddress = ctx[CpuRegister.Rdi];
var memorySize = ctx[CpuRegister.Rsi];
var ownerCount = ctx[CpuRegister.Rdx];
if (memoryAddress == 0 || memorySize == 0 || ownerCount == 0 || ownerCount > uint.MaxValue)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
lock (state.Gate)
{
state.ResourceRegistrationInitialized = true;
state.ResourceRegistrationMemory = memoryAddress;
state.ResourceRegistrationMemorySize = memorySize;
state.ResourceRegistrationMaxOwners = (uint)ownerCount;
state.ResourceOwners.Clear();
state.RegisteredResources.Clear();
state.DefaultOwner = DefaultAgcOwner;
state.NextOwner = 1;
state.NextResource = 1;
}
TraceAgc(
$"agc.driver_init_resource_registration memory=0x{memoryAddress:X16} " +
$"bytes=0x{memorySize:X} owners={ownerCount}");
TraceAgc($"agc.driver_get_resource_registration_max_name_length out=0x{outAddress:X16} value=256");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
@@ -2354,97 +2144,12 @@ public static class AgcExports
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
uint owner;
lock (state.Gate)
{
owner = state.DefaultOwner;
}
if (!ctx.TryWriteUInt32(ownerAddress, owner))
if (!ctx.TryWriteUInt32(ownerAddress, DefaultAgcOwner))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceAgc($"agc.driver_get_default_owner out=0x{ownerAddress:X16} owner={owner}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "U9ueyEhSkF4",
ExportName = "sceAgcDriverRegisterDefaultOwner",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int DriverRegisterDefaultOwner(CpuContext ctx)
{
var owner = (uint)ctx[CpuRegister.Rdi];
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
lock (state.Gate)
{
state.DefaultOwner = owner;
}
TraceAgc($"agc.driver_register_default_owner owner={owner}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "X-Nm5KLREeg",
ExportName = "sceAgcDriverRegisterOwner",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int DriverRegisterOwner(CpuContext ctx)
{
var ownerAddress = ctx[CpuRegister.Rdi];
var nameAddress = ctx[CpuRegister.Rsi];
if (ownerAddress == 0 || nameAddress == 0 ||
!TryReadGuestCString(
ctx,
nameAddress,
ResourceRegistrationMaxNameLength,
out var nameBytes))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
uint owner;
lock (state.Gate)
{
if (!state.ResourceRegistrationInitialized ||
state.ResourceRegistrationMaxOwners != 0 &&
state.ResourceOwners.Count >= state.ResourceRegistrationMaxOwners)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
owner = state.NextOwner;
while (owner == state.DefaultOwner || state.ResourceOwners.ContainsKey(owner))
{
owner++;
if (owner == 0)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
}
state.NextOwner = owner + 1;
state.ResourceOwners.Add(owner, System.Text.Encoding.UTF8.GetString(nameBytes));
}
if (!ctx.TryWriteUInt32(ownerAddress, owner))
{
lock (state.Gate)
{
state.ResourceOwners.Remove(owner);
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceAgc(
$"agc.driver_register_owner out=0x{ownerAddress:X16} owner={owner} " +
$"name={System.Text.Encoding.UTF8.GetString(nameBytes)}");
TraceAgc($"agc.driver_get_default_owner out=0x{ownerAddress:X16} owner={DefaultAgcOwner}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
@@ -2455,90 +2160,19 @@ public static class AgcExports
LibraryName = "libSceAgc")]
public static int DriverRegisterResource(CpuContext ctx)
{
var resourceHandleAddress = ctx[CpuRegister.Rdi];
var resourceAddress = ctx[CpuRegister.Rdi];
var owner = (uint)ctx[CpuRegister.Rsi];
var resourceAddress = ctx[CpuRegister.Rdx];
var resourceSize = ctx[CpuRegister.Rcx];
var nameAddress = ctx[CpuRegister.R8];
var type = (uint)ctx[CpuRegister.R9];
if (resourceHandleAddress == 0 || resourceAddress == 0 || resourceSize == 0 ||
!ctx.TryReadUInt32(ctx[CpuRegister.Rsp] + sizeof(ulong), out var flags) ||
!TryReadGuestCString(
ctx,
nameAddress,
ResourceRegistrationMaxNameLength,
out var nameBytes))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
uint resourceHandle;
lock (state.Gate)
{
if (!state.ResourceRegistrationInitialized ||
owner != state.DefaultOwner &&
!state.ResourceOwners.ContainsKey(owner))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
resourceHandle = state.NextResource++;
if (resourceHandle == 0)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
state.RegisteredResources.Add(
resourceHandle,
new RegisteredAgcResource(
owner,
resourceAddress,
resourceSize,
System.Text.Encoding.UTF8.GetString(nameBytes),
type,
flags));
}
if (!ctx.TryWriteUInt32(resourceHandleAddress, resourceHandle))
{
lock (state.Gate)
{
state.RegisteredResources.Remove(resourceHandle);
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var nameAddress = ctx[CpuRegister.Rdx];
var type = (uint)ctx[CpuRegister.R8];
var flags = (uint)ctx[CpuRegister.R9];
TraceAgc(
$"agc.driver_register_resource handle={resourceHandle} owner={owner} " +
$"resource=0x{resourceAddress:X16} bytes=0x{resourceSize:X} " +
$"name={System.Text.Encoding.UTF8.GetString(nameBytes)} type={type} flags={flags}");
$"agc.driver_register_resource resource=0x{resourceAddress:X16} owner={owner} " +
$"name=0x{nameAddress:X16} type={type} flags={flags}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "pWLG7WOpVcw",
ExportName = "sceAgcDriverUnregisterResource",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int DriverUnregisterResource(CpuContext ctx)
{
var resourceHandle = (uint)ctx[CpuRegister.Rdi];
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
lock (state.Gate)
{
if (!state.RegisteredResources.Remove(resourceHandle))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
}
TraceAgc($"agc.driver_unregister_resource handle={resourceHandle}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "-KRzWekV120",
ExportName = "sceAgcDriverUnknown_KRzWekV120",
@@ -3420,13 +3054,12 @@ public static class AgcExports
textures,
globalMemoryBuffers,
translatedDraw.AttributeCount,
translatedDraw.RenderTargets.Select(target =>
new VulkanGuestRenderTarget(
target.Address,
target.Width,
target.Height,
target.Format,
target.NumberType)).ToArray(),
new VulkanGuestRenderTarget(
firstTarget.Address,
firstTarget.Width,
firstTarget.Height,
firstTarget.Format,
firstTarget.NumberType),
translatedDraw.VertexSpirv,
translatedDraw.VertexCount,
translatedDraw.InstanceCount,
@@ -3534,8 +3167,7 @@ public static class AgcExports
exportState,
out var exportEvaluation,
out error,
resolveVertexInputs: true,
vertexRecordLimit: indexed ? null : vertexCount) ||
resolveVertexInputs: true) ||
!Gen5ShaderTranslator.TryCreateState(
ctx,
pixelShaderAddress,
@@ -3554,47 +3186,19 @@ public static class AgcExports
}
var renderTargets = GetRenderTargets(state.CxRegisters)
.Where(target => HasPixelColorExport(pixelState, target.Slot))
.OrderBy(target => target.Slot)
.Where(target =>
target.Slot == 0 &&
HasPixelColorExport(pixelState, target.Slot))
.ToArray();
var renderTargetFormats = new VulkanRenderTargetFormat[renderTargets.Length];
for (var index = 0; index < renderTargets.Length; index++)
{
var target = renderTargets[index];
if (!VulkanVideoPresenter.TryDecodeRenderTargetFormat(
target.Format,
target.NumberType,
out renderTargetFormats[index]))
{
error =
$"unsupported color target format={target.Format} number_type={target.NumberType}";
return false;
}
}
var pixelOutputs = renderTargets
.Select((target, location) => new Gen5PixelOutputBinding(
target.Slot,
(uint)location,
renderTargetFormats[location].OutputKind))
.ToArray();
var outputLayout = string.Join(
';',
pixelOutputs.Select(output =>
$"{output.GuestSlot}:{output.HostLocation}:{(int)output.Kind}"));
var attributeCount = GetInterpolatedAttributeCount(pixelState);
var exportStateFingerprint = ComputeShaderStructureFingerprint(exportEvaluation);
var pixelStateFingerprint = ComputeShaderStructureFingerprint(pixelEvaluation);
var outputKind = GetPixelOutputKind(renderTargets.FirstOrDefault().NumberType);
var exportStateFingerprint = ComputeShaderStateFingerprint(exportEvaluation);
var pixelStateFingerprint = ComputeShaderStateFingerprint(pixelEvaluation);
var shaderKey = (
exportShaderAddress,
exportStateFingerprint,
pixelShaderAddress,
pixelStateFingerprint,
outputLayout,
attributeCount);
var totalGlobalBuffers =
pixelEvaluation.GlobalMemoryBindings.Count +
exportEvaluation.GlobalMemoryBindings.Count;
outputKind);
(byte[] Vertex, byte[] Pixel) compiled;
lock (_submitTraceGate)
{
@@ -3603,25 +3207,26 @@ public static class AgcExports
if (compiled.Vertex is null || compiled.Pixel is null)
{
var totalGlobalBuffers =
pixelEvaluation.GlobalMemoryBindings.Count +
exportEvaluation.GlobalMemoryBindings.Count;
if (!Gen5SpirvTranslator.TryCompilePixelShader(
pixelState,
pixelEvaluation,
pixelOutputs,
outputKind,
out var pixelShader,
out error,
globalBufferBase: 0,
totalGlobalBufferCount: totalGlobalBuffers + 2,
imageBindingBase: 0,
scalarRegisterBufferIndex: totalGlobalBuffers) ||
totalGlobalBufferCount: totalGlobalBuffers,
imageBindingBase: 0) ||
!Gen5SpirvTranslator.TryCompileVertexShader(
exportState,
exportEvaluation,
out var vertexShader,
out error,
globalBufferBase: pixelEvaluation.GlobalMemoryBindings.Count,
totalGlobalBufferCount: totalGlobalBuffers + 2,
imageBindingBase: pixelEvaluation.ImageBindings.Count,
scalarRegisterBufferIndex: totalGlobalBuffers + 1))
totalGlobalBufferCount: totalGlobalBuffers,
imageBindingBase: pixelEvaluation.ImageBindings.Count))
{
return false;
}
@@ -3673,8 +3278,6 @@ public static class AgcExports
var globalMemoryBindings = pixelEvaluation.GlobalMemoryBindings
.Concat(exportEvaluation.GlobalMemoryBindings)
.Append(CreateScalarRegisterBinding(pixelEvaluation))
.Append(CreateScalarRegisterBinding(exportEvaluation))
.ToArray();
IReadOnlyList<Gen5VertexInputBinding> vertexInputs =
exportEvaluation.VertexInputs ?? [];
@@ -3685,7 +3288,7 @@ public static class AgcExports
primitiveType,
compiled.Vertex,
compiled.Pixel,
attributeCount,
GetInterpolatedAttributeCount(pixelState),
vertexCount,
state.InstanceCount,
indexed ? CreateVulkanIndexBuffer(ctx, state, vertexCount) : null,
@@ -3694,7 +3297,7 @@ public static class AgcExports
vertexInputs,
renderTargets,
ApplyTransparentPremultipliedFillClear(
CreateRenderState(state.CxRegisters, renderTargets, pixelState),
CreateRenderState(state.CxRegisters, renderTargets.FirstOrDefault()),
textures,
vertexInputs,
pixelEvaluation.InitialScalarRegisters));
@@ -3710,8 +3313,7 @@ public static class AgcExports
/// Chowdren resets its effect layers with an untextured transparent-black
/// fill using premultiplied blending. With One/OneMinusSrcAlpha that draw
/// is otherwise a no-op, causing fog and vignette layers to accumulate.
/// Treat precisely that draw shape as an overwrite only when every MRT
/// attachment uses the same premultiplied blend pattern.
/// Treat precisely that draw shape as an overwrite.
/// </summary>
private static VulkanGuestRenderState ApplyTransparentPremultipliedFillClear(
VulkanGuestRenderState renderState,
@@ -3723,7 +3325,13 @@ public static class AgcExports
textures.Count != 0 ||
vertexInputs.Count != 0 ||
pixelUserData.Count < 4 ||
!renderState.Blends.All(IsTransparentPremultipliedFillBlend))
renderState.Blend is not
{
Enable: true,
ColorSrcFactor: 1,
ColorDstFactor: 5,
ColorFunc: 0,
})
{
return renderState;
}
@@ -3738,21 +3346,10 @@ public static class AgcExports
return renderState with
{
Blends = renderState.Blends
.Select(blend => blend with { Enable = false })
.ToArray(),
Blend = renderState.Blend with { Enable = false },
};
}
private static bool IsTransparentPremultipliedFillBlend(VulkanGuestBlendState blend) =>
blend is
{
Enable: true,
ColorSrcFactor: 1,
ColorDstFactor: 5,
ColorFunc: 0,
};
private static VulkanGuestIndexBuffer? CreateVulkanIndexBuffer(
CpuContext ctx,
SubmittedDcbState state,
@@ -3775,15 +3372,19 @@ public static class AgcExports
: null;
}
private static bool HasPixelColorExport(Gen5ShaderState state, uint target) =>
GetPixelColorExportMask(state, target) != 0;
private static Gen5PixelOutputKind GetPixelOutputKind(uint numberType) =>
numberType switch
{
4 => Gen5PixelOutputKind.Uint,
5 => Gen5PixelOutputKind.Sint,
_ => Gen5PixelOutputKind.Float,
};
private static uint GetPixelColorExportMask(Gen5ShaderState state, uint target) =>
state.Program.Instructions
.Select(instruction => instruction.Control)
.OfType<Gen5ExportControl>()
.Where(export => export.Target == target)
.Aggregate(0u, (mask, export) => mask | (export.EnableMask & 0xFu));
private static bool HasPixelColorExport(Gen5ShaderState state, uint target) =>
state.Program.Instructions.Any(instruction =>
instruction.Control is Gen5ExportControl export &&
export.Target == target &&
export.EnableMask != 0);
private static uint GetInterpolatedAttributeCount(Gen5ShaderState state)
{
@@ -3799,88 +3400,6 @@ public static class AgcExports
return (uint)(maxAttribute + 1);
}
private const int ShaderScalarRegisterCount = 256;
private static Gen5GlobalMemoryBinding CreateScalarRegisterBinding(
Gen5ShaderEvaluation evaluation)
{
var data = new byte[ShaderScalarRegisterCount * sizeof(uint)];
var registers = evaluation.InitialScalarRegisters;
var count = Math.Min(registers.Count, ShaderScalarRegisterCount);
for (var index = 0; index < count; index++)
{
var value = registers[index];
var offset = index * sizeof(uint);
data[offset] = (byte)value;
data[offset + 1] = (byte)(value >> 8);
data[offset + 2] = (byte)(value >> 16);
data[offset + 3] = (byte)(value >> 24);
}
return new Gen5GlobalMemoryBinding(0, 0, [], data);
}
private static ulong ComputeShaderStructureFingerprint(Gen5ShaderEvaluation evaluation)
{
const ulong offsetBasis = 14695981039346656037UL;
const ulong prime = 1099511628211UL;
var hash = offsetBasis;
void Mix(ulong value) => hash = (hash ^ value) * prime;
Mix((ulong)evaluation.GlobalMemoryBindings.Count);
foreach (var binding in evaluation.GlobalMemoryBindings)
{
Mix(binding.ScalarAddress);
Mix((ulong)binding.InstructionPcs.Count);
foreach (var pc in binding.InstructionPcs)
{
Mix(pc);
}
}
Mix((ulong)evaluation.ImageBindings.Count);
foreach (var image in evaluation.ImageBindings)
{
Mix(image.Pc);
Mix((ulong)(uint)image.Opcode.GetHashCode());
foreach (var word in image.ResourceDescriptor)
{
Mix(word);
}
foreach (var word in image.SamplerDescriptor)
{
Mix(word);
}
Mix(image.MipLevel ?? uint.MaxValue);
}
if (evaluation.VertexInputs is { } vertexInputs)
{
Mix((ulong)vertexInputs.Count);
foreach (var input in vertexInputs)
{
Mix(input.Pc);
Mix(input.Location);
Mix(input.ComponentCount);
Mix(input.DataFormat);
Mix(input.NumberFormat);
Mix(input.Stride);
}
}
if (evaluation.ComputeSystemRegisters is { } computeSystemRegisters)
{
Mix(computeSystemRegisters.WorkGroupXRegister ?? uint.MaxValue);
Mix(computeSystemRegisters.WorkGroupYRegister ?? uint.MaxValue);
Mix(computeSystemRegisters.WorkGroupZRegister ?? uint.MaxValue);
Mix(computeSystemRegisters.ThreadGroupSizeRegister ?? uint.MaxValue);
}
return hash;
}
private static ulong ComputeShaderStateFingerprint(Gen5ShaderEvaluation evaluation)
{
const ulong offsetBasis = 14695981039346656037UL;
@@ -3941,25 +3460,11 @@ public static class AgcExports
private static VulkanGuestRenderState CreateRenderState(
IReadOnlyDictionary<uint, uint> registers,
IReadOnlyList<RenderTargetDescriptor> targets,
Gen5ShaderState pixelState)
RenderTargetDescriptor target)
{
if (targets.Count == 0)
{
return VulkanGuestRenderState.Default;
}
var target = targets[0];
var scissor = DecodeScissor(registers, target.Width, target.Height);
return new VulkanGuestRenderState(
targets.Select(target =>
{
var blend = DecodeBlendState(registers, target.Slot);
return blend with
{
WriteMask = blend.WriteMask & GetPixelColorExportMask(pixelState, target.Slot),
};
}).ToArray(),
DecodeBlendState(registers, target.Slot),
scissor,
DecodeViewport(registers, target.Width, target.Height, scissor));
}
@@ -3984,7 +3489,7 @@ public static class AgcExports
(control >> 24) & 0x1Fu,
(control >> 21) & 0x7u,
((control >> 29) & 1u) != 0,
writeMask);
writeMask == 0 ? 0xFu : writeMask);
}
private static VulkanGuestRect? DecodeScissor(
@@ -4745,7 +4250,7 @@ public static class AgcExports
var localSizeZ = GetComputeLocalSize(state.ShRegisters, ComputeNumThreadZ);
var gpuDispatch = false;
var computeError = string.Empty;
if ((hasStorageBinding || evaluation.GlobalMemoryBindings.Count != 0) &&
if (hasStorageBinding &&
(ulong)localSizeX * localSizeY * localSizeZ <= 1024)
{
var shaderKey = (
@@ -4815,8 +4320,7 @@ public static class AgcExports
$"groups={dispatch.GroupCountX}x{dispatch.GroupCountY}x{dispatch.GroupCountZ} " +
$"local={localSizeX}x{localSizeY}x{localSizeZ} " +
$"sys={DescribeComputeSystemRegisters(computeSystemRegisters)} " +
$"gpu={gpuDispatch} blits={blitCount} " +
$"globals={evaluation.GlobalMemoryBindings.Count}" +
$"gpu={gpuDispatch} blits={blitCount}" +
(computeError.Length == 0 ? string.Empty : $" error={computeError}") +
$" bindings=[{string.Join(',', descriptions)}]");
}
@@ -5322,7 +4826,7 @@ public static class AgcExports
if (Gen5SpirvTranslator.TryCompilePixelShader(
pixelState,
evaluation,
[new(0, 0, Gen5PixelOutputKind.Float)],
Gen5PixelOutputKind.Float,
out var compiledPixel,
out var compileError))
{
-5
View File
@@ -49,11 +49,6 @@ internal enum Gen5PixelOutputKind
Sint,
}
internal readonly record struct Gen5PixelOutputBinding(
uint GuestSlot,
uint HostLocation,
Gen5PixelOutputKind Kind);
internal enum Gen5SpirvStage
{
Vertex,
@@ -13,24 +13,6 @@ internal static class Gen5ShaderScalarEvaluator
private const int ImageDescriptorDwords = 8;
private const int SamplerDescriptorDwords = 4;
private const int MaxGlobalMemoryBindingBytes = 16 * 1024 * 1024;
private static readonly int DefaultGlobalMemoryBindingBytes =
int.TryParse(
Environment.GetEnvironmentVariable("SHARPEMU_GLOBAL_BINDING_BYTES"),
out var configured) && configured >= sizeof(uint)
? Math.Min(configured, MaxGlobalMemoryBindingBytes)
: 1 * 1024 * 1024;
internal static long GlobalMemoryReadCount;
internal static long GlobalMemoryReadBytes;
internal static long GlobalMemoryReadCacheHits;
internal static long GlobalMemoryReadPvmBytes;
internal static long GlobalMemoryReadLibcBytes;
internal static long GlobalMemoryReadReuses;
private const long CrossFrameReadCacheMaxBytes = 1024L * 1024 * 1024;
private static readonly object _crossFrameReadGate = new();
private static readonly Dictionary<(ulong BaseAddress, int SizeBytes), byte[]> _crossFrameReadCache = new();
private static long _crossFrameReadCacheBytes;
private const ulong RdnaWaveMask = 0xFFFF_FFFFUL;
private readonly record struct BufferDescriptor(
@@ -62,8 +44,7 @@ internal static class Gen5ShaderScalarEvaluator
Gen5ShaderState state,
out Gen5ShaderEvaluation evaluation,
out string error,
bool resolveVertexInputs = false,
uint? vertexRecordLimit = null)
bool resolveVertexInputs = false)
{
evaluation = default!;
error = string.Empty;
@@ -274,28 +255,10 @@ internal static class Gen5ShaderScalarEvaluator
if (resolveVertexInputs &&
IsVertexFetchCandidate(instruction, bufferMemory, bufferDescriptor))
{
var vertexReadSize = bufferDescriptor.SizeBytes;
if (vertexRecordLimit is { } recordLimit &&
instruction.Sources.Count > 2 &&
TryEvaluateScalarOperand(
instruction.Sources[2],
scalarRegisters,
out var scalarOffset))
{
var bindingOffset = unchecked((uint)bufferMemory.OffsetBytes + scalarOffset);
var requiredBytes =
(ulong)bindingOffset +
(ulong)(Math.Max(recordLimit, 1u) - 1u) * bufferDescriptor.Stride +
(ulong)bufferMemory.DwordCount * sizeof(uint);
vertexReadSize = Math.Min(
bufferDescriptor.SizeBytes,
Math.Max(requiredBytes, sizeof(uint)));
}
if (!TryReadGlobalMemory(
ctx,
bufferDescriptor.BaseAddress,
vertexReadSize,
bufferDescriptor.SizeBytes,
out var vertexData))
{
error =
@@ -570,29 +533,22 @@ internal static class Gen5ShaderScalarEvaluator
return false;
}
[ThreadStatic]
private static Dictionary<(ulong BaseAddress, int SizeBytes), byte[]>? _globalMemoryReadCache;
internal static void BeginGlobalMemoryReadScope()
{
_globalMemoryReadCache = new Dictionary<(ulong, int), byte[]>();
}
internal static void EndGlobalMemoryReadScope()
{
_globalMemoryReadCache = null;
}
private static bool TryReadGlobalMemory(
CpuContext ctx,
ulong baseAddress,
out byte[] data)
{
return TryReadGlobalMemory(
ctx,
baseAddress,
(ulong)DefaultGlobalMemoryBindingBytes,
out data);
for (var size = MaxGlobalMemoryBindingBytes; size >= 4096; size >>= 1)
{
data = GC.AllocateUninitializedArray<byte>(size);
if (ctx.Memory.TryRead(baseAddress, data))
{
return true;
}
}
data = [];
return false;
}
private static bool TryReadGlobalMemory(
@@ -614,74 +570,13 @@ internal static class Gen5ShaderScalarEvaluator
return false;
}
var cache = _globalMemoryReadCache;
var cacheKey = (baseAddress, (int)cappedSize);
if (cache is not null && cache.TryGetValue(cacheKey, out var cached))
{
Interlocked.Increment(ref GlobalMemoryReadCacheHits);
data = cached;
return true;
}
byte[]? previous;
lock (_crossFrameReadGate)
{
_crossFrameReadCache.TryGetValue(cacheKey, out previous);
}
if (previous is not null && ctx.Memory.TryCompare(baseAddress, previous))
{
Interlocked.Increment(ref GlobalMemoryReadReuses);
if (cache is not null)
{
cache[cacheKey] = previous;
}
data = previous;
return true;
}
var candidateSize = (int)cappedSize;
while (candidateSize >= sizeof(uint))
{
data = GC.AllocateUninitializedArray<byte>(candidateSize);
var readFromPvm = ctx.Memory.TryRead(baseAddress, data);
if (readFromPvm ||
if (ctx.Memory.TryRead(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;
}
+4 -17
View File
@@ -621,8 +621,6 @@ internal static class Gen5ShaderTranslator
0x10 => "SSendmsg",
0x16 => "STtraceData",
0x20 => "SInstPrefetch",
0x21 => "SClause",
0x23 => "SWaitcntDepctr",
_ => string.Empty,
};
@@ -720,7 +718,7 @@ internal static class Gen5ShaderTranslator
}
var src0 = word & 0x1FF;
sizeDwords = opcode is 0x20 or 0x21 or 0x2C or 0x2D || src0 is 0xF9 or 0xFA or 0xFF ? 2u : 1u;
sizeDwords = opcode is 0x20 or 0x21 || src0 is 0xF9 or 0xFA or 0xFF ? 2u : 1u;
error = string.Empty;
name = opcode switch
{
@@ -759,9 +757,7 @@ internal static class Gen5ShaderTranslator
0x28 => "VAddcU32",
0x29 => "VSubbU32",
0x2A => "VSubbrevU32",
0x2B => "VFmacF32",
0x2C => "VFmamkF32",
0x2D => "VFmaakF32",
0x2B => "VLdexpF32",
0x2F => "VCvtPkrtzF16F32",
0x30 => "VCvtPkU16U32",
0x31 => "VCvtPkI16I32",
@@ -802,15 +798,8 @@ internal static class Gen5ShaderTranslator
0x14 => "VCmpxGtF32",
0x15 => "VCmpxLgF32",
0x16 => "VCmpxGeF32",
0x17 => "VCmpxOF32",
0x18 => "VCmpxUF32",
0x19 => "VCmpxNgeF32",
0x1A => "VCmpxNlgF32",
0x1B => "VCmpxNgtF32",
0x1C => "VCmpxNleF32",
0x1D => "VCmpxNeqF32",
0x1E => "VCmpxNltF32",
0x1F => "VCmpxTruF32",
0x80 => "VCmpFI32",
0x81 => "VCmpLtI32",
0x82 => "VCmpEqI32",
@@ -881,7 +870,6 @@ internal static class Gen5ShaderTranslator
0x10F => "VMinF32",
0x110 => "VMaxF32",
0x11F => "VMacF32",
0x12B => "VFmacF32",
0x12F => "VCvtPkrtzF16F32",
0x141 => "VMadF32",
0x143 => "VMadU32U24",
@@ -909,7 +897,6 @@ internal static class Gen5ShaderTranslator
0x169 => "VMulLoU32",
0x16A => "VMulHiU32",
0x16B => "VMulLoI32",
0x16C => "VMulHiI32",
0x360 => "VMadU32U16",
0x361 => "VMulLoU32",
0x362 => "VLdexpF32",
@@ -1392,7 +1379,7 @@ internal static class Gen5ShaderTranslator
Gen5Operand.Source(word & 0x1FF, literal),
Gen5Operand.Vector((word >> 9) & 0xFF),
];
if (opcode is "VMadMkF32" or "VFmamkF32" && literal.HasValue)
if (opcode == "VMadMkF32" && literal.HasValue)
{
sources =
[
@@ -1401,7 +1388,7 @@ internal static class Gen5ShaderTranslator
sources[1],
];
}
else if (opcode is "VMadAkF32" or "VFmaakF32" && literal.HasValue)
else if (opcode == "VMadAkF32" && literal.HasValue)
{
sources =
[
@@ -255,8 +255,6 @@ internal static partial class Gen5SpirvTranslator
case "VFmaF32":
case "VMadMkF32":
case "VMadAkF32":
case "VFmamkF32":
case "VFmaakF32":
result = EmitFloatResult(
instruction,
Ext(
@@ -267,7 +265,6 @@ internal static partial class Gen5SpirvTranslator
GetFloatSource(instruction, 2)));
break;
case "VMacF32":
case "VFmacF32":
{
var addend = Bitcast(_floatType, LoadV(destination));
result = EmitFloatResult(
@@ -337,7 +334,6 @@ internal static partial class Gen5SpirvTranslator
result = EmitSubtractWithBorrow(instruction, reverse: true);
break;
case "VMulLoU32":
case "VMulLoI32":
case "VMulU32U24":
result = EmitIntegerBinary(instruction, SpirvOp.IMul);
break;
@@ -373,37 +369,6 @@ internal static partial class Gen5SpirvTranslator
_module.Constant64(_ulongType, 32)));
break;
}
case "VMulHiI32":
{
var wideLeft = _module.AddInstruction(
SpirvOp.SConvert,
_longType,
Bitcast(_intType, GetRawSource(instruction, 0)));
var wideRight = _module.AddInstruction(
SpirvOp.SConvert,
_longType,
Bitcast(_intType, GetRawSource(instruction, 1)));
var product = _module.AddInstruction(
SpirvOp.IMul,
_longType,
wideLeft,
wideRight);
result = _module.AddInstruction(
SpirvOp.UConvert,
_uintType,
ShiftRightLogical64(
Bitcast(_ulongType, product),
_module.Constant64(_ulongType, 32)));
break;
}
case "VBcntU32B32":
result = IAdd(
_module.AddInstruction(
SpirvOp.BitCount,
_uintType,
GetRawSource(instruction, 0)),
GetRawSource(instruction, 1));
break;
case "VMadU32U24":
{
var left = BitwiseAnd(
@@ -601,18 +566,6 @@ internal static partial class Gen5SpirvTranslator
Ext(38, _uintType, high, right));
break;
}
case "VSadU32":
{
var left = GetRawSource(instruction, 0);
var right = GetRawSource(instruction, 1);
var difference = _module.AddInstruction(
SpirvOp.ISub,
_uintType,
Ext(41, _uintType, left, right),
Ext(38, _uintType, left, right));
result = IAdd(difference, GetRawSource(instruction, 2));
break;
}
case "VMed3I32":
{
var left = Bitcast(_intType, GetRawSource(instruction, 0));
@@ -722,14 +675,6 @@ internal static partial class Gen5SpirvTranslator
result = Ext(58, _uintType, vector);
break;
}
case "VCvtPkU16U32":
case "VCvtPkI16I32":
result = BitwiseOr(
BitwiseAnd(GetRawSource(instruction, 0), UInt(0xFFFF)),
ShiftLeftLogical(
BitwiseAnd(GetRawSource(instruction, 1), UInt(0xFFFF)),
UInt(16)));
break;
default:
error = $"unsupported vector opcode {instruction.Opcode}";
return false;
@@ -861,25 +806,6 @@ internal static partial class Gen5SpirvTranslator
{
condition = _module.ConstantBool(true);
}
else if (opcode is "VCmpOF32" or "VCmpxOF32" or "VCmpUF32" or "VCmpxUF32")
{
// The ordered/unordered predicates only test whether either
// operand is NaN. SPIR-V's OpOrdered/OpUnordered are Kernel-only,
// so build the same result from OpIsNan, which needs no extra
// capability: unordered = isnan(a) || isnan(b), ordered = !that.
var left = GetFloatSource(instruction, 0);
var right = GetFloatSource(instruction, 1);
var nanLeft = _module.AddInstruction(SpirvOp.IsNan, _boolType, left);
var nanRight = _module.AddInstruction(SpirvOp.IsNan, _boolType, right);
var unordered = _module.AddInstruction(
SpirvOp.LogicalOr,
_boolType,
nanLeft,
nanRight);
condition = opcode is "VCmpUF32" or "VCmpxUF32"
? unordered
: _module.AddInstruction(SpirvOp.LogicalNot, _boolType, unordered);
}
else if (opcode is not ("VCmpClassF32" or "VCmpxClassF32") &&
opcode.EndsWith("F32", StringComparison.Ordinal))
{
@@ -894,7 +820,6 @@ internal static partial class Gen5SpirvTranslator
"VCmpLgF32" or "VCmpxLgF32" => SpirvOp.FOrdNotEqual,
"VCmpGeF32" or "VCmpxGeF32" => SpirvOp.FOrdGreaterThanEqual,
"VCmpNeqF32" or "VCmpxNeqF32" => SpirvOp.FUnordNotEqual,
"VCmpNlgF32" or "VCmpxNlgF32" => SpirvOp.FUnordEqual,
"VCmpNltF32" or "VCmpxNltF32" => SpirvOp.FUnordGreaterThanEqual,
"VCmpNleF32" or "VCmpxNleF32" => SpirvOp.FUnordGreaterThan,
"VCmpNgtF32" or "VCmpxNgtF32" => SpirvOp.FUnordLessThanEqual,
@@ -945,8 +870,7 @@ internal static partial class Gen5SpirvTranslator
condition = _module.AddInstruction(operation, _boolType, left, right);
}
// On gfx10, VCmpx writes EXEC only and preserves VCC; the sdst
// operand was removed from the cmpx encodings on this generation.
StoreWaveMask(106, condition);
if (opcode.StartsWith("VCmpx", StringComparison.Ordinal))
{
var active = _module.AddInstruction(
@@ -956,10 +880,6 @@ internal static partial class Gen5SpirvTranslator
condition);
StoreWaveMask(126, active);
}
else
{
StoreWaveMask(106, condition);
}
return true;
}
@@ -1189,52 +1109,6 @@ internal static partial class Gen5SpirvTranslator
left,
right);
break;
case "SMulHiU32":
{
var wideLeft = _module.AddInstruction(
SpirvOp.UConvert,
_ulongType,
left);
var wideRight = _module.AddInstruction(
SpirvOp.UConvert,
_ulongType,
right);
var product = _module.AddInstruction(
SpirvOp.IMul,
_ulongType,
wideLeft,
wideRight);
result = _module.AddInstruction(
SpirvOp.UConvert,
_uintType,
ShiftRightLogical64(
product,
_module.Constant64(_ulongType, 32)));
break;
}
case "SMulHiI32":
{
var wideLeft = _module.AddInstruction(
SpirvOp.SConvert,
_longType,
Bitcast(_intType, left));
var wideRight = _module.AddInstruction(
SpirvOp.SConvert,
_longType,
Bitcast(_intType, right));
var product = _module.AddInstruction(
SpirvOp.IMul,
_longType,
wideLeft,
wideRight);
result = _module.AddInstruction(
SpirvOp.UConvert,
_uintType,
ShiftRightLogical64(
Bitcast(_ulongType, product),
_module.Constant64(_ulongType, 32)));
break;
}
case "SAndB32":
result = BitwiseAnd(left, right);
Store(_scc, IsNotZero(result));
@@ -1356,43 +1230,6 @@ internal static partial class Gen5SpirvTranslator
Store(_scc, IsNotZero(result));
break;
}
case "SAbsdiffI32":
{
var wideLeft = _module.AddInstruction(
SpirvOp.SConvert,
_longType,
Bitcast(_intType, left));
var wideRight = _module.AddInstruction(
SpirvOp.SConvert,
_longType,
Bitcast(_intType, right));
var difference = _module.AddInstruction(
SpirvOp.ISub,
_longType,
wideLeft,
wideRight);
result = _module.AddInstruction(
SpirvOp.UConvert,
_uintType,
Ext(5, _longType, difference));
Store(_scc, IsNotZero(result));
break;
}
case "SPackLlB32B16":
result = BitwiseOr(
BitwiseAnd(left, UInt(0xFFFF)),
ShiftLeftLogical(right, UInt(16)));
break;
case "SPackLhB32B16":
result = BitwiseOr(
BitwiseAnd(left, UInt(0xFFFF)),
BitwiseAnd(right, UInt(0xFFFF0000)));
break;
case "SPackHhB32B16":
result = BitwiseOr(
ShiftRightLogical(left, UInt(16)),
BitwiseAnd(right, UInt(0xFFFF0000)));
break;
case "SCselectB32":
result = _module.AddInstruction(
SpirvOp.Select,
+46 -129
View File
@@ -18,67 +18,19 @@ internal static partial class Gen5SpirvTranslator
out string error,
int globalBufferBase = 0,
int totalGlobalBufferCount = -1,
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)
int imageBindingBase = 0)
{
if (outputs.Count > 8 || outputs.Any(output => output.GuestSlot > 7))
{
shader = default!;
error = "pixel outputs must contain at most eight guest slots in the 0..7 range";
return false;
}
if (outputs.Select(output => output.GuestSlot).Distinct().Count() != outputs.Count ||
outputs.Select(output => output.HostLocation).Distinct().Count() != outputs.Count)
{
shader = default!;
error = "pixel output guest slots and host locations must be unique";
return false;
}
if (!outputs
.OrderBy(output => output.HostLocation)
.Select((output, index) => output.HostLocation == (uint)index)
.All(isDense => isDense))
{
shader = default!;
error = "pixel output host locations must be dense in the 0..N-1 range";
return false;
}
var context = new CompilationContext(
Gen5SpirvStage.Pixel,
state,
evaluation,
outputs,
outputKind,
1,
1,
1,
globalBufferBase,
totalGlobalBufferCount,
imageBindingBase,
scalarRegisterBufferIndex);
imageBindingBase);
return context.TryCompile(out shader, out error);
}
@@ -89,21 +41,19 @@ internal static partial class Gen5SpirvTranslator
out string error,
int globalBufferBase = 0,
int totalGlobalBufferCount = -1,
int imageBindingBase = 0,
int scalarRegisterBufferIndex = -1)
int imageBindingBase = 0)
{
var context = new CompilationContext(
Gen5SpirvStage.Vertex,
state,
evaluation,
[],
Gen5PixelOutputKind.Float,
1,
1,
1,
globalBufferBase,
totalGlobalBufferCount,
imageBindingBase,
scalarRegisterBufferIndex);
imageBindingBase);
return context.TryCompile(out shader, out error);
}
@@ -120,14 +70,13 @@ internal static partial class Gen5SpirvTranslator
Gen5SpirvStage.Compute,
state,
evaluation,
[],
Gen5PixelOutputKind.Float,
Math.Max(localSizeX, 1),
Math.Max(localSizeY, 1),
Math.Max(localSizeZ, 1),
0,
-1,
0,
-1);
0);
return context.TryCompile(out shader, out error);
}
@@ -137,17 +86,15 @@ internal static partial class Gen5SpirvTranslator
private readonly Gen5SpirvStage _stage;
private readonly Gen5ShaderState _state;
private readonly Gen5ShaderEvaluation _evaluation;
private readonly IReadOnlyList<Gen5PixelOutputBinding> _pixelOutputBindings;
private readonly Gen5PixelOutputKind _outputKind;
private readonly uint _localSizeX;
private readonly uint _localSizeY;
private readonly uint _localSizeZ;
private readonly int _globalBufferBase;
private readonly int _totalGlobalBufferCount;
private readonly int _imageBindingBase;
private readonly int _scalarRegisterBufferIndex;
private readonly List<uint> _interfaces = [];
private readonly Dictionary<uint, uint> _pixelInputs = [];
private readonly Dictionary<uint, SpirvPixelOutput> _pixelOutputs = [];
private readonly Dictionary<uint, uint> _vertexOutputs = [];
private readonly Dictionary<uint, SpirvVertexInput> _vertexInputsByPc = [];
private readonly List<SpirvImageResource> _imageResources = [];
@@ -180,6 +127,7 @@ internal static partial class Gen5SpirvTranslator
private uint _lds;
private uint _workgroupUintPointer;
private uint _positionOutput;
private uint _pixelOutput;
private uint _vertexIndexInput;
private uint _instanceIndexInput;
private uint _fragCoordInput;
@@ -209,28 +157,22 @@ internal static partial class Gen5SpirvTranslator
uint Type,
uint ComponentCount);
private readonly record struct SpirvPixelOutput(
uint Variable,
uint Type,
Gen5PixelOutputKind Kind);
public CompilationContext(
Gen5SpirvStage stage,
Gen5ShaderState state,
Gen5ShaderEvaluation evaluation,
IReadOnlyList<Gen5PixelOutputBinding> pixelOutputBindings,
Gen5PixelOutputKind outputKind,
uint localSizeX,
uint localSizeY,
uint localSizeZ,
int globalBufferBase,
int totalGlobalBufferCount,
int imageBindingBase,
int scalarRegisterBufferIndex)
int imageBindingBase)
{
_stage = stage;
_state = state;
_evaluation = evaluation;
_pixelOutputBindings = pixelOutputBindings;
_outputKind = outputKind;
_localSizeX = localSizeX;
_localSizeY = localSizeY;
_localSizeZ = localSizeZ;
@@ -239,7 +181,6 @@ internal static partial class Gen5SpirvTranslator
? evaluation.GlobalMemoryBindings.Count
: totalGlobalBufferCount;
_imageBindingBase = imageBindingBase;
_scalarRegisterBufferIndex = scalarRegisterBufferIndex;
}
public bool TryCompile(out Gen5SpirvShader shader, out string error)
@@ -751,24 +692,19 @@ internal static partial class Gen5SpirvTranslator
(uint)SpirvBuiltIn.FragCoord);
_interfaces.Add(_fragCoordInput);
foreach (var binding in _pixelOutputBindings)
var outputType = _outputKind switch
{
var outputType = GetPixelOutputType(binding.Kind);
var outputPointer =
_module.TypePointer(SpirvStorageClass.Output, outputType);
var variable = _module.AddGlobalVariable(
outputPointer,
SpirvStorageClass.Output);
_module.AddName(variable, $"mrt{binding.GuestSlot}");
_module.AddDecoration(
variable,
SpirvDecoration.Location,
binding.HostLocation);
_pixelOutputs.Add(
binding.GuestSlot,
new SpirvPixelOutput(variable, outputType, binding.Kind));
_interfaces.Add(variable);
}
Gen5PixelOutputKind.Uint => _uvec4Type,
Gen5PixelOutputKind.Sint => _module.TypeVector(_intType, 4),
_ => _vec4Type,
};
var outputPointer =
_module.TypePointer(SpirvStorageClass.Output, outputType);
_pixelOutput = _module.AddGlobalVariable(
outputPointer,
SpirvStorageClass.Output);
_module.AddDecoration(_pixelOutput, SpirvDecoration.Location, 0);
_interfaces.Add(_pixelOutput);
}
else
{
@@ -831,25 +767,15 @@ internal static partial class Gen5SpirvTranslator
private void EmitInitialState()
{
if (_scalarRegisterBufferIndex >= 0)
for (uint index = 0;
index < _evaluation.InitialScalarRegisters.Count &&
index < ScalarRegisterCount;
index++)
{
for (uint index = 0; index < ScalarRegisterCount; index++)
var value = _evaluation.InitialScalarRegisters[(int)index];
if (value != 0)
{
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));
}
StoreS(index, UInt(value));
}
}
@@ -887,10 +813,7 @@ internal static partial class Gen5SpirvTranslator
1);
StoreV(2, Bitcast(_uintType, x), guardWithExec: false);
StoreV(3, Bitcast(_uintType, y), guardWithExec: false);
foreach (var output in _pixelOutputs.Values)
{
Store(output.Variable, _module.ConstantNull(output.Type));
}
Store(_pixelOutput, _module.ConstantNull(GetPixelOutputType()));
}
else
{
@@ -2143,27 +2066,21 @@ internal static partial class Gen5SpirvTranslator
if (_stage == Gen5SpirvStage.Pixel)
{
if (!_pixelOutputs.TryGetValue(export.Target, out var output))
if (export.Target != 0)
{
return true;
}
var outputType = GetPixelOutputType();
var values = new uint[4];
for (var component = 0; component < 4; component++)
{
var enabled = (export.EnableMask & (1u << component)) != 0;
if (!enabled)
{
values[component] = _module.AddInstruction(
SpirvOp.CompositeExtract,
output.Kind switch
{
Gen5PixelOutputKind.Uint => _uintType,
Gen5PixelOutputKind.Sint => _intType,
_ => _floatType,
},
Load(output.Type, output.Variable),
(uint)component);
values[component] = _outputKind == Gen5PixelOutputKind.Sint
? Bitcast(_intType, UInt(0))
: UInt(0);
continue;
}
@@ -2172,7 +2089,7 @@ internal static partial class Gen5SpirvTranslator
var value = LoadCompressedExportComponent(
instruction,
component);
values[component] = output.Kind switch
values[component] = _outputKind switch
{
Gen5PixelOutputKind.Uint => _module.AddInstruction(
SpirvOp.ConvertFToU,
@@ -2188,7 +2105,7 @@ internal static partial class Gen5SpirvTranslator
}
var raw = LoadV(instruction.Sources[component].Value);
values[component] = output.Kind switch
values[component] = _outputKind switch
{
Gen5PixelOutputKind.Uint => raw,
Gen5PixelOutputKind.Sint => Bitcast(_intType, raw),
@@ -2198,15 +2115,15 @@ internal static partial class Gen5SpirvTranslator
var vector = _module.AddInstruction(
SpirvOp.CompositeConstruct,
output.Type,
outputType,
values);
vector = _module.AddInstruction(
SpirvOp.Select,
output.Type,
outputType,
Load(_boolType, _exec),
vector,
Load(output.Type, output.Variable));
Store(output.Variable, vector);
Load(outputType, _pixelOutput));
Store(_pixelOutput, vector);
return true;
}
@@ -2274,8 +2191,8 @@ internal static partial class Gen5SpirvTranslator
(uint)(component & 1));
}
private uint GetPixelOutputType(Gen5PixelOutputKind kind) =>
kind switch
private uint GetPixelOutputType() =>
_outputKind switch
{
Gen5PixelOutputKind.Uint => _uvec4Type,
Gen5PixelOutputKind.Sint => _module.TypeVector(_intType, 4),
-37
View File
@@ -1,37 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Ajm;
public static class AjmExports
{
private const int OrbisAjmErrorInvalidParameter = unchecked((int)0x80930005);
private static int _nextContextId;
[SysAbiExport(
Nid = "dl+4eHSzUu4",
ExportName = "sceAjmInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAjm")]
public static int Initialize(CpuContext ctx)
{
var reserved = ctx[CpuRegister.Rdi];
var outContextAddress = ctx[CpuRegister.Rsi];
// AJM requires a zero reserved argument.
if (reserved != 0 || outContextAddress == 0)
{
return ctx.SetReturn(OrbisAjmErrorInvalidParameter);
}
var contextId = unchecked((uint)Interlocked.Increment(ref _nextContextId));
if (!ctx.TryWriteUInt32(outContextAddress, contextId))
{
return ctx.SetReturn(OrbisAjmErrorInvalidParameter);
}
return ctx.SetReturn(0);
}
}
-46
View File
@@ -348,18 +348,6 @@ public static class AmprExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "Zi3dBUjgyXI",
ExportName = "sceAmprMeasureCommandSizeWriteKernelEventQueueOnCompletion",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int MeasureCommandSizeWriteKernelEventQueueOnCompletion(CpuContext ctx)
{
TraceAmpr(ctx, "measure_write_equeue_complete", 0, KernelEventQueueRecordSize, 0);
ctx[CpuRegister.Rax] = KernelEventQueueRecordSize;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "C+IEj+BsAFM",
ExportName = "sceAmprMeasureCommandSizeWriteAddressOnCompletion",
@@ -452,40 +440,6 @@ public static class AmprExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "o67gODLFpls",
ExportName = "sceAmprCommandBufferWriteKernelEventQueueOnCompletion",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int CommandBufferWriteKernelEventQueueOnCompletion(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
var equeue = ctx[CpuRegister.Rsi];
var ident = ctx[CpuRegister.Rdx];
var completionToken = ctx[CpuRegister.Rcx];
var userData = ctx[CpuRegister.R8];
if (commandBuffer == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!AppendKernelEventQueueRecord(
ctx,
commandBuffer,
equeue,
ident,
completionToken,
userData))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceAmpr(ctx, "write_equeue_complete", commandBuffer, ident, completionToken);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "sJXyWHjP-F8",
ExportName = "sceAmprCommandBufferWriteAddressOnCompletion",
+9 -30
View File
@@ -54,42 +54,21 @@ internal static class PakDirectoryTracker
if (state.Directory is { } entries)
{
// Several unrelated archived files can share a byte count (e.g. a head model and a bot
// navigation file both 0x3A34 bytes). Directory order alone then picks whichever comes
// first, which is wrong when the game reads them out of order. id archives cluster
// related assets, and the guest streams them with locality, so disambiguate collisions
// by choosing the unconsumed match nearest the running read cursor.
DirectoryEntry? best = null;
var bestDistance = ulong.MaxValue;
foreach (var entry in entries)
{
if (entry.Consumed || entry.FileLen != requestedSize)
if (!entry.Consumed && entry.FileLen == requestedSize)
{
continue;
}
entry.Consumed = true;
if (_trace)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] pak.directory_match: id=0x{fileId:X8} name='{entry.Name}' " +
$"filepos=0x{entry.FilePos:X8} filelen=0x{entry.FileLen:X8}");
}
var distance = entry.FilePos >= state.NextOffset
? entry.FilePos - state.NextOffset
: state.NextOffset - entry.FilePos;
if (distance < bestDistance)
{
bestDistance = distance;
best = entry;
return entry.FilePos;
}
}
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;
}
}
return state.NextOffset;
+1 -1
View File
@@ -202,7 +202,7 @@ public static class AudioOut2Exports
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var outUserAddress = ctx[CpuRegister.Rsi];
if ((userId != 0 && userId != 1 && userId != 1000 && userId != 255) || outUserAddress == 0)
if ((userId != 0 && userId != 1 && userId != 255) || outUserAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
+10 -39
View File
@@ -2,7 +2,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.HLE.Host;
using System.Buffers;
using System.Collections.Concurrent;
using System.Diagnostics;
@@ -28,7 +27,7 @@ public static class AudioOutExports
int channels,
int bytesPerSample,
bool isFloat,
IHostAudioStream? backend)
WinMmAudioPort? backend)
{
UserId = userId;
Type = type;
@@ -49,7 +48,7 @@ public static class AudioOutExports
public int Channels { get; }
public int BytesPerSample { get; }
public bool IsFloat { get; }
public IHostAudioStream? Backend { get; }
public WinMmAudioPort? Backend { get; }
public int BufferByteLength =>
checked((int)BufferLength * Channels * BytesPerSample);
@@ -104,13 +103,12 @@ public static class AudioOutExports
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
IHostAudioStream? backend = null;
WinMmAudioPort? backend = null;
string backendName;
try
{
var audio = HostPlatform.Current.Audio;
backend = audio.OpenStereoPcm16Stream(frequency);
backendName = audio.BackendName;
backend = new WinMmAudioPort(frequency);
backendName = "winmm";
}
catch (Exception exception)
{
@@ -182,31 +180,15 @@ public static class AudioOutExports
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (port.Backend is null)
{
port.PaceSilence();
return ctx.SetReturn(0);
}
var outputLength = checked((int)port.BufferLength * AudioPcmConversion.OutputFrameSize);
var output = ArrayPool<byte>.Shared.Rent(outputLength);
try
{
AudioPcmConversion.ConvertToStereoPcm16(
if (port.Backend is null ||
!port.Backend.Submit(
source,
output.AsSpan(0, outputLength),
checked((int)port.BufferLength),
port.BufferLength,
port.Channels,
port.BytesPerSample,
port.IsFloat);
if (!port.Backend.Submit(output.AsSpan(0, outputLength)))
{
port.PaceSilence();
}
}
finally
port.IsFloat))
{
ArrayPool<byte>.Shared.Return(output);
port.PaceSilence();
}
return ctx.SetReturn(0);
@@ -231,17 +213,6 @@ public static class AudioOutExports
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
public static void ShutdownAllPorts()
{
foreach (var handle in Ports.Keys)
{
if (Ports.TryRemove(handle, out var port))
{
port.Dispose();
}
}
}
private static bool TryGetFormat(
int rawFormat,
out int channels,
@@ -1,55 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
namespace SharpEmu.Libs.Audio;
/// <summary>
/// Converts guest AudioOut submissions (mono/stereo/7.1, s16 or float32) into the
/// interleaved stereo 16-bit PCM that host audio streams accept. Platform-neutral —
/// device specifics live behind IHostAudioStream.
/// </summary>
internal static class AudioPcmConversion
{
/// <summary>Bytes per output frame: two 16-bit channels.</summary>
public const int OutputFrameSize = 4;
public static void ConvertToStereoPcm16(
ReadOnlySpan<byte> source,
Span<byte> destination,
int frames,
int channels,
int bytesPerSample,
bool isFloat)
{
var sourceFrameSize = checked(channels * bytesPerSample);
for (var frame = 0; frame < frames; frame++)
{
var sourceFrame = source.Slice(frame * sourceFrameSize, sourceFrameSize);
var left = ReadSample(sourceFrame, 0, bytesPerSample, isFloat);
var right = channels == 1
? left
: ReadSample(sourceFrame, 1, bytesPerSample, isFloat);
BinaryPrimitives.WriteInt16LittleEndian(destination[(frame * OutputFrameSize)..], left);
BinaryPrimitives.WriteInt16LittleEndian(destination[((frame * OutputFrameSize) + 2)..], right);
}
}
private static short ReadSample(
ReadOnlySpan<byte> frame,
int channel,
int bytesPerSample,
bool isFloat)
{
var sample = frame.Slice(channel * bytesPerSample, bytesPerSample);
if (!isFloat)
{
return BinaryPrimitives.ReadInt16LittleEndian(sample);
}
var bits = BinaryPrimitives.ReadInt32LittleEndian(sample);
var value = Math.Clamp(BitConverter.Int32BitsToSingle(bits), -1.0f, 1.0f);
return checked((short)MathF.Round(value * short.MaxValue));
}
}
+302
View File
@@ -0,0 +1,302 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers;
using System.Buffers.Binary;
using System.Runtime.InteropServices;
namespace SharpEmu.Libs.Audio;
internal sealed class WinMmAudioPort : IDisposable
{
private const uint WaveMapper = uint.MaxValue;
private const uint CallbackEvent = 0x0005_0000;
private const ushort WaveFormatPcm = 1;
private const uint WaveHeaderDone = 0x0000_0001;
private const int MaximumQueuedPcmBytes = 32 * 1024;
private readonly object _gate = new();
private readonly AutoResetEvent _completion = new(false);
private readonly Queue<NativeBuffer> _buffers = new();
private IntPtr _device;
private int _queuedPcmBytes;
private bool _disposed;
public WinMmAudioPort(uint sampleRate)
{
if (!OperatingSystem.IsWindows())
{
throw new PlatformNotSupportedException("WinMM audio is only available on Windows.");
}
var format = new WaveFormat
{
FormatTag = WaveFormatPcm,
Channels = 2,
SamplesPerSecond = sampleRate,
AverageBytesPerSecond = checked(sampleRate * 4),
BlockAlign = 4,
BitsPerSample = 16,
ExtraSize = 0,
};
var result = WaveOutOpen(
out _device,
WaveMapper,
ref format,
_completion.SafeWaitHandle.DangerousGetHandle(),
IntPtr.Zero,
CallbackEvent);
if (result != 0)
{
throw new InvalidOperationException($"waveOutOpen failed with MMRESULT {result}.");
}
}
public bool Submit(
ReadOnlySpan<byte> source,
uint frames,
int channels,
int bytesPerSample,
bool isFloat)
{
lock (_gate)
{
if (_disposed)
{
return false;
}
var outputLength = checked((int)frames * 4);
ReapCompletedBuffers();
while (_queuedPcmBytes != 0 &&
_queuedPcmBytes + outputLength > MaximumQueuedPcmBytes)
{
if (!_completion.WaitOne(TimeSpan.FromSeconds(1)))
{
return false;
}
ReapCompletedBuffers();
}
var output = ArrayPool<byte>.Shared.Rent(outputLength);
try
{
ConvertToStereoPcm16(
source,
output.AsSpan(0, outputLength),
checked((int)frames),
channels,
bytesPerSample,
isFloat);
return QueueBuffer(output.AsSpan(0, outputLength));
}
finally
{
ArrayPool<byte>.Shared.Return(output);
}
}
}
public void Dispose()
{
lock (_gate)
{
if (_disposed)
{
return;
}
_disposed = true;
if (_device != IntPtr.Zero)
{
WaveOutReset(_device);
while (_buffers.TryDequeue(out var buffer))
{
ReleaseBuffer(buffer);
}
WaveOutClose(_device);
_device = IntPtr.Zero;
}
_completion.Dispose();
}
}
private bool QueueBuffer(ReadOnlySpan<byte> data)
{
var dataAddress = Marshal.AllocHGlobal(data.Length);
var headerAddress = IntPtr.Zero;
try
{
unsafe
{
data.CopyTo(new Span<byte>((void*)dataAddress, data.Length));
}
var header = new WaveHeader
{
Data = dataAddress,
BufferLength = checked((uint)data.Length),
};
headerAddress = Marshal.AllocHGlobal(Marshal.SizeOf<WaveHeader>());
Marshal.StructureToPtr(header, headerAddress, false);
var result = WaveOutPrepareHeader(
_device,
headerAddress,
checked((uint)Marshal.SizeOf<WaveHeader>()));
if (result != 0)
{
return false;
}
result = WaveOutWrite(
_device,
headerAddress,
checked((uint)Marshal.SizeOf<WaveHeader>()));
if (result != 0)
{
WaveOutUnprepareHeader(
_device,
headerAddress,
checked((uint)Marshal.SizeOf<WaveHeader>()));
return false;
}
_buffers.Enqueue(new NativeBuffer(dataAddress, headerAddress, data.Length));
_queuedPcmBytes += data.Length;
dataAddress = IntPtr.Zero;
headerAddress = IntPtr.Zero;
return true;
}
finally
{
if (headerAddress != IntPtr.Zero)
{
Marshal.FreeHGlobal(headerAddress);
}
if (dataAddress != IntPtr.Zero)
{
Marshal.FreeHGlobal(dataAddress);
}
}
}
private void ReapCompletedBuffers()
{
while (_buffers.TryPeek(out var buffer))
{
var header = Marshal.PtrToStructure<WaveHeader>(buffer.Header);
if ((header.Flags & WaveHeaderDone) == 0)
{
return;
}
_buffers.Dequeue();
ReleaseBuffer(buffer);
}
}
private void ReleaseBuffer(NativeBuffer buffer)
{
WaveOutUnprepareHeader(
_device,
buffer.Header,
checked((uint)Marshal.SizeOf<WaveHeader>()));
_queuedPcmBytes -= buffer.Length;
Marshal.FreeHGlobal(buffer.Header);
Marshal.FreeHGlobal(buffer.Data);
}
private static void ConvertToStereoPcm16(
ReadOnlySpan<byte> source,
Span<byte> destination,
int frames,
int channels,
int bytesPerSample,
bool isFloat)
{
var sourceFrameSize = checked(channels * bytesPerSample);
for (var frame = 0; frame < frames; frame++)
{
var sourceFrame = source.Slice(frame * sourceFrameSize, sourceFrameSize);
var left = ReadSample(sourceFrame, 0, bytesPerSample, isFloat);
var right = channels == 1
? left
: ReadSample(sourceFrame, 1, bytesPerSample, isFloat);
BinaryPrimitives.WriteInt16LittleEndian(destination[(frame * 4)..], left);
BinaryPrimitives.WriteInt16LittleEndian(destination[((frame * 4) + 2)..], right);
}
}
private static short ReadSample(
ReadOnlySpan<byte> frame,
int channel,
int bytesPerSample,
bool isFloat)
{
var sample = frame.Slice(channel * bytesPerSample, bytesPerSample);
if (!isFloat)
{
return BinaryPrimitives.ReadInt16LittleEndian(sample);
}
var bits = BinaryPrimitives.ReadInt32LittleEndian(sample);
var value = Math.Clamp(BitConverter.Int32BitsToSingle(bits), -1.0f, 1.0f);
return checked((short)MathF.Round(value * short.MaxValue));
}
private readonly record struct NativeBuffer(IntPtr Data, IntPtr Header, int Length);
[StructLayout(LayoutKind.Sequential, Pack = 2)]
private struct WaveFormat
{
public ushort FormatTag;
public ushort Channels;
public uint SamplesPerSecond;
public uint AverageBytesPerSecond;
public ushort BlockAlign;
public ushort BitsPerSample;
public ushort ExtraSize;
}
[StructLayout(LayoutKind.Sequential)]
private struct WaveHeader
{
public IntPtr Data;
public uint BufferLength;
public uint BytesRecorded;
public nuint User;
public uint Flags;
public uint Loops;
public IntPtr Next;
public nuint Reserved;
}
[DllImport("winmm.dll", EntryPoint = "waveOutOpen")]
private static extern uint WaveOutOpen(
out IntPtr device,
uint deviceId,
ref WaveFormat format,
IntPtr callback,
IntPtr instance,
uint flags);
[DllImport("winmm.dll", EntryPoint = "waveOutPrepareHeader")]
private static extern uint WaveOutPrepareHeader(IntPtr device, IntPtr header, uint headerSize);
[DllImport("winmm.dll", EntryPoint = "waveOutWrite")]
private static extern uint WaveOutWrite(IntPtr device, IntPtr header, uint headerSize);
[DllImport("winmm.dll", EntryPoint = "waveOutUnprepareHeader")]
private static extern uint WaveOutUnprepareHeader(IntPtr device, IntPtr header, uint headerSize);
[DllImport("winmm.dll", EntryPoint = "waveOutReset")]
private static extern uint WaveOutReset(IntPtr device);
[DllImport("winmm.dll", EntryPoint = "waveOutClose")]
private static extern uint WaveOutClose(IntPtr device);
}
@@ -35,17 +35,6 @@ public static class AvPlayerExports
return unchecked((int)handle);
}
[SysAbiExport(
Nid = "KMcEa+rHsIo",
ExportName = "sceAvPlayerAddSource",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAvPlayer")]
public static int AvPlayerAddSource(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "JdksQu8pNdQ",
ExportName = "sceAvPlayerGetVideoDataEx",
@@ -2,7 +2,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Threading;
namespace SharpEmu.Libs.CommonDialog;
@@ -11,21 +10,10 @@ public static class MsgDialogExports
{
private const int StatusNone = 0;
private const int StatusInitialized = 1;
private const int StatusRunning = 2;
private const int StatusFinished = 3;
private const int 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 _initialized;
private static int _status;
[SysAbiExport(
@@ -36,10 +24,11 @@ public static class MsgDialogExports
public static int MsgDialogInitialize(CpuContext ctx)
{
// Treat repeated initialization as success. The dialog service is process-global in
// this HLE implementation and has no per-call resources to recreate. Only promote
// from NONE so re-initializing mid-flow cannot clobber a running/finished dialog.
Interlocked.CompareExchange(ref _status, StatusInitialized, StatusNone);
return ctx.SetReturn(ErrorOk);
// this HLE implementation and has no per-call resources to recreate.
Interlocked.Exchange(ref _initialized, 1);
Interlocked.Exchange(ref _status, StatusInitialized);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
@@ -49,12 +38,9 @@ public static class MsgDialogExports
LibraryName = "libSceMsgDialog")]
public static int MsgDialogTerminate(CpuContext ctx)
{
if (Interlocked.Exchange(ref _status, StatusNone) == StatusNone)
{
return ctx.SetReturn(ErrorNotInitialized);
}
return ctx.SetReturn(ErrorOk);
Interlocked.Exchange(ref _initialized, 0);
Interlocked.Exchange(ref _status, StatusNone);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -64,29 +50,13 @@ public static class MsgDialogExports
LibraryName = "libSceMsgDialog")]
public static int MsgDialogOpen(CpuContext ctx)
{
var paramAddress = ctx[CpuRegister.Rdi];
if (paramAddress == 0)
{
return ctx.SetReturn(ErrorArgNull);
}
LogDialogMessage(ctx, ctx[CpuRegister.Rdi]);
var status = Volatile.Read(ref _status);
if (status == StatusNone)
{
return ctx.SetReturn(ErrorNotInitialized);
}
if (status == StatusRunning)
{
return ctx.SetReturn(ErrorBusy);
}
LogDialogMessage(ctx, paramAddress);
// There is no host popup to actually show. Enter RUNNING so close/cancel paths see
// a live dialog; the guest's next status poll auto-dismisses it (see PollStatus).
Interlocked.Exchange(ref _status, StatusRunning);
return ctx.SetReturn(ErrorOk);
// There is no host popup to actually show. Complete immediately with "finished" so a
// guest polling loop (GetStatus/UpdateStatus -> GetResult -> Close) sees a dismissed
// dialog on its very first poll instead of spinning forever waiting for user input.
Interlocked.Exchange(ref _status, StatusFinished);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
}
// Best-effort extraction of the dialog text so fatal-error popups are visible in the
@@ -95,6 +65,12 @@ public static class MsgDialogExports
// level deep, then a second level for nested sub-param structs.
private static void LogDialogMessage(CpuContext ctx, ulong paramAddress)
{
if (paramAddress == 0)
{
Console.Error.WriteLine("[LOADER][INFO] sceMsgDialogOpen: param=NULL");
return;
}
Console.Error.WriteLine($"[LOADER][INFO] sceMsgDialogOpen: param=0x{paramAddress:X12}");
Span<byte> head = stackalloc byte[0xA0];
@@ -158,24 +134,14 @@ public static class MsgDialogExports
ExportName = "sceMsgDialogGetStatus",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceMsgDialog")]
public static int MsgDialogGetStatus(CpuContext ctx) => ctx.SetReturn(PollStatus());
public static int MsgDialogGetStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
[SysAbiExport(
Nid = "6fIC3XKt2k0",
ExportName = "sceMsgDialogUpdateStatus",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceMsgDialog")]
public static int MsgDialogUpdateStatus(CpuContext ctx) => 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);
}
public static int MsgDialogUpdateStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
[SysAbiExport(
Nid = "Lr8ovHH9l6A",
@@ -187,27 +153,17 @@ public static class MsgDialogExports
var resultAddress = ctx[CpuRegister.Rdi];
if (resultAddress == 0)
{
return ctx.SetReturn(ErrorArgNull);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (Volatile.Read(ref _status) != StatusFinished)
{
return ctx.SetReturn(ErrorNotFinished);
}
// Report the affirmative button so yes/no prompts take the confirming branch;
// buttonId 0 is the "invalid" sentinel and games may treat it as an error.
Span<byte> result = stackalloc byte[ResultSize];
result.Clear();
BinaryPrimitives.WriteInt32LittleEndian(result[0x04..], 0);
BinaryPrimitives.WriteInt32LittleEndian(result[0x08..], ButtonIdAffirmative);
if (!ctx.Memory.TryWrite(resultAddress, result))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
return ctx.SetReturn(ErrorOk);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -217,44 +173,13 @@ public static class MsgDialogExports
LibraryName = "libSceMsgDialog")]
public static int MsgDialogClose(CpuContext ctx)
{
if (Interlocked.CompareExchange(ref _status, StatusFinished, StatusRunning) != StatusRunning)
{
return ctx.SetReturn(ErrorNotRunning);
}
return ctx.SetReturn(ErrorOk);
Interlocked.Exchange(ref _status, StatusFinished);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
}
[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)
private static int SetReturn(CpuContext ctx, int result)
{
if (Volatile.Read(ref _status) == StatusNone)
{
return ctx.SetReturn(ErrorNotInitialized);
}
return ctx.SetReturn(ErrorOk);
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
}
-76
View File
@@ -78,82 +78,6 @@ public static class JsonExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// sce::Json::Initializer::setGlobalNullAccessCallback(const Value& (*)(ValueType, const Value*, void*), void*)
// Registers the guest hook invoked when a Value is accessed as the wrong type. Quake calls it
// during kexPSNWebAPI::Initialize and treats a non-zero return as a fatal init failure.
[SysAbiExport(
Nid = "+drDFyAS6u4",
ExportName = "_ZN3sce4Json11Initializer27setGlobalNullAccessCallbackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int InitializerSetGlobalNullAccessCallback(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
if (thisAddress == 0)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
JsonObjectHeap.GlobalNullAccessCallback = ctx[CpuRegister.Rsi];
JsonObjectHeap.GlobalNullAccessCallbackContext = ctx[CpuRegister.Rdx];
TraceJson("Initializer.setGlobalNullAccessCallback", thisAddress, ctx[CpuRegister.Rsi]);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "WSOuge5IsCg",
ExportName = "_ZN3sce4Json14InitParameter2C1Ev",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson2")]
public static int InitParameter2Constructor(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
TraceJson("InitParameter2.ctor", thisAddress, ctx[CpuRegister.Rsi]);
ctx[CpuRegister.Rax] = thisAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "I2QC8PYhJWY",
ExportName = "_ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson2")]
public static int InitParameter2SetAllocator(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
TraceJson("InitParameter2.setAllocator", thisAddress, ctx[CpuRegister.Rsi]);
ctx[CpuRegister.Rax] = thisAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "Eu95jmqn5Rw",
ExportName = "_ZN3sce4Json14InitParameter217setFileBufferSizeEm",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson2")]
public static int InitParameter2SetFileBufferSize(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
TraceJson("InitParameter2.setFileBufferSize", thisAddress, ctx[CpuRegister.Rsi]);
ctx[CpuRegister.Rax] = thisAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "IXW-z8pggfg",
ExportName = "_ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson2")]
public static int Initializer2Constructor(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
TraceJson("Initializer2.ctor", thisAddress, ctx[CpuRegister.Rsi]);
ctx[CpuRegister.Rax] = thisAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static void TraceJson(string operation, ulong thisAddress, ulong argument)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_JSON"), "1", StringComparison.Ordinal))
-234
View File
@@ -1,234 +0,0 @@
// 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);
}
}
-110
View File
@@ -1,110 +0,0 @@
// 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;
}
}

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