mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-26 20:50:53 +08:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 373100a6b0 | |||
| f23161be9a | |||
| 081760be3f | |||
| e604fb606d | |||
| df53ff59d9 | |||
| 4c35831cb8 | |||
| 90fdd20f9a | |||
| ae5ef0abe7 | |||
| 5e2c21edf1 | |||
| 3fb9d4db1c | |||
| de13735972 | |||
| fc0efca297 | |||
| 2a9a261913 | |||
| 290f5fd3d7 | |||
| c06c70cad7 | |||
| 5e54250752 |
@@ -100,6 +100,9 @@ jobs:
|
|||||||
- name: Build solution
|
- name: Build solution
|
||||||
run: dotnet build SharpEmu.slnx -c Release --no-restore
|
run: dotnet build SharpEmu.slnx -c Release --no-restore
|
||||||
|
|
||||||
|
- name: Validate synthetic shaders
|
||||||
|
run: dotnet run --project tools/SharpEmu.Tools.ShaderDump/SharpEmu.Tools.ShaderDump.csproj -c Release -- artifacts/shader-dump
|
||||||
|
|
||||||
- name: Publish win-x64 CLI
|
- name: Publish win-x64 CLI
|
||||||
run: dotnet publish src/SharpEmu.CLI/SharpEmu.CLI.csproj -c Release -r win-x64 --self-contained true --no-restore -p:PublishDir="${env:PUBLISH_DIR}"
|
run: dotnet publish src/SharpEmu.CLI/SharpEmu.CLI.csproj -c Release -r win-x64 --self-contained true --no-restore -p:PublishDir="${env:PUBLISH_DIR}"
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,14 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.3.18" />
|
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.3.18" />
|
||||||
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.3.18" />
|
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.3.18" />
|
||||||
<PackageVersion Include="Iced" Version="1.21.0" />
|
<PackageVersion Include="Iced" Version="1.21.0" />
|
||||||
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
<PackageVersion Include="Silk.NET.Vulkan" Version="2.23.0" />
|
<PackageVersion Include="Silk.NET.Vulkan" Version="2.23.0" />
|
||||||
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.23.0" />
|
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.23.0" />
|
||||||
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.23.0" />
|
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.23.0" />
|
||||||
<PackageVersion Include="Silk.NET.Windowing" Version="2.23.0" />
|
<PackageVersion Include="Silk.NET.Windowing" Version="2.23.0" />
|
||||||
<!-- Transitive of Avalonia.Desktop; pinned to fix GHSA-xrw6-gwf8-vvr9 -->
|
<!-- Transitive of Avalonia.Desktop; pinned to fix GHSA-xrw6-gwf8-vvr9 -->
|
||||||
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.21.3" />
|
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.21.3" />
|
||||||
|
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||||
|
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -12,4 +12,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<Project Path="src/SharpEmu.Libs/SharpEmu.Libs.csproj" />
|
<Project Path="src/SharpEmu.Libs/SharpEmu.Libs.csproj" />
|
||||||
<Project Path="src/SharpEmu.Logging/SharpEmu.Logging.csproj" />
|
<Project Path="src/SharpEmu.Logging/SharpEmu.Logging.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
|
<Folder Name="/tests/">
|
||||||
|
<Project Path="tests/SharpEmu.Libs.Tests/SharpEmu.Libs.Tests.csproj" />
|
||||||
|
</Folder>
|
||||||
</Solution>
|
</Solution>
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
# Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
"""Offline check: SysAbiExport ExportName must hash to its Nid (name2nid).
|
||||||
|
|
||||||
|
NIDs absent from aerolib.bin are skipped (unknown/unresolved symbols).
|
||||||
|
Known historic mislabels may be allowlisted with a one-line reason.
|
||||||
|
|
||||||
|
Run from the repository root:
|
||||||
|
python scripts/check_sysabi_aerolib.py
|
||||||
|
python scripts/check_sysabi_aerolib.py --strict
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import re
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
from base64 import b64encode as base64enc
|
||||||
|
from binascii import unhexlify as uhx
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SRC_ROOT = Path("src")
|
||||||
|
AEROLIB_BIN = Path("src/SharpEmu.HLE/Aerolib/aerolib.bin")
|
||||||
|
SYSABI_EXPORT_RE = re.compile(r"\[SysAbiExport\((.*?)\)\]", re.DOTALL)
|
||||||
|
NID_RE = re.compile(r'Nid\s*=\s*"([^"]+)"')
|
||||||
|
EXPORT_NAME_RE = re.compile(r'ExportName\s*=\s*"([^"]+)"')
|
||||||
|
|
||||||
|
# NID -> reason. Keep minimal; fix ExportName when safe instead of growing this list.
|
||||||
|
ALLOWLISTED_NIDS: dict[str, str] = {
|
||||||
|
"KMcEa+rHsIo": "Historic kernel MapMemory stub bound to sceAvPlayerAddSource NID; API rewrite deferred.",
|
||||||
|
"WV1GwM32NgY": "Historic WebApi2 init alias for PushEventCreateHandle NID; ABI rewrite deferred.",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def name2nid(name: str) -> str:
|
||||||
|
symbol = hashlib.sha1(name.encode() + uhx("518D64A635DED8C1E6B039B1C3E55230")).digest()
|
||||||
|
id_val = struct.unpack("<Q", symbol[:8])[0]
|
||||||
|
nid = base64enc(uhx("%016x" % id_val), b"+-").rstrip(b"=")
|
||||||
|
return nid.decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def find_repo_root() -> Path:
|
||||||
|
cwd = Path.cwd()
|
||||||
|
if (cwd / SRC_ROOT).is_dir() and (cwd / "scripts").is_dir():
|
||||||
|
return cwd
|
||||||
|
script_root = Path(__file__).resolve().parent.parent
|
||||||
|
if (script_root / SRC_ROOT).is_dir():
|
||||||
|
return script_root
|
||||||
|
raise SystemExit("Run from the repository root (src/ and scripts/ expected).")
|
||||||
|
|
||||||
|
|
||||||
|
def load_aerolib_nids(aerolib_path: Path) -> set[str]:
|
||||||
|
data = aerolib_path.read_bytes()
|
||||||
|
if len(data) < 4:
|
||||||
|
raise SystemExit(f"Aerolib binary too small: {aerolib_path}")
|
||||||
|
|
||||||
|
count = struct.unpack_from("<I", data, 0)[0]
|
||||||
|
offset = 4
|
||||||
|
nids: set[str] = set()
|
||||||
|
for _ in range(count):
|
||||||
|
if offset >= len(data):
|
||||||
|
raise SystemExit(f"Truncated aerolib.bin while reading NIDs: {aerolib_path}")
|
||||||
|
nid_len = data[offset]
|
||||||
|
offset += 1
|
||||||
|
nid = data[offset : offset + nid_len].decode("utf-8")
|
||||||
|
offset += nid_len
|
||||||
|
if offset + 2 > len(data):
|
||||||
|
raise SystemExit(f"Truncated aerolib.bin name length: {aerolib_path}")
|
||||||
|
name_len = struct.unpack_from("<H", data, offset)[0]
|
||||||
|
offset += 2 + name_len
|
||||||
|
nids.add(nid)
|
||||||
|
return nids
|
||||||
|
|
||||||
|
|
||||||
|
def iter_sysabi_exports(cs_path: Path, text: str):
|
||||||
|
for match in SYSABI_EXPORT_RE.finditer(text):
|
||||||
|
block = match.group(1)
|
||||||
|
nid_match = NID_RE.search(block)
|
||||||
|
export_match = EXPORT_NAME_RE.search(block)
|
||||||
|
if nid_match is None or export_match is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
nid = nid_match.group(1)
|
||||||
|
export_name = export_match.group(1)
|
||||||
|
nid_attr = f'Nid = "{nid}"'
|
||||||
|
abs_pos = text.find(nid_attr, match.start(), match.end())
|
||||||
|
if abs_pos < 0:
|
||||||
|
abs_pos = match.start()
|
||||||
|
line = text.count("\n", 0, abs_pos) + 1
|
||||||
|
yield cs_path, line, nid, export_name
|
||||||
|
|
||||||
|
|
||||||
|
def scan(src_root: Path, catalog_nids: set[str]):
|
||||||
|
checked = 0
|
||||||
|
mismatches = []
|
||||||
|
skipped_no_catalog = 0
|
||||||
|
allowlisted = 0
|
||||||
|
|
||||||
|
for cs_path in sorted(src_root.rglob("*.cs")):
|
||||||
|
text = cs_path.read_text(encoding="utf-8")
|
||||||
|
for path, line, nid, export_name in iter_sysabi_exports(cs_path, text):
|
||||||
|
checked += 1
|
||||||
|
computed = name2nid(export_name)
|
||||||
|
if computed == nid:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if nid not in catalog_nids:
|
||||||
|
skipped_no_catalog += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if nid in ALLOWLISTED_NIDS:
|
||||||
|
allowlisted += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
mismatches.append((path, line, nid, export_name, computed))
|
||||||
|
|
||||||
|
return checked, mismatches, skipped_no_catalog, allowlisted
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Check that SysAbiExport ExportName values hash to their Nid via name2nid."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--strict",
|
||||||
|
action="store_true",
|
||||||
|
help="Exit 1 when any non-skipped/non-allowlisted ExportName does not hash to its Nid.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--quiet",
|
||||||
|
action="store_true",
|
||||||
|
help="Print only the summary line.",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
repo_root = find_repo_root()
|
||||||
|
aerolib_path = repo_root / AEROLIB_BIN
|
||||||
|
if not aerolib_path.is_file():
|
||||||
|
raise SystemExit(f"Missing Aerolib catalog: {aerolib_path.as_posix()}")
|
||||||
|
|
||||||
|
catalog_nids = load_aerolib_nids(aerolib_path)
|
||||||
|
checked, mismatches, skipped_no_catalog, allowlisted = scan(
|
||||||
|
repo_root / SRC_ROOT, catalog_nids
|
||||||
|
)
|
||||||
|
ok = checked - len(mismatches) - skipped_no_catalog - allowlisted
|
||||||
|
|
||||||
|
if not args.quiet:
|
||||||
|
for path, line, nid, export_name, computed in mismatches:
|
||||||
|
rel = path.relative_to(repo_root).as_posix()
|
||||||
|
print(
|
||||||
|
f"{rel}:{line}: NID={nid} ExportName={export_name!r} "
|
||||||
|
f"computed={computed}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"checked={checked} ok={ok} fail={len(mismatches)} "
|
||||||
|
f"skipped_no_catalog={skipped_no_catalog} allowlisted={allowlisted} "
|
||||||
|
f"allowlist_size={len(ALLOWLISTED_NIDS)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if args.strict and mismatches:
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -154449,3 +154449,9 @@ vector_str_substr
|
|||||||
WTFAnnotateBenignRaceSized
|
WTFAnnotateBenignRaceSized
|
||||||
WTFAnnotateHappensAfter
|
WTFAnnotateHappensAfter
|
||||||
WTFAnnotateHappensBefore
|
WTFAnnotateHappensBefore
|
||||||
|
_sceUlobjmgrRegisterObject
|
||||||
|
_sceUlobjmgrUnregisterObject
|
||||||
|
sceNpEAAccessInitialize
|
||||||
|
sceNpEAAccessTerminate
|
||||||
|
sceNpHasEAAccessSubscription
|
||||||
|
sceNpHasEAAccessSubscriptionAbortRequest
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using SharpEmu.Core.Cpu.Native;
|
|||||||
using SharpEmu.Core.Loader;
|
using SharpEmu.Core.Loader;
|
||||||
using SharpEmu.Core.Memory;
|
using SharpEmu.Core.Memory;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
using SharpEmu.Logging;
|
using SharpEmu.Logging;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu;
|
namespace SharpEmu.Core.Cpu;
|
||||||
@@ -41,16 +42,19 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
];
|
];
|
||||||
private readonly IVirtualMemory _virtualMemory;
|
private readonly IVirtualMemory _virtualMemory;
|
||||||
private readonly IModuleManager _moduleManager;
|
private readonly IModuleManager _moduleManager;
|
||||||
|
private readonly IHostPlatform? _hostPlatform;
|
||||||
private INativeCpuBackend? _nativeCpuBackend;
|
private INativeCpuBackend? _nativeCpuBackend;
|
||||||
|
|
||||||
public CpuDispatcher(
|
public CpuDispatcher(
|
||||||
IVirtualMemory virtualMemory,
|
IVirtualMemory virtualMemory,
|
||||||
IModuleManager moduleManager,
|
IModuleManager moduleManager,
|
||||||
INativeCpuBackend? nativeCpuBackend = null)
|
INativeCpuBackend? nativeCpuBackend = null,
|
||||||
|
IHostPlatform? hostPlatform = null)
|
||||||
{
|
{
|
||||||
_virtualMemory = virtualMemory ?? throw new ArgumentNullException(nameof(virtualMemory));
|
_virtualMemory = virtualMemory ?? throw new ArgumentNullException(nameof(virtualMemory));
|
||||||
_moduleManager = moduleManager ?? throw new ArgumentNullException(nameof(moduleManager));
|
_moduleManager = moduleManager ?? throw new ArgumentNullException(nameof(moduleManager));
|
||||||
_nativeCpuBackend = nativeCpuBackend;
|
_nativeCpuBackend = nativeCpuBackend;
|
||||||
|
_hostPlatform = hostPlatform;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ulong? LastEntryPoint { get; private set; }
|
public ulong? LastEntryPoint { get; private set; }
|
||||||
@@ -266,7 +270,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
entryFrameDiagnostic,
|
entryFrameDiagnostic,
|
||||||
Environment.NewLine,
|
Environment.NewLine,
|
||||||
"CpuEngine: native-only");
|
"CpuEngine: native-only");
|
||||||
_nativeCpuBackend ??= new DirectExecutionBackend(_moduleManager);
|
_nativeCpuBackend ??= new DirectExecutionBackend(_moduleManager, _hostPlatform);
|
||||||
if (_nativeCpuBackend.TryExecute(
|
if (_nativeCpuBackend.TryExecute(
|
||||||
context,
|
context,
|
||||||
entryPoint,
|
entryPoint,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using System.Linq;
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
using SharpEmu.Logging;
|
using SharpEmu.Logging;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
@@ -134,8 +135,9 @@ public sealed partial class DirectExecutionBackend
|
|||||||
int num2 = 0;
|
int num2 = 0;
|
||||||
List<ulong> list = new List<ulong>(16);
|
List<ulong> list = new List<ulong>(16);
|
||||||
ulong num3 = scanStart;
|
ulong num3 = scanStart;
|
||||||
MEMORY_BASIC_INFORMATION64 lpBuffer;
|
var hostMemory = ResolveDiagnosticsHostMemory();
|
||||||
while (num3 < scanEnd && VirtualQuery((void*)num3, out lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) != 0)
|
HostRegionInfo lpBuffer;
|
||||||
|
while (num3 < scanEnd && hostMemory.Query(num3, out lpBuffer))
|
||||||
{
|
{
|
||||||
ulong baseAddress = lpBuffer.BaseAddress;
|
ulong baseAddress = lpBuffer.BaseAddress;
|
||||||
ulong num4 = baseAddress + lpBuffer.RegionSize;
|
ulong num4 = baseAddress + lpBuffer.RegionSize;
|
||||||
@@ -145,7 +147,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
ulong value = Math.Max(num3, baseAddress);
|
ulong value = Math.Max(num3, baseAddress);
|
||||||
ulong num5 = Math.Min(num4, scanEnd);
|
ulong num5 = Math.Min(num4, scanEnd);
|
||||||
if (lpBuffer.State == 4096 && IsReadableProtection(lpBuffer.Protect) && !IsExecutableProtection(lpBuffer.Protect))
|
if (lpBuffer.State == HostRegionState.Committed && IsReadableProtection(lpBuffer.RawProtection) && !IsExecutableProtection(lpBuffer.RawProtection))
|
||||||
{
|
{
|
||||||
ulong num6 = AlignUp(value, 8uL);
|
ulong num6 = AlignUp(value, 8uL);
|
||||||
for (ulong num7 = num6; num7 + 8 <= num5; num7 += 8)
|
for (ulong num7 = num6; num7 + 8 <= num5; num7 += 8)
|
||||||
@@ -350,7 +352,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
if (!ResolveDiagnosticsHostMemory().Query(address, out var lpBuffer))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -359,7 +361,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (lpBuffer.State != 4096 || !IsReadableProtection(lpBuffer.Protect))
|
if (lpBuffer.State != HostRegionState.Committed || !IsReadableProtection(lpBuffer.RawProtection))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -391,12 +393,12 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
if (!ResolveDiagnosticsHostMemory().Query(address, out var lpBuffer))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var executable = lpBuffer.State == 4096 && IsExecutableProtection(lpBuffer.Protect);
|
var executable = lpBuffer.State == HostRegionState.Committed && IsExecutableProtection(lpBuffer.RawProtection);
|
||||||
if (executable)
|
if (executable)
|
||||||
{
|
{
|
||||||
_knownExecutablePages.TryAdd(pageAddress, 0);
|
_knownExecutablePages.TryAdd(pageAddress, 0);
|
||||||
@@ -415,6 +417,14 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return (value + num) & ~num;
|
return (value + num) & ~num;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Diagnostics helpers are static (reachable from static handler paths), so
|
||||||
|
// they use the platform injected into the backend active on this thread and
|
||||||
|
// fall back to the process-wide singleton only when no run is bound.
|
||||||
|
private static IHostMemory ResolveDiagnosticsHostMemory()
|
||||||
|
{
|
||||||
|
return _activeExecutionBackend?._hostMemory ?? HostPlatform.Current.Memory;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsReadableProtection(uint protect)
|
private static bool IsReadableProtection(uint protect)
|
||||||
{
|
{
|
||||||
if ((protect & 0x100) != 0 || (protect & 1) != 0)
|
if ((protect & 0x100) != 0 || (protect & 1) != 0)
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ using System.Reflection;
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using SharpEmu.Core.Cpu.Disasm;
|
using SharpEmu.Core.Cpu.Disasm;
|
||||||
|
using SharpEmu.Core.Cpu.Native.Windows;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
@@ -22,12 +24,12 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_RAW_HANDLER"), "1", StringComparison.Ordinal))
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_RAW_HANDLER"), "1", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
_rawExceptionHandlerStub = CreateExceptionHandlerTrampoline(RawVectoredHandlerPtrManaged);
|
_rawExceptionHandlerStub = _faultHandling.CreateHandlerThunk(RawVectoredHandlerPtrManaged, _hostRspSlotTlsIndex, _tlsGetValueAddress);
|
||||||
if (_rawExceptionHandlerStub == 0)
|
if (_rawExceptionHandlerStub == 0)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Failed to create raw exception handler trampoline");
|
throw new InvalidOperationException("Failed to create raw exception handler trampoline");
|
||||||
}
|
}
|
||||||
_rawExceptionHandler = (nint)AddVectoredExceptionHandler(1u, _rawExceptionHandlerStub);
|
_rawExceptionHandler = _faultHandling.AddFirstChanceHandler(_rawExceptionHandlerStub);
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] Raw exception handler installed: 0x{_rawExceptionHandler:X16}");
|
Console.Error.WriteLine($"[LOADER][INFO] Raw exception handler installed: 0x{_rawExceptionHandler:X16}");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -37,22 +39,22 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
_handlerDelegate = VectoredHandler;
|
_handlerDelegate = VectoredHandler;
|
||||||
_handlerHandle = GCHandle.Alloc(_handlerDelegate);
|
_handlerHandle = GCHandle.Alloc(_handlerDelegate);
|
||||||
_exceptionHandlerStub = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_handlerDelegate));
|
_exceptionHandlerStub = _faultHandling.CreateHandlerThunk(Marshal.GetFunctionPointerForDelegate(_handlerDelegate), _hostRspSlotTlsIndex, _tlsGetValueAddress);
|
||||||
if (_exceptionHandlerStub == 0)
|
if (_exceptionHandlerStub == 0)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Failed to create exception handler trampoline");
|
throw new InvalidOperationException("Failed to create exception handler trampoline");
|
||||||
}
|
}
|
||||||
_exceptionHandler = (nint)AddVectoredExceptionHandler(1u, _exceptionHandlerStub);
|
_exceptionHandler = _faultHandling.AddFirstChanceHandler(_exceptionHandlerStub);
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] Exception handler installed: 0x{_exceptionHandler:X16}");
|
Console.Error.WriteLine($"[LOADER][INFO] Exception handler installed: 0x{_exceptionHandler:X16}");
|
||||||
|
|
||||||
_unhandledFilterDelegate = UnhandledExceptionFilter;
|
_unhandledFilterDelegate = UnhandledExceptionFilter;
|
||||||
_unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate);
|
_unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate);
|
||||||
_unhandledFilterStub = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate));
|
_unhandledFilterStub = _faultHandling.CreateHandlerThunk(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate), _hostRspSlotTlsIndex, _tlsGetValueAddress);
|
||||||
if (_unhandledFilterStub == 0)
|
if (_unhandledFilterStub == 0)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Failed to create unhandled exception filter trampoline");
|
throw new InvalidOperationException("Failed to create unhandled exception filter trampoline");
|
||||||
}
|
}
|
||||||
SetUnhandledExceptionFilter(_unhandledFilterStub);
|
_faultHandling.SetUnhandledFilter(_unhandledFilterStub);
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe int UnhandledExceptionFilter(void* exceptionInfo)
|
private unsafe int UnhandledExceptionFilter(void* exceptionInfo)
|
||||||
@@ -60,8 +62,8 @@ public sealed partial class DirectExecutionBackend
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
|
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
|
||||||
ulong rip = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, 248);
|
ulong rip = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, CTX_RIP);
|
||||||
ulong rsp = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, 152);
|
ulong rsp = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, CTX_RSP);
|
||||||
Console.Error.WriteLine("[LOADER][FATAL] Unhandled exception filter fired.");
|
Console.Error.WriteLine("[LOADER][FATAL] Unhandled exception filter fired.");
|
||||||
Console.Error.WriteLine($"[LOADER][FATAL] Code: 0x{exceptionRecord->ExceptionCode:X8}");
|
Console.Error.WriteLine($"[LOADER][FATAL] Code: 0x{exceptionRecord->ExceptionCode:X8}");
|
||||||
Console.Error.WriteLine($"[LOADER][FATAL] Exception Address: 0x{(ulong)(nint)exceptionRecord->ExceptionAddress:X16}");
|
Console.Error.WriteLine($"[LOADER][FATAL] Exception Address: 0x{(ulong)(nint)exceptionRecord->ExceptionAddress:X16}");
|
||||||
@@ -100,8 +102,8 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong rip = ReadCtxU64(contextRecord, 248);
|
ulong rip = ReadCtxU64(contextRecord, CTX_RIP);
|
||||||
ulong rsp = ReadCtxU64(contextRecord, 152);
|
ulong rsp = ReadCtxU64(contextRecord, CTX_RSP);
|
||||||
|
|
||||||
// Thread-mode probe: a hardware exception raised while this thread is inside
|
// Thread-mode probe: a hardware exception raised while this thread is inside
|
||||||
// the managed import gateway means the VEH->managed reentry happened from
|
// the managed import gateway means the VEH->managed reentry happened from
|
||||||
@@ -112,7 +114,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
$"veh_in_gateway code=0x{exceptionCode:X8} rip=0x{rip:X16} gateway_depth={_threadModeGatewayDepth}");
|
$"veh_in_gateway code=0x{exceptionCode:X8} rip=0x{rip:X16} gateway_depth={_threadModeGatewayDepth}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
|
if (exceptionCode == WindowsFaultCodes.AccessViolation && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -127,10 +129,10 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
switch (exceptionCode)
|
switch (exceptionCode)
|
||||||
{
|
{
|
||||||
case 3221225477u:
|
case WindowsFaultCodes.AccessViolation:
|
||||||
LogAccessViolationTrace(exceptionAddress, exceptionRecord);
|
LogAccessViolationTrace(exceptionAddress, exceptionRecord);
|
||||||
break;
|
break;
|
||||||
case 3221226505u:
|
case WindowsFaultCodes.FastFail:
|
||||||
{
|
{
|
||||||
ulong p0 = exceptionRecord->NumberParameters >= 1 ? (*exceptionRecord->ExceptionInformation) : 0;
|
ulong p0 = exceptionRecord->NumberParameters >= 1 ? (*exceptionRecord->ExceptionInformation) : 0;
|
||||||
ulong p1 = exceptionRecord->NumberParameters >= 2 ? exceptionRecord->ExceptionInformation[1] : 0;
|
ulong p1 = exceptionRecord->NumberParameters >= 2 ? exceptionRecord->ExceptionInformation[1] : 0;
|
||||||
@@ -140,21 +142,21 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong rax = ReadCtxU64(contextRecord, 120);
|
ulong rax = ReadCtxU64(contextRecord, CTX_RAX);
|
||||||
ulong rbx = ReadCtxU64(contextRecord, 144);
|
ulong rbx = ReadCtxU64(contextRecord, CTX_RBX);
|
||||||
ulong rcx = ReadCtxU64(contextRecord, 128);
|
ulong rcx = ReadCtxU64(contextRecord, CTX_RCX);
|
||||||
ulong rdx = ReadCtxU64(contextRecord, 136);
|
ulong rdx = ReadCtxU64(contextRecord, CTX_RDX);
|
||||||
ulong rsi = ReadCtxU64(contextRecord, 168);
|
ulong rsi = ReadCtxU64(contextRecord, CTX_RSI);
|
||||||
ulong rdi = ReadCtxU64(contextRecord, 176);
|
ulong rdi = ReadCtxU64(contextRecord, CTX_RDI);
|
||||||
ulong rbp = ReadCtxU64(contextRecord, 160);
|
ulong rbp = ReadCtxU64(contextRecord, CTX_RBP);
|
||||||
ulong r8 = ReadCtxU64(contextRecord, 184);
|
ulong r8 = ReadCtxU64(contextRecord, CTX_R8);
|
||||||
ulong r9 = ReadCtxU64(contextRecord, 192);
|
ulong r9 = ReadCtxU64(contextRecord, CTX_R9);
|
||||||
ulong r10 = ReadCtxU64(contextRecord, 200);
|
ulong r10 = ReadCtxU64(contextRecord, CTX_R10);
|
||||||
ulong r11 = ReadCtxU64(contextRecord, 208);
|
ulong r11 = ReadCtxU64(contextRecord, CTX_R11);
|
||||||
ulong r12 = ReadCtxU64(contextRecord, 216);
|
ulong r12 = ReadCtxU64(contextRecord, CTX_R12);
|
||||||
ulong r13 = ReadCtxU64(contextRecord, 224);
|
ulong r13 = ReadCtxU64(contextRecord, CTX_R13);
|
||||||
ulong r14 = ReadCtxU64(contextRecord, 232);
|
ulong r14 = ReadCtxU64(contextRecord, CTX_R14);
|
||||||
ulong r15 = ReadCtxU64(contextRecord, 240);
|
ulong r15 = ReadCtxU64(contextRecord, CTX_R15);
|
||||||
|
|
||||||
Console.Error.WriteLine("[LOADER][INFO] =========================================");
|
Console.Error.WriteLine("[LOADER][INFO] =========================================");
|
||||||
Console.Error.WriteLine("[LOADER][INFO] NATIVE EXCEPTION CAUGHT!");
|
Console.Error.WriteLine("[LOADER][INFO] NATIVE EXCEPTION CAUGHT!");
|
||||||
@@ -185,7 +187,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
ulong accessType = 0;
|
ulong accessType = 0;
|
||||||
ulong target = 0;
|
ulong target = 0;
|
||||||
if (exceptionCode == 3221225477u && exceptionRecord->NumberParameters >= 2)
|
if (exceptionCode == WindowsFaultCodes.AccessViolation && exceptionRecord->NumberParameters >= 2)
|
||||||
{
|
{
|
||||||
accessType = *exceptionRecord->ExceptionInformation;
|
accessType = *exceptionRecord->ExceptionInformation;
|
||||||
target = exceptionRecord->ExceptionInformation[1];
|
target = exceptionRecord->ExceptionInformation[1];
|
||||||
@@ -198,9 +200,9 @@ public sealed partial class DirectExecutionBackend
|
|||||||
};
|
};
|
||||||
Console.Error.WriteLine("[LOADER][INFO] AV access: " + accessText);
|
Console.Error.WriteLine("[LOADER][INFO] AV access: " + accessText);
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] AV target: 0x{target:X16}");
|
Console.Error.WriteLine($"[LOADER][INFO] AV target: 0x{target:X16}");
|
||||||
if (VirtualQuery((void*)target, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) != 0)
|
if (_hostMemory.Query(target, out var mbi))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] AV target region: base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} state=0x{mbi.State:X08} protect=0x{mbi.Protect:X08}");
|
Console.Error.WriteLine($"[LOADER][INFO] AV target region: base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} state=0x{mbi.RawState:X08} protect=0x{mbi.RawProtection:X08}");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -248,7 +250,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
switch (exceptionCode)
|
switch (exceptionCode)
|
||||||
{
|
{
|
||||||
case 3221225477u:
|
case WindowsFaultCodes.AccessViolation:
|
||||||
Console.Error.WriteLine("[LOADER][ERROR] Type: Access Violation");
|
Console.Error.WriteLine("[LOADER][ERROR] Type: Access Violation");
|
||||||
Console.Error.WriteLine("[LOADER][ERROR] This usually means:");
|
Console.Error.WriteLine("[LOADER][ERROR] This usually means:");
|
||||||
Console.Error.WriteLine("[LOADER][ERROR] - Guest code called an unmapped import");
|
Console.Error.WriteLine("[LOADER][ERROR] - Guest code called an unmapped import");
|
||||||
@@ -295,11 +297,11 @@ public sealed partial class DirectExecutionBackend
|
|||||||
DumpGuestReferenceDiagnostics();
|
DumpGuestReferenceDiagnostics();
|
||||||
DumpGuestPointerWindowDiagnostics();
|
DumpGuestPointerWindowDiagnostics();
|
||||||
break;
|
break;
|
||||||
case 2147483651u:
|
case WindowsFaultCodes.Breakpoint:
|
||||||
Console.Error.WriteLine("[LOADER][WARNING] Type: Breakpoint (int3)");
|
Console.Error.WriteLine("[LOADER][WARNING] Type: Breakpoint (int3)");
|
||||||
Console.Error.WriteLine("[LOADER][WARNING] Unexpected breakpoint in direct-bridge mode");
|
Console.Error.WriteLine("[LOADER][WARNING] Unexpected breakpoint in direct-bridge mode");
|
||||||
break;
|
break;
|
||||||
case 3221225501u:
|
case WindowsFaultCodes.IllegalInstruction:
|
||||||
Console.Error.WriteLine("[LOADER][INFO] Type: Illegal Instruction");
|
Console.Error.WriteLine("[LOADER][INFO] Type: Illegal Instruction");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -332,8 +334,8 @@ public sealed partial class DirectExecutionBackend
|
|||||||
EXCEPTION_POINTERS* pointers = (EXCEPTION_POINTERS*)exceptionInfo;
|
EXCEPTION_POINTERS* pointers = (EXCEPTION_POINTERS*)exceptionInfo;
|
||||||
EXCEPTION_RECORD* record = pointers->ExceptionRecord;
|
EXCEPTION_RECORD* record = pointers->ExceptionRecord;
|
||||||
void* contextRecord = pointers->ContextRecord;
|
void* contextRecord = pointers->ContextRecord;
|
||||||
ulong rip = contextRecord != null ? ReadCtxU64(contextRecord, 248) : 0;
|
ulong rip = contextRecord != null ? ReadCtxU64(contextRecord, CTX_RIP) : 0;
|
||||||
ulong rsp = contextRecord != null ? ReadCtxU64(contextRecord, 152) : 0;
|
ulong rsp = contextRecord != null ? ReadCtxU64(contextRecord, CTX_RSP) : 0;
|
||||||
ulong accessType = record->NumberParameters >= 1 ? *record->ExceptionInformation : 0;
|
ulong accessType = record->NumberParameters >= 1 ? *record->ExceptionInformation : 0;
|
||||||
ulong target = record->NumberParameters >= 2 ? record->ExceptionInformation[1] : 0;
|
ulong target = record->NumberParameters >= 2 ? record->ExceptionInformation[1] : 0;
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
@@ -479,7 +481,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
ulong address = scanBase;
|
ulong address = scanBase;
|
||||||
while (address < scanEnd)
|
while (address < scanEnd)
|
||||||
{
|
{
|
||||||
if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
if (!_hostMemory.Query(address, out var mbi))
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -491,9 +493,9 @@ public sealed partial class DirectExecutionBackend
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mbi.State == MEM_COMMIT &&
|
if (mbi.State == HostRegionState.Committed &&
|
||||||
IsReadableProtection(mbi.Protect) &&
|
IsReadableProtection(mbi.RawProtection) &&
|
||||||
IsExecutableProtection(mbi.Protect))
|
IsExecutableProtection(mbi.RawProtection))
|
||||||
{
|
{
|
||||||
ScanExecutableRegionForTargetReferences(regionBase, regionEnd, targetList, hitCounts, maxHitsPerTarget);
|
ScanExecutableRegionForTargetReferences(regionBase, regionEnd, targetList, hitCounts, maxHitsPerTarget);
|
||||||
}
|
}
|
||||||
@@ -798,13 +800,13 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
if (!_hostMemory.Query(address, out var mbi))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong regionEnd = mbi.BaseAddress + mbi.RegionSize;
|
ulong regionEnd = mbi.BaseAddress + mbi.RegionSize;
|
||||||
if (mbi.State != MEM_COMMIT || !IsReadableProtection(mbi.Protect) || regionEnd <= address || address > regionEnd - 8)
|
if (mbi.State != HostRegionState.Committed || !IsReadableProtection(mbi.RawProtection) || regionEnd <= address || address > regionEnd - 8)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -916,25 +918,25 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (VirtualQuery((void*)faultAddress, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
if (!_hostMemory.Query(faultAddress, out var mbi))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong pageBase = faultAddress & 0xFFFFFFFFFFFFF000uL;
|
ulong pageBase = faultAddress & 0xFFFFFFFFFFFFF000uL;
|
||||||
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.AllocationProtect);
|
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.RawAllocationProtection);
|
||||||
int traceIndex = Interlocked.Increment(ref _lazyCommitTraceCount);
|
int traceIndex = Interlocked.Increment(ref _lazyCommitTraceCount);
|
||||||
bool traceLazyCommit = ShouldTraceLazyCommit(traceIndex);
|
bool traceLazyCommit = ShouldTraceLazyCommit(traceIndex);
|
||||||
if (traceLazyCommit)
|
if (traceLazyCommit)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-query#{traceIndex}: fault=0x{faultAddress:X16} owner={owner} rip=0x{rip:X16} rsp=0x{rsp:X16} state=0x{mbi.State:X08} base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} alloc=0x{mbi.AllocationProtect:X08} prot=0x{mbi.Protect:X08}");
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-query#{traceIndex}: fault=0x{faultAddress:X16} owner={owner} rip=0x{rip:X16} rsp=0x{rsp:X16} state=0x{mbi.RawState:X08} base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} alloc=0x{mbi.RawAllocationProtection:X08} prot=0x{mbi.RawProtection:X08}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mbi.State == 4096 && IsAccessCompatible(accessType, mbi.Protect))
|
if (mbi.State == HostRegionState.Committed && IsAccessCompatible(accessType, mbi.RawProtection))
|
||||||
{
|
{
|
||||||
if (traceLazyCommit)
|
if (traceLazyCommit)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit-race#{traceIndex}: fault=0x{faultAddress:X16} protect=0x{mbi.Protect:X08}");
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit-race#{traceIndex}: fault=0x{faultAddress:X16} protect=0x{mbi.RawProtection:X08}");
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -943,10 +945,10 @@ public sealed partial class DirectExecutionBackend
|
|||||||
ulong committedBase = 0;
|
ulong committedBase = 0;
|
||||||
ulong committedSize = 0;
|
ulong committedSize = 0;
|
||||||
|
|
||||||
if (mbi.State == 65536)
|
if (mbi.State == HostRegionState.Free)
|
||||||
{
|
{
|
||||||
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var windowBase, out var windowSize) &&
|
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var windowBase, out var windowSize) &&
|
||||||
TryReserveThenCommit(windowBase, windowSize, windowBase, windowSize, commitProtect))
|
TryReserveThenCommit(_hostMemory, windowBase, windowSize, windowBase, windowSize, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = windowBase;
|
committedBase = windowBase;
|
||||||
@@ -955,7 +957,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
||||||
if (TryReserveThenCommit(largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
|
if (TryReserveThenCommit(_hostMemory, largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = largeBase;
|
committedBase = largeBase;
|
||||||
@@ -966,13 +968,13 @@ public sealed partial class DirectExecutionBackend
|
|||||||
if (!committed)
|
if (!committed)
|
||||||
{
|
{
|
||||||
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
||||||
if (TryReserveThenCommit(region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
|
if (TryReserveThenCommit(_hostMemory, region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = region64kBase;
|
committedBase = region64kBase;
|
||||||
committedSize = 65536uL;
|
committedSize = 65536uL;
|
||||||
}
|
}
|
||||||
else if (TryReserveThenCommit(pageBase, 4096uL, pageBase, 4096uL, commitProtect))
|
else if (TryReserveThenCommit(_hostMemory, pageBase, 4096uL, pageBase, 4096uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = pageBase;
|
committedBase = pageBase;
|
||||||
@@ -985,7 +987,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
|
TryCommitRange(_hostMemory, pageBase + 4096, 4096uL, commitProtect);
|
||||||
if (traceLazyCommit)
|
if (traceLazyCommit)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-reserve-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-reserve-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
||||||
@@ -993,13 +995,13 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mbi.State != 8192)
|
if (mbi.State != HostRegionState.Reserved)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var commitWindowBase, out var commitWindowSize) &&
|
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var commitWindowBase, out var commitWindowSize) &&
|
||||||
TryCommitRange(commitWindowBase, commitWindowSize, commitProtect))
|
TryCommitRange(_hostMemory, commitWindowBase, commitWindowSize, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = commitWindowBase;
|
committedBase = commitWindowBase;
|
||||||
@@ -1008,7 +1010,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
||||||
if (TryCommitRange(largeCommitBase, 2097152uL, commitProtect))
|
if (TryCommitRange(_hostMemory, largeCommitBase, 2097152uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = largeCommitBase;
|
committedBase = largeCommitBase;
|
||||||
@@ -1019,19 +1021,19 @@ public sealed partial class DirectExecutionBackend
|
|||||||
if (!committed)
|
if (!committed)
|
||||||
{
|
{
|
||||||
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
||||||
if (TryCommitRange(region64kBase, 65536uL, commitProtect))
|
if (TryCommitRange(_hostMemory, region64kBase, 65536uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = region64kBase;
|
committedBase = region64kBase;
|
||||||
committedSize = 65536uL;
|
committedSize = 65536uL;
|
||||||
}
|
}
|
||||||
else if (TryCommitRange(pageBase, 8192uL, commitProtect))
|
else if (TryCommitRange(_hostMemory, pageBase, 8192uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = pageBase;
|
committedBase = pageBase;
|
||||||
committedSize = 8192uL;
|
committedSize = 8192uL;
|
||||||
}
|
}
|
||||||
else if (TryCommitRange(pageBase, 4096uL, commitProtect))
|
else if (TryCommitRange(_hostMemory, pageBase, 4096uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = pageBase;
|
committedBase = pageBase;
|
||||||
@@ -1044,7 +1046,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
|
TryCommitRange(_hostMemory, pageBase + 4096, 4096uL, commitProtect);
|
||||||
if (traceLazyCommit)
|
if (traceLazyCommit)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
||||||
@@ -1085,31 +1087,33 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static unsafe bool TryCommitRange(ulong baseAddress, ulong length, uint protection)
|
// The commit protection is one of the two raw values ResolveLazyCommitProtection
|
||||||
|
// produces (0x40 RWX / 0x04 RW); the enum mapping reproduces those exactly.
|
||||||
|
static bool TryCommitRange(IHostMemory hostMemory, ulong baseAddress, ulong length, uint protection)
|
||||||
{
|
{
|
||||||
if (length == 0)
|
if (length == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return VirtualAlloc((void*)baseAddress, (nuint)length, 4096u, protection) != null;
|
return hostMemory.Commit(baseAddress, length, protection == 64u ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite);
|
||||||
}
|
}
|
||||||
|
|
||||||
static unsafe bool TryReserveRange(ulong baseAddress, ulong length)
|
static bool TryReserveRange(IHostMemory hostMemory, ulong baseAddress, ulong length)
|
||||||
{
|
{
|
||||||
if (length == 0)
|
if (length == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return VirtualAlloc((void*)baseAddress, (nuint)length, 8192u, 4u) != null;
|
return hostMemory.Reserve(baseAddress, length, HostPageProtection.ReadWrite) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool TryReserveThenCommit(ulong reserveAddress, ulong reserveSize, ulong commitAddress, ulong commitSize, uint protection)
|
static bool TryReserveThenCommit(IHostMemory hostMemory, ulong reserveAddress, ulong reserveSize, ulong commitAddress, ulong commitSize, uint protection)
|
||||||
{
|
{
|
||||||
if (!TryReserveRange(reserveAddress, reserveSize))
|
if (!TryReserveRange(hostMemory, reserveAddress, reserveSize))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return TryCommitRange(commitAddress, commitSize, protection);
|
return TryCommitRange(hostMemory, commitAddress, commitSize, protection);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool IsAccessCompatible(ulong accessType, uint protection)
|
static bool IsAccessCompatible(ulong accessType, uint protection)
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ using System.Linq;
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using SharpEmu.Core.Cpu;
|
using SharpEmu.Core.Cpu;
|
||||||
|
using SharpEmu.Core.Cpu.Native.Windows;
|
||||||
using SharpEmu.Core.Loader;
|
using SharpEmu.Core.Loader;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
@@ -69,23 +71,23 @@ public sealed partial class DirectExecutionBackend
|
|||||||
private unsafe static int TryRecoverUnresolvedSentinel(void* exceptionInfo)
|
private unsafe static int TryRecoverUnresolvedSentinel(void* exceptionInfo)
|
||||||
{
|
{
|
||||||
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
|
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
|
||||||
if (exceptionRecord->ExceptionCode != 3221225477u)
|
if (exceptionRecord->ExceptionCode != WindowsFaultCodes.AccessViolation)
|
||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
void* contextRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord;
|
void* contextRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord;
|
||||||
ulong value = ReadCtxU64(contextRecord, 248);
|
ulong value = ReadCtxU64(contextRecord, CTX_RIP);
|
||||||
ulong value2 = (ulong)exceptionRecord->ExceptionAddress;
|
ulong value2 = (ulong)exceptionRecord->ExceptionAddress;
|
||||||
if (!IsUnresolvedSentinel(value) && !IsUnresolvedSentinel(value2))
|
if (!IsUnresolvedSentinel(value) && !IsUnresolvedSentinel(value2))
|
||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
ulong rsp = ReadCtxU64(contextRecord, 152);
|
ulong rsp = ReadCtxU64(contextRecord, CTX_RSP);
|
||||||
WriteCtxU64(contextRecord, 120, 0uL);
|
WriteCtxU64(contextRecord, CTX_RAX, 0uL);
|
||||||
if (TryGetPlausibleReturnFromStack(rsp, out var returnRip, out var nextRsp))
|
if (TryGetPlausibleReturnFromStack(rsp, out var returnRip, out var nextRsp))
|
||||||
{
|
{
|
||||||
WriteCtxU64(contextRecord, 152, nextRsp);
|
WriteCtxU64(contextRecord, CTX_RSP, nextRsp);
|
||||||
WriteCtxU64(contextRecord, 248, returnRip);
|
WriteCtxU64(contextRecord, CTX_RIP, returnRip);
|
||||||
Interlocked.Increment(ref _rawSentinelRecoveries);
|
Interlocked.Increment(ref _rawSentinelRecoveries);
|
||||||
if (LogThreadMode)
|
if (LogThreadMode)
|
||||||
{
|
{
|
||||||
@@ -1725,9 +1727,9 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
var candidateBase = ImportStubRegionCanonicalBase -
|
var candidateBase = ImportStubRegionCanonicalBase -
|
||||||
(ulong)candidateIndex * ImportStubRegionAddressStride;
|
(ulong)candidateIndex * ImportStubRegionAddressStride;
|
||||||
if (VirtualQuery((void*)candidateBase, out var memoryInfo, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0 ||
|
if (!_hostMemory.Query(candidateBase, out var memoryInfo) ||
|
||||||
memoryInfo.RegionSize == 0 ||
|
memoryInfo.RegionSize == 0 ||
|
||||||
memoryInfo.State != 4096)
|
memoryInfo.State != HostRegionState.Committed)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -2066,7 +2068,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
uint flNewProtect = default(uint);
|
uint flNewProtect = default(uint);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (Marshal.ReadByte(num2) != 232 || !VirtualProtect((void*)num, 5u, 64u, &flNewProtect))
|
if (Marshal.ReadByte(num2) != 232 || !_hostMemory.Protect((ulong)(void*)num, 5u, HostPageProtection.ReadWriteExecute, out flNewProtect))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2074,7 +2076,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
Marshal.WriteByte(num2 + i, 144);
|
Marshal.WriteByte(num2 + i, 144);
|
||||||
}
|
}
|
||||||
FlushInstructionCache(GetCurrentProcess(), (void*)num, 5u);
|
_hostMemory.FlushInstructionCache((ulong)(void*)num, 5u);
|
||||||
_patchedEa020eLookupCall = true;
|
_patchedEa020eLookupCall = true;
|
||||||
Console.Error.WriteLine($"[LOADER][WARNING] Import#{dispatchIndex}: patched hash-lookup call at 0x{num:X16} -> NOP*5");
|
Console.Error.WriteLine($"[LOADER][WARNING] Import#{dispatchIndex}: patched hash-lookup call at 0x{num:X16} -> NOP*5");
|
||||||
}
|
}
|
||||||
@@ -2085,7 +2087,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
if (flNewProtect != 0)
|
if (flNewProtect != 0)
|
||||||
{
|
{
|
||||||
VirtualProtect((void*)num, 5u, flNewProtect, &flNewProtect);
|
_hostMemory.ProtectRaw((ulong)(void*)num, 5u, flNewProtect, out flNewProtect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System.Collections.Generic;
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
@@ -35,20 +36,6 @@ public sealed partial class DirectExecutionBackend
|
|||||||
private bool _nativeWorkersDisposed;
|
private bool _nativeWorkersDisposed;
|
||||||
private int _nativeWorkerCreationFailedLogged;
|
private int _nativeWorkerCreationFailedLogged;
|
||||||
|
|
||||||
private const uint StackSizeParamIsAReservation = 0x00010000u;
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
private static extern nint CreateThread(
|
|
||||||
nint lpThreadAttributes,
|
|
||||||
nuint dwStackSize,
|
|
||||||
nint lpStartAddress,
|
|
||||||
nint lpParameter,
|
|
||||||
uint dwCreationFlags,
|
|
||||||
out uint lpThreadId);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
private static extern uint WaitForSingleObject(nint hHandle, uint dwMilliseconds);
|
|
||||||
|
|
||||||
// Runs an emitted guest entry stub. Preferred path is a pooled native worker
|
// Runs an emitted guest entry stub. Preferred path is a pooled native worker
|
||||||
// thread; falls back to the historical inline calli (guest frames above this
|
// thread; falls back to the historical inline calli (guest frames above this
|
||||||
// thread's managed frames) when workers are disabled or unavailable.
|
// thread's managed frames) when workers are disabled or unavailable.
|
||||||
@@ -61,7 +48,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
var worker = RentNativeGuestExecutor();
|
var worker = RentNativeGuestExecutor();
|
||||||
if (worker is null)
|
if (worker is null)
|
||||||
{
|
{
|
||||||
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
_hostThreading.SetTlsValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
||||||
return CallNativeEntry(entryStub);
|
return CallNativeEntry(entryStub);
|
||||||
}
|
}
|
||||||
try
|
try
|
||||||
@@ -229,7 +216,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
public static NativeGuestExecutor? TryCreate(DirectExecutionBackend backend)
|
public static NativeGuestExecutor? TryCreate(DirectExecutionBackend backend)
|
||||||
{
|
{
|
||||||
if (!EnsureKernel32Exports())
|
if (!EnsureHostRuntimeExports(backend._hostSymbols))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -242,32 +229,27 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return executor;
|
return executor;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool EnsureKernel32Exports()
|
private static bool EnsureHostRuntimeExports(IHostSymbolResolver symbols)
|
||||||
{
|
{
|
||||||
if (_exitThreadAddress != 0)
|
if (_exitThreadAddress != 0)
|
||||||
{
|
{
|
||||||
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0;
|
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0;
|
||||||
}
|
}
|
||||||
nint kernel32 = GetModuleHandle("kernel32.dll");
|
_waitForSingleObjectAddress = symbols.GetAddress(HostRuntimeFunction.WaitForSingleObject);
|
||||||
if (kernel32 == 0)
|
_setEventAddress = symbols.GetAddress(HostRuntimeFunction.SetEvent);
|
||||||
{
|
_exitThreadAddress = symbols.GetAddress(HostRuntimeFunction.ExitThread);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
_waitForSingleObjectAddress = GetProcAddress(kernel32, "WaitForSingleObject");
|
|
||||||
_setEventAddress = GetProcAddress(kernel32, "SetEvent");
|
|
||||||
_exitThreadAddress = GetProcAddress(kernel32, "ExitThread");
|
|
||||||
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0 && _exitThreadAddress != 0;
|
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0 && _exitThreadAddress != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool Initialize()
|
private bool Initialize()
|
||||||
{
|
{
|
||||||
_selfHandle = GCHandle.Alloc(this);
|
_selfHandle = GCHandle.Alloc(this);
|
||||||
_controlBlock = VirtualAlloc(null, 4096u, 12288u, 4u);
|
_controlBlock = (void*)_backend._hostMemory.Allocate(0, 4096u, HostPageProtection.ReadWrite);
|
||||||
if (_controlBlock == null)
|
if (_controlBlock == null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
_loopStub = VirtualAlloc(null, LoopStubSize, 12288u, 64u);
|
_loopStub = (void*)_backend._hostMemory.Allocate(0, LoopStubSize, HostPageProtection.ReadWriteExecute);
|
||||||
if (_loopStub == null)
|
if (_loopStub == null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -349,17 +331,15 @@ public sealed partial class DirectExecutionBackend
|
|||||||
*(int*)(code + skipJump) = skipEntryOffset - (skipJump + sizeof(int));
|
*(int*)(code + skipJump) = skipEntryOffset - (skipJump + sizeof(int));
|
||||||
|
|
||||||
uint oldProtect = 0;
|
uint oldProtect = 0;
|
||||||
if (!VirtualProtect(_loopStub, LoopStubSize, 32u, &oldProtect))
|
if (!_backend._hostMemory.Protect((ulong)_loopStub, LoopStubSize, HostPageProtection.ReadExecute, out oldProtect))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
FlushInstructionCache(GetCurrentProcess(), _loopStub, LoopStubSize);
|
_backend._hostMemory.FlushInstructionCache((ulong)_loopStub, LoopStubSize);
|
||||||
_threadHandle = CreateThread(
|
_threadHandle = _backend._hostThreading.CreateNativeThread(
|
||||||
0,
|
|
||||||
WorkerStackReservation,
|
|
||||||
(nint)_loopStub,
|
(nint)_loopStub,
|
||||||
0,
|
0,
|
||||||
StackSizeParamIsAReservation,
|
WorkerStackReservation,
|
||||||
out _nativeThreadId);
|
out _nativeThreadId);
|
||||||
if (_threadHandle == 0)
|
if (_threadHandle == 0)
|
||||||
{
|
{
|
||||||
@@ -465,7 +445,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
_prevYieldRequested = _activeGuestThreadYieldRequested;
|
_prevYieldRequested = _activeGuestThreadYieldRequested;
|
||||||
_prevYieldReason = _activeGuestThreadYieldReason;
|
_prevYieldReason = _activeGuestThreadYieldReason;
|
||||||
_prevState = _activeGuestThreadState;
|
_prevState = _activeGuestThreadState;
|
||||||
_prevHostRspSlot = TlsGetValue(backend._hostRspSlotTlsIndex);
|
_prevHostRspSlot = backend._hostThreading.GetTlsValue(backend._hostRspSlotTlsIndex);
|
||||||
_prevGuestThreadHandle = GuestThreadExecution.EnterGuestThread(_runGuestThreadHandle);
|
_prevGuestThreadHandle = GuestThreadExecution.EnterGuestThread(_runGuestThreadHandle);
|
||||||
_entered = true;
|
_entered = true;
|
||||||
_activeExecutionBackend = backend;
|
_activeExecutionBackend = backend;
|
||||||
@@ -477,11 +457,11 @@ public sealed partial class DirectExecutionBackend
|
|||||||
_activeGuestThreadYieldReason = null;
|
_activeGuestThreadYieldReason = null;
|
||||||
_activeGuestThreadState = _runState;
|
_activeGuestThreadState = _runState;
|
||||||
backend.BindTlsBase(_runContext!);
|
backend.BindTlsBase(_runContext!);
|
||||||
TlsSetValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
|
backend._hostThreading.SetTlsValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
|
||||||
if (_runState is { } state)
|
if (_runState is { } state)
|
||||||
{
|
{
|
||||||
_prevHostThreadId = Volatile.Read(ref state.HostThreadId);
|
_prevHostThreadId = Volatile.Read(ref state.HostThreadId);
|
||||||
Volatile.Write(ref state.HostThreadId, unchecked((int)GetCurrentThreadId()));
|
Volatile.Write(ref state.HostThreadId, unchecked((int)backend._hostThreading.CurrentThreadId));
|
||||||
}
|
}
|
||||||
if (_runAffinityMask != 0)
|
if (_runAffinityMask != 0)
|
||||||
{
|
{
|
||||||
@@ -511,7 +491,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
Volatile.Write(ref state.HostThreadId, _prevHostThreadId);
|
Volatile.Write(ref state.HostThreadId, _prevHostThreadId);
|
||||||
}
|
}
|
||||||
TlsSetValue(_backend._hostRspSlotTlsIndex, _prevHostRspSlot);
|
_backend._hostThreading.SetTlsValue(_backend._hostRspSlotTlsIndex, _prevHostRspSlot);
|
||||||
GuestThreadExecution.RestoreGuestThread(_prevGuestThreadHandle);
|
GuestThreadExecution.RestoreGuestThread(_prevGuestThreadHandle);
|
||||||
_activeExecutionBackend = _prevBackend;
|
_activeExecutionBackend = _prevBackend;
|
||||||
_activeCpuContext = _prevContext;
|
_activeCpuContext = _prevContext;
|
||||||
@@ -548,8 +528,8 @@ public sealed partial class DirectExecutionBackend
|
|||||||
var exited = _threadHandle == 0;
|
var exited = _threadHandle == 0;
|
||||||
if (_threadHandle != 0)
|
if (_threadHandle != 0)
|
||||||
{
|
{
|
||||||
exited = WaitForSingleObject(_threadHandle, 1000u) == 0u;
|
exited = _backend._hostThreading.WaitForThreadExit(_threadHandle, 1000u);
|
||||||
CloseHandle(_threadHandle);
|
_backend._hostThreading.CloseThreadHandle(_threadHandle);
|
||||||
_threadHandle = 0;
|
_threadHandle = 0;
|
||||||
}
|
}
|
||||||
if (!exited)
|
if (!exited)
|
||||||
@@ -563,12 +543,12 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
if (_loopStub != null)
|
if (_loopStub != null)
|
||||||
{
|
{
|
||||||
VirtualFree(_loopStub, 0u, 32768u);
|
_backend._hostMemory.Free((ulong)_loopStub);
|
||||||
_loopStub = null;
|
_loopStub = null;
|
||||||
}
|
}
|
||||||
if (_controlBlock != null)
|
if (_controlBlock != null)
|
||||||
{
|
{
|
||||||
VirtualFree(_controlBlock, 0u, 32768u);
|
_backend._hostMemory.Free((ulong)_controlBlock);
|
||||||
_controlBlock = null;
|
_controlBlock = null;
|
||||||
}
|
}
|
||||||
if (_selfHandle.IsAllocated)
|
if (_selfHandle.IsAllocated)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
@@ -11,17 +12,15 @@ public sealed unsafe class StubManager : IDisposable
|
|||||||
private readonly List<nint> _allocatedStubs = new();
|
private readonly List<nint> _allocatedStubs = new();
|
||||||
private readonly Dictionary<string, nint> _importHandlers = new();
|
private readonly Dictionary<string, nint> _importHandlers = new();
|
||||||
private readonly Dictionary<ulong, nint> _stubAddresses = new();
|
private readonly Dictionary<ulong, nint> _stubAddresses = new();
|
||||||
|
private readonly IHostMemory _hostMemory;
|
||||||
private byte* _pltMemory;
|
private byte* _pltMemory;
|
||||||
private int _pltOffset;
|
private int _pltOffset;
|
||||||
private const int PltMemorySize = 1024 * 1024; // 1MB for stubs
|
private const int PltMemorySize = 1024 * 1024; // 1MB for stubs
|
||||||
|
|
||||||
public StubManager()
|
public StubManager(IHostMemory? hostMemory = null)
|
||||||
{
|
{
|
||||||
_pltMemory = (byte*)VirtualAlloc(
|
_hostMemory = hostMemory ?? HostPlatform.Current.Memory;
|
||||||
null,
|
_pltMemory = (byte*)_hostMemory.Allocate(0, PltMemorySize, HostPageProtection.ReadWriteExecute);
|
||||||
(nuint)PltMemorySize,
|
|
||||||
AllocationType.Reserve | AllocationType.Commit,
|
|
||||||
MemoryProtection.ExecuteReadWrite);
|
|
||||||
|
|
||||||
if (_pltMemory == null)
|
if (_pltMemory == null)
|
||||||
{
|
{
|
||||||
@@ -185,7 +184,7 @@ public sealed unsafe class StubManager : IDisposable
|
|||||||
{
|
{
|
||||||
if (_pltMemory != null)
|
if (_pltMemory != null)
|
||||||
{
|
{
|
||||||
VirtualFree(_pltMemory, 0, FreeType.Release);
|
_hostMemory.Free((ulong)_pltMemory);
|
||||||
_pltMemory = null;
|
_pltMemory = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,29 +193,5 @@ public sealed unsafe class StubManager : IDisposable
|
|||||||
_stubAddresses.Clear();
|
_stubAddresses.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
private static extern void* VirtualAlloc(void* lpAddress, nuint dwSize, AllocationType flAllocationType, MemoryProtection flProtect);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
private static extern bool VirtualFree(void* lpAddress, nuint dwSize, FreeType dwFreeType);
|
|
||||||
|
|
||||||
[Flags]
|
|
||||||
private enum AllocationType : uint
|
|
||||||
{
|
|
||||||
Commit = 0x1000,
|
|
||||||
Reserve = 0x2000,
|
|
||||||
}
|
|
||||||
|
|
||||||
[Flags]
|
|
||||||
private enum MemoryProtection : uint
|
|
||||||
{
|
|
||||||
ExecuteReadWrite = 0x40,
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum FreeType : uint
|
|
||||||
{
|
|
||||||
Release = 0x8000,
|
|
||||||
}
|
|
||||||
|
|
||||||
public delegate void ImportHandler(CpuContext context);
|
public delegate void ImportHandler(CpuContext context);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu.Native.Windows;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Byte offsets into the Win64 CONTEXT record delivered to vectored exception
|
||||||
|
/// handlers. The handlers read/write guest registers directly at these offsets
|
||||||
|
/// (no managed CONTEXT struct exists); a future POSIX backend gets a sibling
|
||||||
|
/// class for its mcontext layout.
|
||||||
|
/// </summary>
|
||||||
|
internal static class Win64ContextOffsets
|
||||||
|
{
|
||||||
|
public const int Mxcsr = 52;
|
||||||
|
public const int Rax = 120;
|
||||||
|
public const int Rcx = 128;
|
||||||
|
public const int Rdx = 136;
|
||||||
|
public const int Rbx = 144;
|
||||||
|
public const int Rsp = 152;
|
||||||
|
public const int Rbp = 160;
|
||||||
|
public const int Rsi = 168;
|
||||||
|
public const int Rdi = 176;
|
||||||
|
public const int R8 = 184;
|
||||||
|
public const int R9 = 192;
|
||||||
|
public const int R10 = 200;
|
||||||
|
public const int R11 = 208;
|
||||||
|
public const int R12 = 216;
|
||||||
|
public const int R13 = 224;
|
||||||
|
public const int R14 = 232;
|
||||||
|
public const int R15 = 240;
|
||||||
|
public const int Rip = 248;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu.Native.Windows;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Windows NTSTATUS exception codes and EXCEPTION_RECORD access-type values the
|
||||||
|
/// fault handlers filter on. Values are the same numbers the handlers previously
|
||||||
|
/// compared as bare literals; only the spelling changed.
|
||||||
|
/// </summary>
|
||||||
|
internal static class WindowsFaultCodes
|
||||||
|
{
|
||||||
|
public const uint AccessViolation = 0xC0000005u; // 3221225477
|
||||||
|
public const uint Breakpoint = 0x80000003u; // 2147483651
|
||||||
|
public const uint IllegalInstruction = 0xC000001Du; // 3221225501
|
||||||
|
public const uint FastFail = 0xC0000409u; // 3221226505
|
||||||
|
public const uint StackOverflow = 0xC00000FDu;
|
||||||
|
public const uint ClrManagedException = 0xE0434352u;
|
||||||
|
|
||||||
|
// EXCEPTION_RECORD.ExceptionInformation[0] for access violations.
|
||||||
|
public const ulong AccessRead = 0;
|
||||||
|
public const ulong AccessWrite = 1;
|
||||||
|
public const ulong AccessExecute = 8;
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu.Native.Windows;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Vectored-exception-handler installation and the handler pre-filter thunk.
|
||||||
|
/// The thunk is inherently Windows-shaped (TEB stack-limit reads via gs:,
|
||||||
|
/// NTSTATUS pre-filtering, Win64 calling convention) and moved here whole from
|
||||||
|
/// DirectExecutionBackend; a POSIX backend supplies a sibling built around
|
||||||
|
/// sigaction/sigaltstack instead.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed unsafe partial class WindowsFaultHandling : IHostFaultHandling
|
||||||
|
{
|
||||||
|
private readonly IHostMemory _memory;
|
||||||
|
|
||||||
|
public WindowsFaultHandling(IHostMemory memory)
|
||||||
|
{
|
||||||
|
_memory = memory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public nint CreateHandlerThunk(nint managedCallback, uint hostRspSwitchTlsSlot, nint tlsGetValueAddress)
|
||||||
|
{
|
||||||
|
const uint stubSize = 256u;
|
||||||
|
void* ptr = (void*)_memory.Allocate(0, stubSize, HostPageProtection.ReadWriteExecute);
|
||||||
|
if (ptr == null)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte* code = (byte*)ptr;
|
||||||
|
int offset = 0;
|
||||||
|
// Native pre-filter: these exception codes are raised while the thread can be in
|
||||||
|
// cooperative GC mode (a C# throw is RaiseException(0xE0434352) on the throwing
|
||||||
|
// thread; FailFast/stack-overflow arrive mid-runtime-failure). Entering the managed
|
||||||
|
// handler then trips the CLR's reverse-P/Invoke check and kills the process with
|
||||||
|
// "Invalid Program: attempted to call a UnmanagedCallersOnly method from managed
|
||||||
|
// code" — this is why no managed throw (even one with a catch handler) ever
|
||||||
|
// survived inside the emulator. Continue the handler search without touching
|
||||||
|
// managed code; the CLR's own VEH handles its exceptions. MSVC C++ exceptions
|
||||||
|
// (Vulkan drivers, host CRT) are excluded too: the managed handler only ever
|
||||||
|
// returned CONTINUE_SEARCH for them.
|
||||||
|
ReadOnlySpan<uint> nonManagedExceptionCodes =
|
||||||
|
[WindowsFaultCodes.ClrManagedException, 0xE06D7363u, WindowsFaultCodes.FastFail, WindowsFaultCodes.StackOverflow];
|
||||||
|
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x01); // mov rax, [rcx] (ExceptionRecord*)
|
||||||
|
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x00); // mov eax, [rax] (ExceptionCode)
|
||||||
|
var passJumpOffsets = stackalloc int[nonManagedExceptionCodes.Length];
|
||||||
|
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
|
||||||
|
{
|
||||||
|
EmitByte(code, ref offset, 0x3D); // cmp eax, imm32
|
||||||
|
EmitUInt32(code, ref offset, nonManagedExceptionCodes[i]);
|
||||||
|
EmitByte(code, ref offset, 0x74); // je pass
|
||||||
|
passJumpOffsets[i] = offset;
|
||||||
|
EmitByte(code, ref offset, 0x00);
|
||||||
|
}
|
||||||
|
EmitByte(code, ref offset, 0xEB); EmitByte(code, ref offset, 0x03); // jmp over pass block
|
||||||
|
int passOffset = offset;
|
||||||
|
EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0); // pass: xor eax, eax (EXCEPTION_CONTINUE_SEARCH)
|
||||||
|
EmitByte(code, ref offset, 0xC3); // ret
|
||||||
|
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
|
||||||
|
{
|
||||||
|
code[passJumpOffsets[i]] = checked((byte)(passOffset - (passJumpOffsets[i] + 1)));
|
||||||
|
}
|
||||||
|
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x54); // push r12
|
||||||
|
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x55); // push r13
|
||||||
|
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE4); // mov r12, rsp
|
||||||
|
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xCD); // mov r13, rcx
|
||||||
|
EmitByte(code, ref offset, 0x65); EmitByte(code, ref offset, 0x48); // mov rax, gs:[8]
|
||||||
|
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x04); EmitByte(code, ref offset, 0x25);
|
||||||
|
EmitUInt32(code, ref offset, 8u);
|
||||||
|
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x39); EmitByte(code, ref offset, 0xC4); // cmp r12, rax
|
||||||
|
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x83); // jae guestStack
|
||||||
|
int aboveStackJump = offset;
|
||||||
|
EmitUInt32(code, ref offset, 0u);
|
||||||
|
EmitByte(code, ref offset, 0x65); EmitByte(code, ref offset, 0x48); // mov rax, gs:[0x10]
|
||||||
|
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x04); EmitByte(code, ref offset, 0x25);
|
||||||
|
EmitUInt32(code, ref offset, 0x10u);
|
||||||
|
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x39); EmitByte(code, ref offset, 0xC4); // cmp r12, rax
|
||||||
|
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x82); // jb guestStack
|
||||||
|
int belowStackJump = offset;
|
||||||
|
EmitUInt32(code, ref offset, 0u);
|
||||||
|
|
||||||
|
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28);
|
||||||
|
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE9); // mov rcx, r13
|
||||||
|
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||||
|
*(nint*)(code + offset) = managedCallback;
|
||||||
|
offset += sizeof(nint);
|
||||||
|
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||||
|
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28);
|
||||||
|
EmitByte(code, ref offset, 0xE9);
|
||||||
|
int hostRestoreJump = offset;
|
||||||
|
EmitUInt32(code, ref offset, 0u);
|
||||||
|
|
||||||
|
int guestStackOffset = offset;
|
||||||
|
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28);
|
||||||
|
EmitByte(code, ref offset, 0xB9);
|
||||||
|
EmitUInt32(code, ref offset, hostRspSwitchTlsSlot);
|
||||||
|
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||||
|
*(nint*)(code + offset) = tlsGetValueAddress;
|
||||||
|
offset += sizeof(nint);
|
||||||
|
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||||
|
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28);
|
||||||
|
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x85); EmitByte(code, ref offset, 0xC0); // test rax, rax
|
||||||
|
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x84);
|
||||||
|
int missingTlsJump = offset;
|
||||||
|
EmitUInt32(code, ref offset, 0u);
|
||||||
|
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x18); // mov r11, [rax]
|
||||||
|
EmitByte(code, ref offset, 0x4D); EmitByte(code, ref offset, 0x85); EmitByte(code, ref offset, 0xDB); // test r11, r11
|
||||||
|
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x84);
|
||||||
|
int missingHostStackJump = offset;
|
||||||
|
EmitUInt32(code, ref offset, 0u);
|
||||||
|
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xDC); // mov rsp, r11
|
||||||
|
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28);
|
||||||
|
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE9); // mov rcx, r13
|
||||||
|
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||||
|
*(nint*)(code + offset) = managedCallback;
|
||||||
|
offset += sizeof(nint);
|
||||||
|
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||||
|
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28);
|
||||||
|
EmitByte(code, ref offset, 0xE9);
|
||||||
|
int guestRestoreJump = offset;
|
||||||
|
EmitUInt32(code, ref offset, 0u);
|
||||||
|
|
||||||
|
int passThroughOffset = offset;
|
||||||
|
EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0); // xor eax, eax
|
||||||
|
int restoreOffset = offset;
|
||||||
|
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE4); // mov rsp, r12
|
||||||
|
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5D);
|
||||||
|
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5C);
|
||||||
|
EmitByte(code, ref offset, 0xC3);
|
||||||
|
|
||||||
|
*(int*)(code + aboveStackJump) = guestStackOffset - (aboveStackJump + sizeof(int));
|
||||||
|
*(int*)(code + belowStackJump) = guestStackOffset - (belowStackJump + sizeof(int));
|
||||||
|
*(int*)(code + hostRestoreJump) = restoreOffset - (hostRestoreJump + sizeof(int));
|
||||||
|
*(int*)(code + missingTlsJump) = passThroughOffset - (missingTlsJump + sizeof(int));
|
||||||
|
*(int*)(code + missingHostStackJump) = passThroughOffset - (missingHostStackJump + sizeof(int));
|
||||||
|
*(int*)(code + guestRestoreJump) = restoreOffset - (guestRestoreJump + sizeof(int));
|
||||||
|
|
||||||
|
if (!_memory.Protect((ulong)ptr, stubSize, HostPageProtection.ReadExecute, out _))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for exception handler trampoline at 0x{(nint)ptr:X16}");
|
||||||
|
_ = _memory.Free((ulong)ptr);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
_memory.FlushInstructionCache((ulong)ptr, (ulong)offset);
|
||||||
|
return (nint)ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void FreeThunk(nint thunk)
|
||||||
|
{
|
||||||
|
_ = _memory.Free((ulong)thunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
public nint AddFirstChanceHandler(nint thunk)
|
||||||
|
{
|
||||||
|
return (nint)AddVectoredExceptionHandler(1u, thunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveHandler(nint handle)
|
||||||
|
{
|
||||||
|
_ = RemoveVectoredExceptionHandler((void*)handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetUnhandledFilter(nint thunk)
|
||||||
|
{
|
||||||
|
_ = SetUnhandledExceptionFilter(thunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EmitByte(byte* code, ref int offset, byte value)
|
||||||
|
{
|
||||||
|
code[offset++] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EmitUInt32(byte* code, ref int offset, uint value)
|
||||||
|
{
|
||||||
|
*(uint*)(code + offset) = value;
|
||||||
|
offset += sizeof(uint);
|
||||||
|
}
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll")]
|
||||||
|
private static partial void* AddVectoredExceptionHandler(uint first, IntPtr handler);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll")]
|
||||||
|
private static partial uint RemoveVectoredExceptionHandler(void* handle);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll")]
|
||||||
|
private static partial IntPtr SetUnhandledExceptionFilter(IntPtr lpTopLevelExceptionFilter);
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ using SharpEmu.HLE;
|
|||||||
|
|
||||||
namespace SharpEmu.Core.Cpu;
|
namespace SharpEmu.Core.Cpu;
|
||||||
|
|
||||||
public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemoryAllocator
|
public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemoryAllocator, ICpuMemoryWrapper
|
||||||
{
|
{
|
||||||
private readonly ICpuMemory _inner;
|
private readonly ICpuMemory _inner;
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ namespace SharpEmu.Core.Loader;
|
|||||||
public sealed class SelfLoader : ISelfLoader
|
public sealed class SelfLoader : ISelfLoader
|
||||||
{
|
{
|
||||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Loader");
|
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Loader");
|
||||||
private const uint SelfMagic = 0x4F153D1D;
|
private const uint ElfMagic = 0x7F454C46;
|
||||||
|
private const uint Ps4SelfMagic = 0x4F153D1D;
|
||||||
|
private const uint Ps5SelfMagic = 0x5414F5EE;
|
||||||
private const ulong SelfSegmentFlag = 0x800;
|
private const ulong SelfSegmentFlag = 0x800;
|
||||||
private const int PageSize = 0x1000;
|
private const int PageSize = 0x1000;
|
||||||
private const ulong ImportStubBaseAddress = 0x0000_7000_0000_0000UL;
|
private const ulong ImportStubBaseAddress = 0x0000_7000_0000_0000UL;
|
||||||
@@ -323,7 +325,8 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
throw new InvalidDataException("Input image is too small to contain an ELF header.");
|
throw new InvalidDataException("Input image is too small to contain an ELF header.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (imageData.Length >= sizeof(uint) && BinaryPrimitives.ReadUInt32BigEndian(imageData[..sizeof(uint)]) == SelfMagic)
|
var magic = BinaryPrimitives.ReadUInt32BigEndian(imageData[..sizeof(uint)]);
|
||||||
|
if (magic is Ps4SelfMagic or Ps5SelfMagic)
|
||||||
{
|
{
|
||||||
var selfHeader = ReadUnmanaged<SelfHeader>(imageData, 0);
|
var selfHeader = ReadUnmanaged<SelfHeader>(imageData, 0);
|
||||||
if (!selfHeader.HasKnownLayout || selfHeader.Unknown != 0x22)
|
if (!selfHeader.HasKnownLayout || selfHeader.Unknown != 0x22)
|
||||||
@@ -345,6 +348,12 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
return new LoadContext(IsSelf: true, elfOffset, selfHeader.FileSize, segments);
|
return new LoadContext(IsSelf: true, elfOffset, selfHeader.FileSize, segments);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (magic != ElfMagic)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"Unsupported executable signature 0x{magic:X8}");
|
||||||
|
}
|
||||||
|
|
||||||
return new LoadContext(IsSelf: false, ElfOffset: 0, SelfFileSize: 0, Array.Empty<SelfSegment>());
|
return new LoadContext(IsSelf: false, ElfOffset: 0, SelfFileSize: 0, Array.Empty<SelfSegment>());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2380,10 +2389,14 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
public ulong FileSize => _fileSize;
|
public ulong FileSize => _fileSize;
|
||||||
|
|
||||||
public bool HasKnownLayout =>
|
public bool HasKnownLayout =>
|
||||||
_ident0 == 0x4F &&
|
((_ident0 == 0x4F &&
|
||||||
_ident1 == 0x15 &&
|
_ident1 == 0x15 &&
|
||||||
_ident2 == 0x3D &&
|
_ident2 == 0x3D &&
|
||||||
_ident3 == 0x1D &&
|
_ident3 == 0x1D) ||
|
||||||
|
(_ident0 == 0x54 &&
|
||||||
|
_ident1 == 0x14 &&
|
||||||
|
_ident2 == 0xF5 &&
|
||||||
|
_ident3 == 0xEE)) &&
|
||||||
_ident4 == 0x00 &&
|
_ident4 == 0x00 &&
|
||||||
_ident5 == 0x01 &&
|
_ident5 == 0x01 &&
|
||||||
_ident6 == 0x01 &&
|
_ident6 == 0x01 &&
|
||||||
|
|||||||
@@ -4,11 +4,12 @@
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using SharpEmu.Core.Loader;
|
using SharpEmu.Core.Loader;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
using SharpEmu.Logging;
|
using SharpEmu.Logging;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Memory;
|
namespace SharpEmu.Core.Memory;
|
||||||
|
|
||||||
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryAllocator, IDisposable
|
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryAllocator, IGuestAddressSpace, IDisposable
|
||||||
{
|
{
|
||||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("VMEM");
|
private static readonly SharpEmuLogger Log = SharpEmuLog.For("VMEM");
|
||||||
|
|
||||||
@@ -28,41 +29,26 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
private const ulong DefaultLazyReservePrimeBytes = 0x0400_0000UL; // 64 MiB
|
private const ulong DefaultLazyReservePrimeBytes = 0x0400_0000UL; // 64 MiB
|
||||||
private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB
|
private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB
|
||||||
|
|
||||||
private const uint MEM_COMMIT = 0x1000;
|
// Raw Windows PAGE_* values retained for the internal region/protection
|
||||||
private const uint MEM_RESERVE = 0x2000;
|
// bookkeeping: regions and saved old-protection values always carry the raw
|
||||||
private const uint MEM_RELEASE = 0x8000;
|
// value of the host platform in use, and these classification helpers only
|
||||||
|
// ever see values this class itself assigned (see IHostMemory.ProtectRaw).
|
||||||
private const uint PAGE_EXECUTE_READ = 0x20;
|
private const uint PAGE_EXECUTE_READ = 0x20;
|
||||||
private const uint PAGE_EXECUTE_READWRITE = 0x40;
|
private const uint PAGE_EXECUTE_READWRITE = 0x40;
|
||||||
private const uint PAGE_EXECUTE = 0x10;
|
private const uint PAGE_EXECUTE = 0x10;
|
||||||
private const uint PAGE_EXECUTE_WRITECOPY = 0x80;
|
private const uint PAGE_EXECUTE_WRITECOPY = 0x80;
|
||||||
private const uint PAGE_NOACCESS = 0x01;
|
|
||||||
private const uint PAGE_READWRITE = 0x04;
|
private const uint PAGE_READWRITE = 0x04;
|
||||||
private const uint PAGE_READONLY = 0x02;
|
private const uint PAGE_READONLY = 0x02;
|
||||||
|
|
||||||
|
private readonly IHostMemory _hostMemory;
|
||||||
private ulong _guestAllocationArenaBase;
|
private ulong _guestAllocationArenaBase;
|
||||||
private ulong _guestAllocationOffset;
|
private ulong _guestAllocationOffset;
|
||||||
private static readonly ulong LazyReservePrimeBytes = ResolveLazyReservePrimeBytes();
|
private static readonly ulong LazyReservePrimeBytes = ResolveLazyReservePrimeBytes();
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
public PhysicalVirtualMemory(IHostMemory? hostMemory = null)
|
||||||
private static extern void* VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
{
|
||||||
|
_hostMemory = hostMemory ?? HostPlatform.Current.Memory;
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
}
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
|
||||||
private static extern bool VirtualFree(void* lpAddress, nuint dwSize, uint dwFreeType);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
|
||||||
private static extern bool VirtualProtect(void* lpAddress, nuint dwSize, uint flNewProtect, out uint lpflOldProtect);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll")]
|
|
||||||
private static extern nuint VirtualQuery(void* lpAddress, out MemoryBasicInformation64 lpBuffer, nuint dwLength);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll")]
|
|
||||||
private static extern void* GetCurrentProcess();
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
|
||||||
private static extern bool FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
|
|
||||||
|
|
||||||
public bool TryAllocateAtExact(ulong desiredAddress, ulong size, bool executable, out ulong actualAddress)
|
public bool TryAllocateAtExact(ulong desiredAddress, ulong size, bool executable, out ulong actualAddress)
|
||||||
{
|
{
|
||||||
@@ -74,17 +60,17 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
var alignedSize = (size + 0xFFF) & ~0xFFFUL;
|
var alignedSize = (size + 0xFFF) & ~0xFFFUL;
|
||||||
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
||||||
var allocationType = MEM_COMMIT | MEM_RESERVE;
|
var hostProtection = executable ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite;
|
||||||
var result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, allocationType, protection);
|
var result = _hostMemory.Allocate(desiredAddress, alignedSize, hostProtection);
|
||||||
if (result == null)
|
if (result == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
actualAddress = (ulong)result;
|
actualAddress = result;
|
||||||
if (actualAddress != desiredAddress)
|
if (actualAddress != desiredAddress)
|
||||||
{
|
{
|
||||||
VirtualFree(result, 0, MEM_RELEASE);
|
_hostMemory.Free(result);
|
||||||
actualAddress = 0;
|
actualAddress = 0;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -119,33 +105,33 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
var alignedSize = (size + 0xFFF) & ~0xFFFUL;
|
var alignedSize = (size + 0xFFF) & ~0xFFFUL;
|
||||||
|
|
||||||
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
||||||
var allocationType = MEM_COMMIT | MEM_RESERVE;
|
var hostProtection = executable ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite;
|
||||||
var reservedOnly = false;
|
var reservedOnly = false;
|
||||||
var preferReserveOnly = !executable &&
|
var preferReserveOnly = !executable &&
|
||||||
alignedSize >= LargeDataReserveThreshold &&
|
alignedSize >= LargeDataReserveThreshold &&
|
||||||
alignedSize > FullCommitRegionLimit;
|
alignedSize > FullCommitRegionLimit;
|
||||||
|
|
||||||
void* result = null;
|
ulong result = 0;
|
||||||
if (preferReserveOnly)
|
if (preferReserveOnly)
|
||||||
{
|
{
|
||||||
result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, MEM_RESERVE, PAGE_READWRITE);
|
result = _hostMemory.Reserve(desiredAddress, alignedSize, HostPageProtection.ReadWrite);
|
||||||
if (result == null && allowAlternative)
|
if (result == 0 && allowAlternative)
|
||||||
{
|
{
|
||||||
result = VirtualAlloc(null, (nuint)alignedSize, MEM_RESERVE, PAGE_READWRITE);
|
result = _hostMemory.Reserve(0, alignedSize, HostPageProtection.ReadWrite);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result != null)
|
if (result != 0)
|
||||||
{
|
{
|
||||||
reservedOnly = true;
|
reservedOnly = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result == null)
|
if (result == 0)
|
||||||
{
|
{
|
||||||
result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, allocationType, protection);
|
result = _hostMemory.Allocate(desiredAddress, alignedSize, hostProtection);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result == null)
|
if (result == 0)
|
||||||
{
|
{
|
||||||
if (!allowAlternative)
|
if (!allowAlternative)
|
||||||
{
|
{
|
||||||
@@ -153,32 +139,32 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
|
|
||||||
TraceVmem($"Could not allocate at 0x{desiredAddress:X16}, trying any address...");
|
TraceVmem($"Could not allocate at 0x{desiredAddress:X16}, trying any address...");
|
||||||
result = VirtualAlloc(null, (nuint)alignedSize, allocationType, protection);
|
result = _hostMemory.Allocate(0, alignedSize, hostProtection);
|
||||||
|
|
||||||
if (result == null)
|
if (result == 0)
|
||||||
{
|
{
|
||||||
if (!executable)
|
if (!executable)
|
||||||
{
|
{
|
||||||
result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, MEM_RESERVE, PAGE_READWRITE);
|
result = _hostMemory.Reserve(desiredAddress, alignedSize, HostPageProtection.ReadWrite);
|
||||||
if (result == null && allowAlternative)
|
if (result == 0 && allowAlternative)
|
||||||
{
|
{
|
||||||
result = VirtualAlloc(null, (nuint)alignedSize, MEM_RESERVE, PAGE_READWRITE);
|
result = _hostMemory.Reserve(0, alignedSize, HostPageProtection.ReadWrite);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result != null)
|
if (result != 0)
|
||||||
{
|
{
|
||||||
reservedOnly = true;
|
reservedOnly = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result == null)
|
if (result == 0)
|
||||||
{
|
{
|
||||||
throw new OutOfMemoryException($"Failed to allocate {alignedSize} bytes of virtual memory");
|
throw new OutOfMemoryException($"Failed to allocate {alignedSize} bytes of virtual memory");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var actualAddress = (ulong)result;
|
var actualAddress = result;
|
||||||
|
|
||||||
var lazyPrimeState = "n/a";
|
var lazyPrimeState = "n/a";
|
||||||
if (reservedOnly)
|
if (reservedOnly)
|
||||||
@@ -191,9 +177,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
{
|
{
|
||||||
var remaining = primeBytes - committedBytes;
|
var remaining = primeBytes - committedBytes;
|
||||||
var chunkBytes = Math.Min(remaining, LazyReservePrimeChunkBytes);
|
var chunkBytes = Math.Min(remaining, LazyReservePrimeChunkBytes);
|
||||||
var commitAddress = (void*)(actualAddress + committedBytes);
|
var commitAddress = actualAddress + committedBytes;
|
||||||
var committed = VirtualAlloc(commitAddress, (nuint)chunkBytes, MEM_COMMIT, PAGE_READWRITE);
|
if (!_hostMemory.Commit(commitAddress, chunkBytes, HostPageProtection.ReadWrite))
|
||||||
if (committed == null)
|
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -336,6 +321,41 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool TryProtect(ulong address, ulong size, GuestPageProtection protection)
|
||||||
|
{
|
||||||
|
if (size == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _hostMemory.Protect(address, size, ResolveProtection(protection), out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reproduces the decomposition KernelMemoryCompatExports.ResolveHostProtection
|
||||||
|
// performed before this seam existed; the Windows backend maps each case back
|
||||||
|
// to the identical PAGE_* value.
|
||||||
|
private static HostPageProtection ResolveProtection(GuestPageProtection protection)
|
||||||
|
{
|
||||||
|
var read = (protection & GuestPageProtection.Read) != 0;
|
||||||
|
var write = (protection & GuestPageProtection.Write) != 0;
|
||||||
|
var execute = (protection & GuestPageProtection.Execute) != 0;
|
||||||
|
|
||||||
|
if (execute)
|
||||||
|
{
|
||||||
|
return write
|
||||||
|
? HostPageProtection.ReadWriteExecute
|
||||||
|
: read
|
||||||
|
? HostPageProtection.ReadExecute
|
||||||
|
: HostPageProtection.Execute;
|
||||||
|
}
|
||||||
|
|
||||||
|
return write
|
||||||
|
? HostPageProtection.ReadWrite
|
||||||
|
: read
|
||||||
|
? HostPageProtection.ReadOnly
|
||||||
|
: HostPageProtection.NoAccess;
|
||||||
|
}
|
||||||
|
|
||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
{
|
||||||
lock (_guestAllocationGate)
|
lock (_guestAllocationGate)
|
||||||
@@ -345,7 +365,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
{
|
{
|
||||||
foreach (var region in _regions)
|
foreach (var region in _regions)
|
||||||
{
|
{
|
||||||
VirtualFree((void*)region.VirtualAddress, 0, MEM_RELEASE);
|
_hostMemory.Free(region.VirtualAddress);
|
||||||
}
|
}
|
||||||
_regions.Clear();
|
_regions.Clear();
|
||||||
_pageProtections.Clear();
|
_pageProtections.Clear();
|
||||||
@@ -430,35 +450,35 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
private void SetProtection(ulong address, ulong size, ProgramHeaderFlags flags)
|
private void SetProtection(ulong address, ulong size, ProgramHeaderFlags flags)
|
||||||
{
|
{
|
||||||
uint protection;
|
HostPageProtection protection;
|
||||||
|
|
||||||
if (flags == ProgramHeaderFlags.None)
|
if (flags == ProgramHeaderFlags.None)
|
||||||
{
|
{
|
||||||
protection = PAGE_NOACCESS;
|
protection = HostPageProtection.NoAccess;
|
||||||
}
|
}
|
||||||
else if ((flags & ProgramHeaderFlags.Execute) != 0)
|
else if ((flags & ProgramHeaderFlags.Execute) != 0)
|
||||||
{
|
{
|
||||||
protection = (flags & ProgramHeaderFlags.Write) != 0
|
protection = (flags & ProgramHeaderFlags.Write) != 0
|
||||||
? PAGE_EXECUTE_READWRITE
|
? HostPageProtection.ReadWriteExecute
|
||||||
: PAGE_EXECUTE_READ;
|
: HostPageProtection.ReadExecute;
|
||||||
}
|
}
|
||||||
else if ((flags & ProgramHeaderFlags.Write) != 0)
|
else if ((flags & ProgramHeaderFlags.Write) != 0)
|
||||||
{
|
{
|
||||||
protection = PAGE_READWRITE;
|
protection = HostPageProtection.ReadWrite;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
protection = PAGE_READONLY;
|
protection = HostPageProtection.ReadOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!VirtualProtect((void*)address, (nuint)size, protection, out _))
|
if (!_hostMemory.Protect(address, size, protection, out _))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"Failed to set memory protection at 0x{address:X16}");
|
throw new InvalidOperationException($"Failed to set memory protection at 0x{address:X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((flags & ProgramHeaderFlags.Execute) != 0)
|
if ((flags & ProgramHeaderFlags.Execute) != 0)
|
||||||
{
|
{
|
||||||
FlushInstructionCache(GetCurrentProcess(), (void*)address, (nuint)size);
|
_hostMemory.FlushInstructionCache(address, size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -730,7 +750,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!VirtualProtect(destPtr, (nuint)source.Length, PAGE_EXECUTE_READWRITE, out var oldProtect))
|
if (!_hostMemory.Protect((ulong)destPtr, (ulong)source.Length, HostPageProtection.ReadWriteExecute, out var oldProtect))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -744,10 +764,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _);
|
_hostMemory.ProtectRaw((ulong)destPtr, (ulong)source.Length, oldProtect, out _);
|
||||||
if (IsExecutableProtection(oldProtect))
|
if (IsExecutableProtection(oldProtect))
|
||||||
{
|
{
|
||||||
FlushInstructionCache(GetCurrentProcess(), destPtr, (nuint)source.Length);
|
_hostMemory.FlushInstructionCache((ulong)destPtr, (ulong)source.Length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -973,12 +993,12 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return protection is PAGE_READWRITE or PAGE_EXECUTE_READWRITE;
|
return protection is PAGE_READWRITE or PAGE_EXECUTE_READWRITE;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static uint GetCommitProtection(MemoryRegion region)
|
private static HostPageProtection GetCommitProtection(MemoryRegion region)
|
||||||
{
|
{
|
||||||
return region.IsExecutable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
return region.IsExecutable ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static unsafe bool EnsureRangeCommitted(ulong address, ulong size, MemoryRegion region)
|
private bool EnsureRangeCommitted(ulong address, ulong size, MemoryRegion region)
|
||||||
{
|
{
|
||||||
if (size == 0 || !region.IsReservedOnly)
|
if (size == 0 || !region.IsReservedOnly)
|
||||||
{
|
{
|
||||||
@@ -992,7 +1012,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
var pageAddress = startPage;
|
var pageAddress = startPage;
|
||||||
while (pageAddress < endPage)
|
while (pageAddress < endPage)
|
||||||
{
|
{
|
||||||
if (VirtualQuery((void*)pageAddress, out var info, (nuint)sizeof(MemoryBasicInformation64)) == 0)
|
if (!_hostMemory.Query(pageAddress, out var info))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1006,19 +1026,19 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info.State == MEM_COMMIT)
|
if (info.State == HostRegionState.Committed)
|
||||||
{
|
{
|
||||||
pageAddress = rangeEnd;
|
pageAddress = rangeEnd;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info.State != MEM_RESERVE)
|
if (info.State != HostRegionState.Reserved)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var commitSize = rangeEnd - pageAddress;
|
var commitSize = rangeEnd - pageAddress;
|
||||||
if (VirtualAlloc((void*)pageAddress, (nuint)commitSize, MEM_COMMIT, commitProtection) == null)
|
if (!_hostMemory.Commit(pageAddress, commitSize, commitProtection))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1039,11 +1059,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
var startPage = AlignDown(address, PageSize);
|
var startPage = AlignDown(address, PageSize);
|
||||||
var endPage = AlignUp(address + size, PageSize);
|
var endPage = AlignUp(address + size, PageSize);
|
||||||
var temporaryProtection = region.IsExecutable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
var temporaryProtection = region.IsExecutable ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite;
|
||||||
|
|
||||||
for (var pageAddress = startPage; pageAddress < endPage; pageAddress += PageSize)
|
for (var pageAddress = startPage; pageAddress < endPage; pageAddress += PageSize)
|
||||||
{
|
{
|
||||||
if (!VirtualProtect((void*)pageAddress, (nuint)PageSize, temporaryProtection, out var oldProtection))
|
if (!_hostMemory.Protect(pageAddress, PageSize, temporaryProtection, out var oldProtection))
|
||||||
{
|
{
|
||||||
RestorePageProtections(touchedPages);
|
RestorePageProtections(touchedPages);
|
||||||
touchedPages.Clear();
|
touchedPages.Clear();
|
||||||
@@ -1056,11 +1076,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RestorePageProtections(List<(ulong Address, uint Protection)> touchedPages)
|
private void RestorePageProtections(List<(ulong Address, uint Protection)> touchedPages)
|
||||||
{
|
{
|
||||||
foreach (var (pageAddress, protection) in touchedPages)
|
foreach (var (pageAddress, protection) in touchedPages)
|
||||||
{
|
{
|
||||||
VirtualProtect((void*)pageAddress, (nuint)PageSize, protection, out _);
|
_hostMemory.ProtectRaw(pageAddress, PageSize, protection, out _);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1117,16 +1137,4 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
public uint Protection { get; set; }
|
public uint Protection { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct MemoryBasicInformation64
|
|
||||||
{
|
|
||||||
public ulong BaseAddress;
|
|
||||||
public ulong AllocationBase;
|
|
||||||
public uint AllocationProtect;
|
|
||||||
public uint Alignment1;
|
|
||||||
public ulong RegionSize;
|
|
||||||
public uint State;
|
|
||||||
public uint Protect;
|
|
||||||
public uint Type;
|
|
||||||
public uint Alignment2;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using SharpEmu.Core.Cpu.Disasm;
|
|||||||
using SharpEmu.Core.Loader;
|
using SharpEmu.Core.Loader;
|
||||||
using SharpEmu.Core.Memory;
|
using SharpEmu.Core.Memory;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
using SharpEmu.Libs.VideoOut;
|
using SharpEmu.Libs.VideoOut;
|
||||||
using SharpEmu.Libs.Kernel;
|
using SharpEmu.Libs.Kernel;
|
||||||
using SharpEmu.Libs.AppContent;
|
using SharpEmu.Libs.AppContent;
|
||||||
@@ -86,14 +87,19 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
moduleManager.RegisterFromAssembly(typeof(KernelExports).Assembly, Generation.Gen4 | Generation.Gen5, Aerolib.Instance);
|
moduleManager.RegisterFromAssembly(typeof(KernelExports).Assembly, Generation.Gen4 | Generation.Gen5, Aerolib.Instance);
|
||||||
moduleManager.Freeze();
|
moduleManager.Freeze();
|
||||||
|
|
||||||
var virtualMemory = new PhysicalVirtualMemory();
|
// Resolve the host platform once at the composition root; on unsupported
|
||||||
|
// OSes this throws PlatformNotSupportedException with a clear message
|
||||||
|
// instead of failing on the first native call.
|
||||||
|
var hostPlatform = HostPlatform.Current;
|
||||||
|
|
||||||
|
var virtualMemory = new PhysicalVirtualMemory(hostPlatform.Memory);
|
||||||
|
|
||||||
var fileSystem = new PhysicalFileSystem();
|
var fileSystem = new PhysicalFileSystem();
|
||||||
|
|
||||||
return new SharpEmuRuntime(
|
return new SharpEmuRuntime(
|
||||||
new SelfLoader(),
|
new SelfLoader(),
|
||||||
virtualMemory,
|
virtualMemory,
|
||||||
new CpuDispatcher(virtualMemory, moduleManager),
|
new CpuDispatcher(virtualMemory, moduleManager, hostPlatform: hostPlatform),
|
||||||
moduleManager,
|
moduleManager,
|
||||||
Aerolib.Instance,
|
Aerolib.Instance,
|
||||||
cpuExecutionOptions,
|
cpuExecutionOptions,
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ public sealed class GuiSettings
|
|||||||
/// <summary>Publish launcher/game status to Discord Rich Presence.</summary>
|
/// <summary>Publish launcher/game status to Discord Rich Presence.</summary>
|
||||||
public bool DiscordRichPresence { get; set; } = true;
|
public bool DiscordRichPresence { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>Names of SHARPEMU_* switches set to "1" in the emulator's environment at launch.</summary>
|
||||||
|
public List<string> EnvironmentToggles { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Discord application ID used for Rich Presence; the default is the
|
/// Discord application ID used for Rich Presence; the default is the
|
||||||
/// SharpEmu application. Override to rebrand what Discord shows as
|
/// SharpEmu application. Override to rebrand what Discord shows as
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"Library.Empty.Title": "Deine Bibliothek ist leer",
|
"Library.Empty.Title": "Deine Bibliothek ist leer",
|
||||||
"Library.Empty.Hint": "Füge einen Ordner mit deinen Spielen hinzu, um zu beginnen.",
|
"Library.Empty.Hint": "Füge einen Ordner mit deinen Spielen hinzu, um zu beginnen.",
|
||||||
"Library.Empty.SearchTitle": "Keine Spiele gefunden",
|
"Library.Empty.SearchTitle": "Keine Spiele gefunden",
|
||||||
"Library.Empty.SearchHint": "Nichts in der Bibliothek entspricht "{0}".",
|
"Library.Empty.SearchHint": "Nichts in der Bibliothek entspricht “{0}”.",
|
||||||
"Library.Empty.AddFolder": "+ Spielordner hinzufügen",
|
"Library.Empty.AddFolder": "+ Spielordner hinzufügen",
|
||||||
|
|
||||||
"Library.Loading": "Bibliothek wird geladen…",
|
"Library.Loading": "Bibliothek wird geladen…",
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
"Status.LibraryScanned": "Bibliothek gescannt: {0} Spiel(e) in {1} Ordner(n).",
|
"Status.LibraryScanned": "Bibliothek gescannt: {0} Spiel(e) in {1} Ordner(n).",
|
||||||
"Status.CouldNotOpenFolder": "Ordner konnte nicht geöffnet werden: {0}",
|
"Status.CouldNotOpenFolder": "Ordner konnte nicht geöffnet werden: {0}",
|
||||||
"Status.CopiedToClipboard": "{0} in die Zwischenablage kopiert.",
|
"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.RemovedFromLibrary": "“{0}” wurde aus der Bibliothek entfernt. Füge den Ordner erneut hinzu, um es wiederherzustellen.",
|
||||||
"Status.Running": "Läuft {0}",
|
"Status.Running": "Läuft {0}",
|
||||||
"Status.Stopping": "Wird gestoppt…",
|
"Status.Stopping": "Wird gestoppt…",
|
||||||
"Status.Idle": "Bereit",
|
"Status.Idle": "Bereit",
|
||||||
|
|||||||
@@ -26,6 +26,15 @@
|
|||||||
"Library.Loading": "Loading library…",
|
"Library.Loading": "Loading library…",
|
||||||
|
|
||||||
"Options.General": "General",
|
"Options.General": "General",
|
||||||
|
"Options.Env.Tab": "Environment",
|
||||||
|
"Options.Section.Environment": "ENVIRONMENT VARIABLES",
|
||||||
|
"Options.Env.Desc": "Switches passed to the emulator as environment variables at launch.",
|
||||||
|
"Options.Env.Bthid.Desc": "Report Bluetooth HID as unavailable for titles whose wheel/FFB middleware polls forever.\nLeave off normally. Some titles freeze when init fails.",
|
||||||
|
"Options.Env.LoopGuard.Desc": "Do not force quit titles that repeat the same call for too long.\nTry this when a game exits on its own while loading.",
|
||||||
|
"Options.Env.VkValidation.Desc": "Enable Vulkan validation layers for GPU debugging.\nSlow. Requires the Vulkan SDK to be installed.",
|
||||||
|
"Options.Env.DumpSpirv.Desc": "Dump AGC shaders and their SPIR-V translations to the shader-dumps folder.\nUse when reporting shader or rendering bugs.",
|
||||||
|
"Options.Env.LogDirectMemory.Desc": "Log direct memory allocations and failures to the console.\nUse when a game aborts or exits during boot.",
|
||||||
|
"Options.Env.LogNp.Desc": "Log NP (PlayStation Network) library calls to the console.",
|
||||||
"Options.Section.Emulation": "EMULATION",
|
"Options.Section.Emulation": "EMULATION",
|
||||||
"Options.Section.Logging": "LOGGING",
|
"Options.Section.Logging": "LOGGING",
|
||||||
"Options.Section.Launcher": "LAUNCHER",
|
"Options.Section.Launcher": "LAUNCHER",
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"_languageName": "Nederlands",
|
||||||
|
|
||||||
|
"Page.Library": "Bibliotheek",
|
||||||
|
"Page.Options": "Opties",
|
||||||
|
"Page.GameCount.One": "1 game",
|
||||||
|
"Page.GameCount.Other": "{0} games",
|
||||||
|
|
||||||
|
"Library.SearchWatermark": "Zoeken in bibliotheek…",
|
||||||
|
"Library.AddFolder": "+ Map toevoegen",
|
||||||
|
"Library.Rescan": "⟳ Opnieuw scannen",
|
||||||
|
"Library.OpenFile": "Bestand openen…",
|
||||||
|
|
||||||
|
"Library.Context.Launch": "Starten",
|
||||||
|
"Library.Context.OpenFolder": "Gamemap openen",
|
||||||
|
"Library.Context.CopyPath": "Pad kopiëren",
|
||||||
|
"Library.Context.CopyTitleId": "Titel-ID kopiëren",
|
||||||
|
"Library.Context.Remove": "Verwijderen uit bibliotheek",
|
||||||
|
|
||||||
|
"Library.Empty.Title": "Je bibliotheek is leeg",
|
||||||
|
"Library.Empty.Hint": "Voeg een map met je games toe om te beginnen.",
|
||||||
|
"Library.Empty.SearchTitle": "Geen games komen overeen met je zoekopdracht",
|
||||||
|
"Library.Empty.SearchHint": "Niets in de bibliotheek komt overeen met “{0}”.",
|
||||||
|
"Library.Empty.AddFolder": "+ Gamemap toevoegen",
|
||||||
|
|
||||||
|
"Library.Loading": "Bibliotheek laden…",
|
||||||
|
|
||||||
|
"Options.General": "Algemeen",
|
||||||
|
"Options.Section.Emulation": "EMULATIE",
|
||||||
|
"Options.Section.Logging": "LOGGING",
|
||||||
|
"Options.Section.Launcher": "LAUNCHER",
|
||||||
|
|
||||||
|
"Options.CpuEngine.Label": "CPU-engine",
|
||||||
|
"Options.CpuEngine.Desc": "Engine die wordt gebruikt om gamecode uit te voeren.",
|
||||||
|
"Options.CpuEngine.Native": "Native",
|
||||||
|
|
||||||
|
"Options.Strict.Label": "Strikte dynlib-resolutie",
|
||||||
|
"Options.Strict.Desc": "Laat het opstarten mislukken wanneer een geïmporteerd symbool niet kan worden opgelost.",
|
||||||
|
|
||||||
|
"Options.LogLevel.Label": "Logniveau",
|
||||||
|
"Options.LogLevel.Desc": "Uitgebreidheid van de console-uitvoer van de emulator.",
|
||||||
|
"Options.LogLevel.Trace": "Trace",
|
||||||
|
"Options.LogLevel.Debug": "Debug",
|
||||||
|
"Options.LogLevel.Info": "Info",
|
||||||
|
"Options.LogLevel.Warning": "Waarschuwing",
|
||||||
|
"Options.LogLevel.Error": "Fout",
|
||||||
|
"Options.LogLevel.Critical": "Kritiek",
|
||||||
|
|
||||||
|
"Options.TraceImports.Label": "Tracelimiet voor imports",
|
||||||
|
"Options.TraceImports.Desc": "Traceer de eerste N imports per module (0 = uit).",
|
||||||
|
|
||||||
|
"Options.LogToFile.Label": "Loggen naar bestand",
|
||||||
|
"Options.LogToFile.Desc": "Stuur de uitvoer van de emulator ook naar een logbestand.",
|
||||||
|
|
||||||
|
"Options.LogFilePath.Label": "Pad naar logbestand",
|
||||||
|
"Options.LogFilePath.Default": "Geen aangepast pad — logs komen terecht in user/logs naast de emulator.",
|
||||||
|
"Options.LogFilePath.Select": "Selecteren…",
|
||||||
|
|
||||||
|
"Options.OverrideLogFile.Label": "Logbestand overschrijven",
|
||||||
|
"Options.OverrideLogFile.Desc": "Gebruik het exacte bestandspad in plaats van de titel-ID en tijdstempel toe te voegen.",
|
||||||
|
|
||||||
|
"Options.TitleMusic.Label": "Titelmuziek",
|
||||||
|
"Options.TitleMusic.Desc": "Herhaal de voorbeeldmuziek van de geselecteerde game in de bibliotheek.",
|
||||||
|
|
||||||
|
"Options.Discord.Label": "Discord-aanwezigheid",
|
||||||
|
"Options.Discord.Desc": "Toon de actieve game op je Discord-profiel.",
|
||||||
|
|
||||||
|
"Options.Language.Label": "Taal van de emulator",
|
||||||
|
"Options.Language.Desc": "Taal die in de hele launcher wordt gebruikt. Wordt direct toegepast.",
|
||||||
|
|
||||||
|
"Common.On": "Aan",
|
||||||
|
"Common.Off": "Uit",
|
||||||
|
|
||||||
|
"Console.Title": "CONSOLE",
|
||||||
|
"Console.SearchWatermark": "Zoeken...",
|
||||||
|
"Console.AutoScroll": "Automatisch scrollen",
|
||||||
|
"Console.Split": "Splitsen",
|
||||||
|
"Console.Copy": "Kopiëren",
|
||||||
|
"Console.Clear": "Wissen",
|
||||||
|
"Console.WindowTitle": "SharpEmu-console",
|
||||||
|
|
||||||
|
"Launch.NoGameSelected": "Geen game geselecteerd",
|
||||||
|
"Launch.NoGameHint": "Kies een game uit de bibliotheek, of open direct een eboot.bin-bestand.",
|
||||||
|
"Launch.Idle": "Inactief",
|
||||||
|
"Launch.Console": "≡ Console",
|
||||||
|
"Launch.Launch": "▶ Starten",
|
||||||
|
"Launch.Stop": "■ Stoppen",
|
||||||
|
"Launch.Running": "Actief — {0}",
|
||||||
|
"Launch.Stopping": "Stoppen…",
|
||||||
|
"Launch.Exited": "Afgesloten met code {0} ({1})",
|
||||||
|
"Launch.ExeNotFound": "SharpEmu-uitvoerbaar bestand niet gevonden. Bouw eerst het SharpEmu.CLI-project (dotnet build).",
|
||||||
|
"Launch.LogFile": "Logbestand: {0}",
|
||||||
|
"Launch.Command": "$ SharpEmu {0}",
|
||||||
|
"Launch.StartFailed": "Starten van de emulator mislukt: {0}",
|
||||||
|
"Launch.ProcessExited": "Proces afgesloten met code {0} ({1}).",
|
||||||
|
|
||||||
|
"Exit.Ok": "OK",
|
||||||
|
"Exit.InvalidArguments": "ongeldige argumenten",
|
||||||
|
"Exit.EbootNotFound": "eboot niet gevonden",
|
||||||
|
"Exit.RuntimeException": "runtime-uitzondering",
|
||||||
|
"Exit.EmulationError": "emulatiefout",
|
||||||
|
"Exit.Unknown": "onbekend",
|
||||||
|
|
||||||
|
"Status.EmulatorLocating": "Emulator: zoeken…",
|
||||||
|
"Status.EmulatorPath": "Emulator: {0}",
|
||||||
|
"Status.EmulatorNotFound": "Emulator: SharpEmu-uitvoerbaar bestand niet gevonden — bouw eerst SharpEmu.CLI.",
|
||||||
|
"Status.ScanningLibrary": "Bibliotheek scannen…",
|
||||||
|
"Status.AddFolderPrompt": "Voeg een gamemap toe om de bibliotheek te vullen.",
|
||||||
|
"Status.LibraryScanned": "Bibliotheek gescand: {0} game(s) in {1} map(pen).",
|
||||||
|
"Status.CouldNotOpenFolder": "Kan map niet openen: {0}",
|
||||||
|
"Status.CopiedToClipboard": "{0} gekopieerd naar klembord.",
|
||||||
|
"Status.RemovedFromLibrary": "“{0}” verwijderd uit de bibliotheek. Voeg de map opnieuw toe om dit te herstellen.",
|
||||||
|
"Status.Running": "Actief {0}",
|
||||||
|
"Status.Stopping": "Stoppen…",
|
||||||
|
"Status.Idle": "Inactief",
|
||||||
|
|
||||||
|
"Clipboard.Path": "Pad",
|
||||||
|
"Clipboard.TitleId": "Titel-ID",
|
||||||
|
|
||||||
|
"Discord.Playing": "Speelt {0}",
|
||||||
|
"Discord.Browsing": "Bladert door de bibliotheek",
|
||||||
|
|
||||||
|
"Dialog.ChooseGameFolder": "Kies een map met games",
|
||||||
|
"Dialog.OpenExecutable": "Open een uitvoerbaar bestand om te starten",
|
||||||
|
"Dialog.PsExecutables": "PS-uitvoerbare bestanden",
|
||||||
|
"Dialog.SaveLogFile": "Selecteer waar het logbestand moet worden opgeslagen",
|
||||||
|
"Dialog.PlainTextFiles": "Platte tekstbestanden",
|
||||||
|
"Dialog.LogFiles": "Logbestanden"
|
||||||
|
}
|
||||||
@@ -387,6 +387,88 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
<TabItem x:Name="EnvTabItem" Header="Environment" FontSize="15">
|
||||||
|
<ScrollViewer>
|
||||||
|
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
|
||||||
|
|
||||||
|
<Border Classes="card">
|
||||||
|
<StackPanel Spacing="14">
|
||||||
|
<TextBlock x:Name="EnvSectionTitle" Classes="sectionTitle" Text="ENVIRONMENT VARIABLES" />
|
||||||
|
<TextBlock x:Name="EnvDesc"
|
||||||
|
Text="Switches passed to the emulator as environment variables at launch."
|
||||||
|
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||||
|
<TextBlock Text="SHARPEMU_BTHID_UNAVAILABLE" FontSize="13" FontFamily="Consolas,monospace" />
|
||||||
|
<TextBlock x:Name="EnvBthidDesc"
|
||||||
|
Text="Report Bluetooth HID as unavailable for titles whose wheel/FFB middleware polls forever. Leave off normally. Some titles freeze when init fails."
|
||||||
|
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
<ToggleSwitch Grid.Column="1" x:Name="EnvBthidToggle" OnContent="On" OffContent="Off"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||||
|
<TextBlock Text="SHARPEMU_DISABLE_IMPORT_LOOP_GUARD" FontSize="13" FontFamily="Consolas,monospace" />
|
||||||
|
<TextBlock x:Name="EnvLoopGuardDesc"
|
||||||
|
Text="Do not force quit titles that repeat the same call for too long. Try this when a game exits on its own while loading."
|
||||||
|
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
<ToggleSwitch Grid.Column="1" x:Name="EnvLoopGuardToggle" OnContent="On" OffContent="Off"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||||
|
<TextBlock Text="SHARPEMU_VK_VALIDATION" FontSize="13" FontFamily="Consolas,monospace" />
|
||||||
|
<TextBlock x:Name="EnvVkValidationDesc"
|
||||||
|
Text="Enable Vulkan validation layers for GPU debugging. Slow. Requires the Vulkan SDK to be installed."
|
||||||
|
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
<ToggleSwitch Grid.Column="1" x:Name="EnvVkValidationToggle" OnContent="On" OffContent="Off"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||||
|
<TextBlock Text="SHARPEMU_DUMP_SPIRV" FontSize="13" FontFamily="Consolas,monospace" />
|
||||||
|
<TextBlock x:Name="EnvDumpSpirvDesc"
|
||||||
|
Text="Dump AGC shaders and their SPIR-V translations to the shader-dumps folder. Use when reporting shader or rendering bugs."
|
||||||
|
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
<ToggleSwitch Grid.Column="1" x:Name="EnvDumpSpirvToggle" OnContent="On" OffContent="Off"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||||
|
<TextBlock Text="SHARPEMU_LOG_DIRECT_MEMORY" FontSize="13" FontFamily="Consolas,monospace" />
|
||||||
|
<TextBlock x:Name="EnvLogDirectMemoryDesc"
|
||||||
|
Text="Log direct memory allocations and failures to the console. Use when a game aborts or exits during boot."
|
||||||
|
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
<ToggleSwitch Grid.Column="1" x:Name="EnvLogDirectMemoryToggle" OnContent="On" OffContent="Off"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||||
|
<TextBlock Text="SHARPEMU_LOG_NP" FontSize="13" FontFamily="Consolas,monospace" />
|
||||||
|
<TextBlock x:Name="EnvLogNpDesc"
|
||||||
|
Text="Log NP (PlayStation Network) library calls to the console."
|
||||||
|
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
<ToggleSwitch Grid.Column="1" x:Name="EnvLogNpToggle" OnContent="On" OffContent="Off"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</TabItem>
|
||||||
</TabControl>
|
</TabControl>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|||||||
@@ -125,6 +125,18 @@ public partial class MainWindow : Window
|
|||||||
UpdateDiscordPresence();
|
UpdateDiscordPresence();
|
||||||
};
|
};
|
||||||
SelectLogFilePathButton.Click += async (_, _) => await SelectLogFilePathAsync();
|
SelectLogFilePathButton.Click += async (_, _) => await SelectLogFilePathAsync();
|
||||||
|
EnvBthidToggle.IsCheckedChanged += (_, _) =>
|
||||||
|
SetEnvironmentToggle("SHARPEMU_BTHID_UNAVAILABLE", EnvBthidToggle.IsChecked == true);
|
||||||
|
EnvLoopGuardToggle.IsCheckedChanged += (_, _) =>
|
||||||
|
SetEnvironmentToggle("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD", EnvLoopGuardToggle.IsChecked == true);
|
||||||
|
EnvVkValidationToggle.IsCheckedChanged += (_, _) =>
|
||||||
|
SetEnvironmentToggle("SHARPEMU_VK_VALIDATION", EnvVkValidationToggle.IsChecked == true);
|
||||||
|
EnvDumpSpirvToggle.IsCheckedChanged += (_, _) =>
|
||||||
|
SetEnvironmentToggle("SHARPEMU_DUMP_SPIRV", EnvDumpSpirvToggle.IsChecked == true);
|
||||||
|
EnvLogDirectMemoryToggle.IsCheckedChanged += (_, _) =>
|
||||||
|
SetEnvironmentToggle("SHARPEMU_LOG_DIRECT_MEMORY", EnvLogDirectMemoryToggle.IsChecked == true);
|
||||||
|
EnvLogNpToggle.IsCheckedChanged += (_, _) =>
|
||||||
|
SetEnvironmentToggle("SHARPEMU_LOG_NP", EnvLogNpToggle.IsChecked == true);
|
||||||
LanguageBox.SelectionChanged += (_, _) => OnLanguageChanged();
|
LanguageBox.SelectionChanged += (_, _) => OnLanguageChanged();
|
||||||
|
|
||||||
GameList.AddHandler(ContextRequestedEvent, OnGameContextRequested, RoutingStrategies.Tunnel);
|
GameList.AddHandler(ContextRequestedEvent, OnGameContextRequested, RoutingStrategies.Tunnel);
|
||||||
@@ -403,6 +415,15 @@ public partial class MainWindow : Window
|
|||||||
LoadingStateText.Text = loc.Get("Library.Loading");
|
LoadingStateText.Text = loc.Get("Library.Loading");
|
||||||
|
|
||||||
GeneralTabItem.Header = loc.Get("Options.General");
|
GeneralTabItem.Header = loc.Get("Options.General");
|
||||||
|
EnvTabItem.Header = loc.Get("Options.Env.Tab");
|
||||||
|
EnvSectionTitle.Text = loc.Get("Options.Section.Environment");
|
||||||
|
EnvDesc.Text = loc.Get("Options.Env.Desc");
|
||||||
|
EnvBthidDesc.Text = loc.Get("Options.Env.Bthid.Desc");
|
||||||
|
EnvLoopGuardDesc.Text = loc.Get("Options.Env.LoopGuard.Desc");
|
||||||
|
EnvVkValidationDesc.Text = loc.Get("Options.Env.VkValidation.Desc");
|
||||||
|
EnvDumpSpirvDesc.Text = loc.Get("Options.Env.DumpSpirv.Desc");
|
||||||
|
EnvLogDirectMemoryDesc.Text = loc.Get("Options.Env.LogDirectMemory.Desc");
|
||||||
|
EnvLogNpDesc.Text = loc.Get("Options.Env.LogNp.Desc");
|
||||||
EmulationSectionTitle.Text = loc.Get("Options.Section.Emulation");
|
EmulationSectionTitle.Text = loc.Get("Options.Section.Emulation");
|
||||||
LoggingSectionTitle.Text = loc.Get("Options.Section.Logging");
|
LoggingSectionTitle.Text = loc.Get("Options.Section.Logging");
|
||||||
LauncherSectionTitle.Text = loc.Get("Options.Section.Launcher");
|
LauncherSectionTitle.Text = loc.Get("Options.Section.Launcher");
|
||||||
@@ -584,9 +605,34 @@ public partial class MainWindow : Window
|
|||||||
OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile;
|
OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile;
|
||||||
TitleMusicToggle.IsChecked = _settings.PlayTitleMusic;
|
TitleMusicToggle.IsChecked = _settings.PlayTitleMusic;
|
||||||
DiscordToggle.IsChecked = _settings.DiscordRichPresence;
|
DiscordToggle.IsChecked = _settings.DiscordRichPresence;
|
||||||
|
EnvBthidToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_BTHID_UNAVAILABLE");
|
||||||
|
EnvLoopGuardToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD");
|
||||||
|
EnvVkValidationToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_VK_VALIDATION");
|
||||||
|
EnvDumpSpirvToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_DUMP_SPIRV");
|
||||||
|
EnvLogDirectMemoryToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_DIRECT_MEMORY");
|
||||||
|
EnvLogNpToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_NP");
|
||||||
UpdateLogFilePathText();
|
UpdateLogFilePathText();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Environment variables set on this process at the previous launch; children
|
||||||
|
// inherit the process environment, so stale names must be cleared explicitly.
|
||||||
|
private readonly HashSet<string> _appliedEnvironmentVariables = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private void SetEnvironmentToggle(string name, bool enabled)
|
||||||
|
{
|
||||||
|
if (enabled)
|
||||||
|
{
|
||||||
|
if (!_settings.EnvironmentToggles.Contains(name))
|
||||||
|
{
|
||||||
|
_settings.EnvironmentToggles.Add(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_settings.EnvironmentToggles.Remove(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private string SelectedLogLevel()
|
private string SelectedLogLevel()
|
||||||
{
|
{
|
||||||
return LogLevelBox.SelectedIndex switch
|
return LogLevelBox.SelectedIndex switch
|
||||||
@@ -1402,6 +1448,24 @@ public partial class MainWindow : Window
|
|||||||
Localization.Instance.Format("Launch.Command", string.Join(' ', arguments)),
|
Localization.Instance.Format("Launch.Command", string.Join(' ', arguments)),
|
||||||
DimLineBrush);
|
DimLineBrush);
|
||||||
|
|
||||||
|
// Apply the enabled switches to this process; both emulator launch paths
|
||||||
|
// (CreateProcessW and Process.Start) inherit it. Clear switches turned
|
||||||
|
// off since the previous launch.
|
||||||
|
foreach (var staleName in _appliedEnvironmentVariables)
|
||||||
|
{
|
||||||
|
if (!_settings.EnvironmentToggles.Contains(staleName))
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable(staleName, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_appliedEnvironmentVariables.Clear();
|
||||||
|
foreach (var name in _settings.EnvironmentToggles)
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable(name, "1");
|
||||||
|
_appliedEnvironmentVariables.Add(name);
|
||||||
|
}
|
||||||
|
|
||||||
var emulator = new EmulatorProcess();
|
var emulator = new EmulatorProcess();
|
||||||
emulator.OutputReceived += (line, isError) => _pendingLines.Enqueue((line, isError));
|
emulator.OutputReceived += (line, isError) => _pendingLines.Enqueue((line, isError));
|
||||||
emulator.Exited += code => Dispatcher.UIThread.Post(() => OnEmulatorExited(code));
|
emulator.Exited += code => Dispatcher.UIThread.Post(() => OnEmulatorExited(code));
|
||||||
@@ -1499,6 +1563,7 @@ public partial class MainWindow : Window
|
|||||||
2 => "Exit.EbootNotFound",
|
2 => "Exit.EbootNotFound",
|
||||||
3 => "Exit.RuntimeException",
|
3 => "Exit.RuntimeException",
|
||||||
4 => "Exit.EmulationError",
|
4 => "Exit.EmulationError",
|
||||||
|
-1073741819 => "Exit.EmulationError",
|
||||||
_ => "Exit.Unknown",
|
_ => "Exit.Unknown",
|
||||||
};
|
};
|
||||||
var meaning = Localization.Instance.Get(meaningKey);
|
var meaning = Localization.Instance.Get(meaningKey);
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,13 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum GuestPageProtection
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Read = 1,
|
||||||
|
Write = 2,
|
||||||
|
Execute = 4,
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// General-purpose register snapshot of a suspended thread, produced by
|
||||||
|
/// <see cref="IHostThreading.TryCaptureThreadRegisters"/>. Registers are named
|
||||||
|
/// after the guest ISA (x86-64), which every supported host executes natively.
|
||||||
|
/// </summary>
|
||||||
|
public readonly record struct HostCapturedRegisters(
|
||||||
|
ulong Rip,
|
||||||
|
ulong Rsp,
|
||||||
|
ulong Rbp,
|
||||||
|
ulong Rax,
|
||||||
|
ulong Rbx,
|
||||||
|
ulong Rcx,
|
||||||
|
ulong Rdx);
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Platform-neutral page protection. Values intentionally enumerate the exact
|
||||||
|
/// combinations the emulator uses today so each maps 1:1 onto a single native
|
||||||
|
/// protection constant (PAGE_* on Windows, PROT_* elsewhere).
|
||||||
|
/// </summary>
|
||||||
|
public enum HostPageProtection
|
||||||
|
{
|
||||||
|
NoAccess,
|
||||||
|
ReadOnly,
|
||||||
|
ReadWrite,
|
||||||
|
Execute,
|
||||||
|
ReadExecute,
|
||||||
|
ReadWriteExecute,
|
||||||
|
ExecuteWriteCopy,
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using SharpEmu.HLE.Host.Windows;
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Process-wide access point for the host platform backend. Static HLE export
|
||||||
|
/// classes (which cannot receive constructor injection) resolve host primitives
|
||||||
|
/// through <see cref="Current"/>; injectable components should instead accept an
|
||||||
|
/// <see cref="IHostPlatform"/> and merely default to this.
|
||||||
|
/// </summary>
|
||||||
|
public static class HostPlatform
|
||||||
|
{
|
||||||
|
private static readonly Lazy<IHostPlatform> Instance = new(Create);
|
||||||
|
|
||||||
|
public static IHostPlatform Current => Instance.Value;
|
||||||
|
|
||||||
|
private static IHostPlatform Create()
|
||||||
|
{
|
||||||
|
// The Windows backend executes guest x86-64 natively and emits x86-64
|
||||||
|
// stubs, so a native ARM64 process must be rejected here rather than
|
||||||
|
// crash undefined later (x64 processes under emulation report X64).
|
||||||
|
if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.X64)
|
||||||
|
{
|
||||||
|
return new WindowsHostPlatform();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new PlatformNotSupportedException(
|
||||||
|
"SharpEmu native guest execution requires a host platform backend and none exists for this OS/architecture yet (currently Windows x64 only).");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Result of <see cref="IHostMemory.Query"/>. The Raw* fields carry the
|
||||||
|
/// untranslated OS values so call sites migrated from direct VirtualQuery use
|
||||||
|
/// keep comparing (and logging) the exact native words they did before;
|
||||||
|
/// <see cref="State"/> and <see cref="Protection"/> are neutral views.
|
||||||
|
/// </summary>
|
||||||
|
public readonly record struct HostRegionInfo(
|
||||||
|
ulong BaseAddress,
|
||||||
|
ulong AllocationBase,
|
||||||
|
ulong RegionSize,
|
||||||
|
HostRegionState State,
|
||||||
|
uint RawState,
|
||||||
|
HostPageProtection Protection,
|
||||||
|
uint RawProtection,
|
||||||
|
uint RawAllocationProtection);
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host;
|
||||||
|
|
||||||
|
public enum HostRegionState
|
||||||
|
{
|
||||||
|
Free,
|
||||||
|
Reserved,
|
||||||
|
Committed,
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Host functions whose addresses the execution engine bakes into emitted
|
||||||
|
/// stubs (spin-waits, worker run loops, TLS reads). Enum-keyed rather than a
|
||||||
|
/// free-form name lookup: each platform's emitters need their own specific
|
||||||
|
/// functions, and this set is exactly what the current emitters consume.
|
||||||
|
/// </summary>
|
||||||
|
public enum HostRuntimeFunction
|
||||||
|
{
|
||||||
|
TlsGetValue,
|
||||||
|
QueryPerformanceCounter,
|
||||||
|
SwitchToThread,
|
||||||
|
Sleep,
|
||||||
|
WaitForSingleObject,
|
||||||
|
SetEvent,
|
||||||
|
ExitThread,
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Installation mechanics for the process-wide fault interception the execution
|
||||||
|
/// engine relies on to catch guest faults. Deliberately thin: the managed
|
||||||
|
/// handlers keep receiving the platform's raw exception data, and the emitted
|
||||||
|
/// pre-filter thunk is an opaque per-platform unit. Implementations live next
|
||||||
|
/// to the execution backend (SharpEmu.Core), not behind HostPlatform.Current.
|
||||||
|
/// </summary>
|
||||||
|
public interface IHostFaultHandling
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Emits the native thunk that wraps a managed fault handler: it pre-filters
|
||||||
|
/// exception codes that must never enter managed code and, when the fault
|
||||||
|
/// happened on a guest stack, switches to the host stack saved in
|
||||||
|
/// <paramref name="hostRspSwitchTlsSlot"/> before the call. Returns 0 on failure.
|
||||||
|
/// </summary>
|
||||||
|
nint CreateHandlerThunk(nint managedCallback, uint hostRspSwitchTlsSlot, nint tlsGetValueAddress);
|
||||||
|
|
||||||
|
void FreeThunk(nint thunk);
|
||||||
|
|
||||||
|
/// <summary>Installs a first-chance handler ahead of existing ones; returns a removal handle (0 on failure).</summary>
|
||||||
|
nint AddFirstChanceHandler(nint thunk);
|
||||||
|
|
||||||
|
void RemoveHandler(nint handle);
|
||||||
|
|
||||||
|
/// <summary>Installs the last-resort filter; pass 0 to clear.</summary>
|
||||||
|
void SetUnhandledFilter(nint thunk);
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Host page-allocation primitives used by the native execution engine.
|
||||||
|
/// Allocate/Reserve/Commit are deliberately separate members (rather than a
|
||||||
|
/// flags parameter) so every call site maps 1:1 onto the exact native call it
|
||||||
|
/// replaced, keeping the Windows behavior byte-for-byte identical.
|
||||||
|
/// </summary>
|
||||||
|
public interface IHostMemory
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Reserves and commits pages in one step. <paramref name="desiredAddress"/> of 0
|
||||||
|
/// lets the OS choose the address. Returns the base address, or 0 on failure.
|
||||||
|
/// The OS may satisfy the request at a different address than desired; callers
|
||||||
|
/// that require an exact placement must check the result themselves.
|
||||||
|
/// </summary>
|
||||||
|
ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection);
|
||||||
|
|
||||||
|
/// <summary>Reserves address space without committing pages (lazy regions).</summary>
|
||||||
|
ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection);
|
||||||
|
|
||||||
|
/// <summary>Commits pages inside a previously reserved range (fault-path lazy commit).</summary>
|
||||||
|
bool Commit(ulong address, ulong size, HostPageProtection protection);
|
||||||
|
|
||||||
|
/// <summary>Releases an entire allocation or reservation by its base address.</summary>
|
||||||
|
bool Free(ulong address);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Changes protection on committed pages. <paramref name="rawOldProtection"/> is the
|
||||||
|
/// untranslated previous OS protection value (see <see cref="HostRegionInfo.RawProtection"/>).
|
||||||
|
/// </summary>
|
||||||
|
bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Restores a raw protection value previously returned by <see cref="Protect"/> or
|
||||||
|
/// <see cref="Query"/> on this same platform. Raw values are opaque to callers and
|
||||||
|
/// must never cross platforms; this exists so save/restore protection sequences
|
||||||
|
/// round-trip OS-specific modifier bits the neutral enum cannot represent.
|
||||||
|
/// </summary>
|
||||||
|
bool ProtectRaw(ulong address, ulong size, uint rawProtection, out uint rawOldProtection);
|
||||||
|
|
||||||
|
bool Query(ulong address, out HostRegionInfo info);
|
||||||
|
|
||||||
|
void FlushInstructionCache(ulong address, ulong size);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Aggregates the host-OS primitives the native execution engine depends on.
|
||||||
|
/// Each supported platform provides one implementation; consumers reach the
|
||||||
|
/// process-wide instance through <see cref="HostPlatform.Current"/> or accept
|
||||||
|
/// one by injection.
|
||||||
|
/// </summary>
|
||||||
|
public interface IHostPlatform
|
||||||
|
{
|
||||||
|
IHostMemory Memory { get; }
|
||||||
|
|
||||||
|
IHostThreading Threading { get; }
|
||||||
|
|
||||||
|
IHostSymbolResolver Symbols { get; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host;
|
||||||
|
|
||||||
|
public interface IHostSymbolResolver
|
||||||
|
{
|
||||||
|
/// <summary>Returns the native address of the function, or 0 if unavailable.</summary>
|
||||||
|
nint GetAddress(HostRuntimeFunction function);
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raw host thread and native-TLS primitives for the execution engine. Guest
|
||||||
|
/// code must run on threads the CLR did not create (no managed frames below
|
||||||
|
/// guest frames), so thread creation takes a native entry point and is not
|
||||||
|
/// expressible with managed threads.
|
||||||
|
/// </summary>
|
||||||
|
public interface IHostThreading
|
||||||
|
{
|
||||||
|
/// <summary>Allocates a native TLS slot; returns <see cref="uint.MaxValue"/> on failure.</summary>
|
||||||
|
uint AllocateTlsSlot();
|
||||||
|
|
||||||
|
bool FreeTlsSlot(uint slot);
|
||||||
|
|
||||||
|
bool SetTlsValue(uint slot, nint value);
|
||||||
|
|
||||||
|
nint GetTlsValue(uint slot);
|
||||||
|
|
||||||
|
uint CurrentThreadId { get; }
|
||||||
|
|
||||||
|
bool TrySetCurrentThreadAffinity(nuint affinityMask);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a raw OS thread executing native code at <paramref name="entry"/> with
|
||||||
|
/// <paramref name="stackReserveBytes"/> of reserved (not committed) stack.
|
||||||
|
/// Returns the thread handle, or 0 on failure.
|
||||||
|
/// </summary>
|
||||||
|
nint CreateNativeThread(nint entry, nint parameter, nuint stackReserveBytes, out uint threadId);
|
||||||
|
|
||||||
|
/// <summary>Waits for the thread to exit; true when it did within the timeout.</summary>
|
||||||
|
bool WaitForThreadExit(nint threadHandle, uint timeoutMilliseconds);
|
||||||
|
|
||||||
|
void CloseThreadHandle(nint threadHandle);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Suspends the thread, snapshots its general-purpose registers, and resumes it —
|
||||||
|
/// one indivisible operation (diagnostics only). The caller must not pass the
|
||||||
|
/// current thread. Returns false if the thread cannot be opened or suspended.
|
||||||
|
/// </summary>
|
||||||
|
bool TryCaptureThreadRegisters(uint threadId, out HostCapturedRegisters registers);
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host.Windows;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Windows implementation over VirtualAlloc/VirtualFree/VirtualProtect/VirtualQuery.
|
||||||
|
/// Sealed so the JIT can devirtualize interface calls on fault-handling hot paths.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed unsafe partial class WindowsHostMemory : IHostMemory
|
||||||
|
{
|
||||||
|
private const uint MEM_COMMIT = 0x1000;
|
||||||
|
private const uint MEM_RESERVE = 0x2000;
|
||||||
|
private const uint MEM_RELEASE = 0x8000;
|
||||||
|
private const uint MEM_FREE = 0x10000;
|
||||||
|
|
||||||
|
private const uint PAGE_NOACCESS = 0x01;
|
||||||
|
private const uint PAGE_READONLY = 0x02;
|
||||||
|
private const uint PAGE_READWRITE = 0x04;
|
||||||
|
private const uint PAGE_WRITECOPY = 0x08;
|
||||||
|
private const uint PAGE_EXECUTE = 0x10;
|
||||||
|
private const uint PAGE_EXECUTE_READ = 0x20;
|
||||||
|
private const uint PAGE_EXECUTE_READWRITE = 0x40;
|
||||||
|
private const uint PAGE_EXECUTE_WRITECOPY = 0x80;
|
||||||
|
|
||||||
|
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection)
|
||||||
|
{
|
||||||
|
return (ulong)VirtualAlloc((void*)desiredAddress, (nuint)size, MEM_COMMIT | MEM_RESERVE, ToNativeProtection(protection));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection)
|
||||||
|
{
|
||||||
|
return (ulong)VirtualAlloc((void*)desiredAddress, (nuint)size, MEM_RESERVE, ToNativeProtection(protection));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Commit(ulong address, ulong size, HostPageProtection protection)
|
||||||
|
{
|
||||||
|
return VirtualAlloc((void*)address, (nuint)size, MEM_COMMIT, ToNativeProtection(protection)) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Free(ulong address)
|
||||||
|
{
|
||||||
|
return VirtualFree((void*)address, 0, MEM_RELEASE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection)
|
||||||
|
{
|
||||||
|
return VirtualProtect((void*)address, (nuint)size, ToNativeProtection(protection), out rawOldProtection);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ProtectRaw(ulong address, ulong size, uint rawProtection, out uint rawOldProtection)
|
||||||
|
{
|
||||||
|
return VirtualProtect((void*)address, (nuint)size, rawProtection, out rawOldProtection);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Query(ulong address, out HostRegionInfo info)
|
||||||
|
{
|
||||||
|
if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MemoryBasicInformation64)) == 0)
|
||||||
|
{
|
||||||
|
info = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
info = new HostRegionInfo(
|
||||||
|
mbi.BaseAddress,
|
||||||
|
mbi.AllocationBase,
|
||||||
|
mbi.RegionSize,
|
||||||
|
ToRegionState(mbi.State),
|
||||||
|
mbi.State,
|
||||||
|
ToHostProtection(mbi.Protect),
|
||||||
|
mbi.Protect,
|
||||||
|
mbi.AllocationProtect);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void FlushInstructionCache(ulong address, ulong size)
|
||||||
|
{
|
||||||
|
FlushInstructionCache(GetCurrentProcess(), (void*)address, (nuint)size);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint ToNativeProtection(HostPageProtection protection) => protection switch
|
||||||
|
{
|
||||||
|
HostPageProtection.NoAccess => PAGE_NOACCESS,
|
||||||
|
HostPageProtection.ReadOnly => PAGE_READONLY,
|
||||||
|
HostPageProtection.ReadWrite => PAGE_READWRITE,
|
||||||
|
HostPageProtection.Execute => PAGE_EXECUTE,
|
||||||
|
HostPageProtection.ReadExecute => PAGE_EXECUTE_READ,
|
||||||
|
HostPageProtection.ReadWriteExecute => PAGE_EXECUTE_READWRITE,
|
||||||
|
HostPageProtection.ExecuteWriteCopy => PAGE_EXECUTE_WRITECOPY,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(protection), protection, null),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static HostRegionState ToRegionState(uint state) => state switch
|
||||||
|
{
|
||||||
|
MEM_COMMIT => HostRegionState.Committed,
|
||||||
|
MEM_RESERVE => HostRegionState.Reserved,
|
||||||
|
MEM_FREE => HostRegionState.Free,
|
||||||
|
_ => HostRegionState.Free,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static HostPageProtection ToHostProtection(uint rawProtection)
|
||||||
|
{
|
||||||
|
// Strip PAGE_GUARD/PAGE_NOCACHE/PAGE_WRITECOMBINE modifiers; callers needing
|
||||||
|
// them compare HostRegionInfo.RawProtection directly.
|
||||||
|
return (rawProtection & 0xFF) switch
|
||||||
|
{
|
||||||
|
PAGE_READONLY => HostPageProtection.ReadOnly,
|
||||||
|
PAGE_READWRITE => HostPageProtection.ReadWrite,
|
||||||
|
PAGE_WRITECOPY => HostPageProtection.ReadWrite,
|
||||||
|
PAGE_EXECUTE => HostPageProtection.Execute,
|
||||||
|
PAGE_EXECUTE_READ => HostPageProtection.ReadExecute,
|
||||||
|
PAGE_EXECUTE_READWRITE => HostPageProtection.ReadWriteExecute,
|
||||||
|
PAGE_EXECUTE_WRITECOPY => HostPageProtection.ExecuteWriteCopy,
|
||||||
|
_ => HostPageProtection.NoAccess,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static partial void* VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static partial bool VirtualFree(void* lpAddress, nuint dwSize, uint dwFreeType);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static partial bool VirtualProtect(void* lpAddress, nuint dwSize, uint flNewProtect, out uint lpflOldProtect);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll")]
|
||||||
|
private static partial nuint VirtualQuery(void* lpAddress, out MemoryBasicInformation64 lpBuffer, nuint dwLength);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll")]
|
||||||
|
private static partial void* GetCurrentProcess();
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static partial bool FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
|
||||||
|
|
||||||
|
private struct MemoryBasicInformation64
|
||||||
|
{
|
||||||
|
public ulong BaseAddress;
|
||||||
|
public ulong AllocationBase;
|
||||||
|
public uint AllocationProtect;
|
||||||
|
public uint Alignment1;
|
||||||
|
public ulong RegionSize;
|
||||||
|
public uint State;
|
||||||
|
public uint Protect;
|
||||||
|
public uint Type;
|
||||||
|
public uint Alignment2;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host.Windows;
|
||||||
|
|
||||||
|
internal sealed class WindowsHostPlatform : IHostPlatform
|
||||||
|
{
|
||||||
|
public IHostMemory Memory { get; } = new WindowsHostMemory();
|
||||||
|
|
||||||
|
public IHostThreading Threading { get; } = new WindowsHostThreading();
|
||||||
|
|
||||||
|
public IHostSymbolResolver Symbols { get; } = new WindowsHostSymbolResolver();
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host.Windows;
|
||||||
|
|
||||||
|
internal sealed partial class WindowsHostSymbolResolver : IHostSymbolResolver
|
||||||
|
{
|
||||||
|
public nint GetAddress(HostRuntimeFunction function)
|
||||||
|
{
|
||||||
|
var kernel32 = GetModuleHandle("kernel32.dll");
|
||||||
|
if (kernel32 == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetProcAddress(kernel32, function switch
|
||||||
|
{
|
||||||
|
HostRuntimeFunction.TlsGetValue => "TlsGetValue",
|
||||||
|
HostRuntimeFunction.QueryPerformanceCounter => "QueryPerformanceCounter",
|
||||||
|
HostRuntimeFunction.SwitchToThread => "SwitchToThread",
|
||||||
|
HostRuntimeFunction.Sleep => "Sleep",
|
||||||
|
HostRuntimeFunction.WaitForSingleObject => "WaitForSingleObject",
|
||||||
|
HostRuntimeFunction.SetEvent => "SetEvent",
|
||||||
|
HostRuntimeFunction.ExitThread => "ExitThread",
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(function), function, null),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utf16 marshalling pins the managed string and passes its address directly
|
||||||
|
// (no copy); Utf8 stack-allocates the transient buffer for these short
|
||||||
|
// ASCII export names. LibraryImport is exact-spelling, hence the W entry point.
|
||||||
|
[LibraryImport("kernel32.dll", EntryPoint = "GetModuleHandleW", StringMarshalling = StringMarshalling.Utf16)]
|
||||||
|
private static partial nint GetModuleHandle(string lpModuleName);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll", StringMarshalling = StringMarshalling.Utf8)]
|
||||||
|
private static partial nint GetProcAddress(nint hModule, string procName);
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host.Windows;
|
||||||
|
|
||||||
|
internal sealed unsafe partial class WindowsHostThreading : IHostThreading
|
||||||
|
{
|
||||||
|
private const uint StackSizeParamIsAReservation = 0x00010000u;
|
||||||
|
private const uint ThreadGetContext = 0x0008u;
|
||||||
|
private const uint ThreadSuspendResume = 0x0002u;
|
||||||
|
|
||||||
|
// Win64 CONTEXT layout (CONTROL | INTEGER only — no XMM state is requested).
|
||||||
|
private const int Win64ContextSize = 0x4D0;
|
||||||
|
private const int Win64ContextFlagsOffset = 0x30;
|
||||||
|
private const uint ContextAmd64ControlInteger = 0x00100003u;
|
||||||
|
private const int CtxRax = 120;
|
||||||
|
private const int CtxRcx = 128;
|
||||||
|
private const int CtxRdx = 136;
|
||||||
|
private const int CtxRbx = 144;
|
||||||
|
private const int CtxRsp = 152;
|
||||||
|
private const int CtxRbp = 160;
|
||||||
|
private const int CtxRip = 248;
|
||||||
|
|
||||||
|
public uint AllocateTlsSlot() => TlsAlloc();
|
||||||
|
|
||||||
|
public bool FreeTlsSlot(uint slot) => TlsFree(slot);
|
||||||
|
|
||||||
|
public bool SetTlsValue(uint slot, nint value) => TlsSetValue(slot, value);
|
||||||
|
|
||||||
|
public nint GetTlsValue(uint slot) => TlsGetValue(slot);
|
||||||
|
|
||||||
|
public uint CurrentThreadId => GetCurrentThreadId();
|
||||||
|
|
||||||
|
public bool TrySetCurrentThreadAffinity(nuint affinityMask)
|
||||||
|
{
|
||||||
|
return SetThreadAffinityMask(GetCurrentThread(), affinityMask) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public nint CreateNativeThread(nint entry, nint parameter, nuint stackReserveBytes, out uint threadId)
|
||||||
|
{
|
||||||
|
return CreateThread(0, stackReserveBytes, entry, parameter, StackSizeParamIsAReservation, out threadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool WaitForThreadExit(nint threadHandle, uint timeoutMilliseconds)
|
||||||
|
{
|
||||||
|
return WaitForSingleObject(threadHandle, timeoutMilliseconds) == 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CloseThreadHandle(nint threadHandle)
|
||||||
|
{
|
||||||
|
_ = CloseHandle(threadHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryCaptureThreadRegisters(uint threadId, out HostCapturedRegisters registers)
|
||||||
|
{
|
||||||
|
registers = default;
|
||||||
|
var threadHandle = OpenThread(ThreadGetContext | ThreadSuspendResume, false, threadId);
|
||||||
|
if (threadHandle == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void* contextRecord = null;
|
||||||
|
var suspended = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (SuspendThread(threadHandle) == uint.MaxValue)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
suspended = true;
|
||||||
|
// CONTEXT requires 16-byte alignment (it embeds M128A fields);
|
||||||
|
// NativeMemory.AllocZeroed guarantees max_align_t, stackalloc only
|
||||||
|
// pointer-size — so this stays a native allocation.
|
||||||
|
contextRecord = NativeMemory.AllocZeroed((nuint)Win64ContextSize);
|
||||||
|
*(uint*)((byte*)contextRecord + Win64ContextFlagsOffset) = ContextAmd64ControlInteger;
|
||||||
|
if (!GetThreadContext(threadHandle, contextRecord))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
registers = new HostCapturedRegisters(
|
||||||
|
ReadU64(contextRecord, CtxRip),
|
||||||
|
ReadU64(contextRecord, CtxRsp),
|
||||||
|
ReadU64(contextRecord, CtxRbp),
|
||||||
|
ReadU64(contextRecord, CtxRax),
|
||||||
|
ReadU64(contextRecord, CtxRbx),
|
||||||
|
ReadU64(contextRecord, CtxRcx),
|
||||||
|
ReadU64(contextRecord, CtxRdx));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (contextRecord != null)
|
||||||
|
{
|
||||||
|
NativeMemory.Free(contextRecord);
|
||||||
|
}
|
||||||
|
if (suspended)
|
||||||
|
{
|
||||||
|
_ = ResumeThread(threadHandle);
|
||||||
|
}
|
||||||
|
_ = CloseHandle(threadHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong ReadU64(void* contextRecord, int offset)
|
||||||
|
{
|
||||||
|
return *(ulong*)((byte*)contextRecord + offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll")]
|
||||||
|
private static partial uint TlsAlloc();
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static partial bool TlsFree(uint dwTlsIndex);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static partial bool TlsSetValue(uint dwTlsIndex, nint lpTlsValue);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll")]
|
||||||
|
private static partial nint TlsGetValue(uint dwTlsIndex);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll")]
|
||||||
|
private static partial uint GetCurrentThreadId();
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll")]
|
||||||
|
private static partial nint GetCurrentThread();
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static partial nuint SetThreadAffinityMask(nint hThread, nuint dwThreadAffinityMask);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static partial nint CreateThread(
|
||||||
|
nint lpThreadAttributes,
|
||||||
|
nuint dwStackSize,
|
||||||
|
nint lpStartAddress,
|
||||||
|
nint lpParameter,
|
||||||
|
uint dwCreationFlags,
|
||||||
|
out uint lpThreadId);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static partial uint WaitForSingleObject(nint hHandle, uint dwMilliseconds);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static partial nint OpenThread(uint dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, uint dwThreadId);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static partial uint SuspendThread(nint hThread);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static partial uint ResumeThread(nint hThread);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static partial bool GetThreadContext(nint hThread, void* lpContext);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static partial bool CloseHandle(nint hObject);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implemented by memories that decorate another <see cref="ICpuMemory"/>
|
||||||
|
/// (e.g. access trackers) so capability lookups can unwrap to the real
|
||||||
|
/// implementation without reflection.
|
||||||
|
/// </summary>
|
||||||
|
public interface ICpuMemoryWrapper
|
||||||
|
{
|
||||||
|
ICpuMemory Inner { get; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Guest address-space manipulation beyond plain allocation: fixed-address
|
||||||
|
/// mapping and page-protection changes. Guest addresses are identity-mapped
|
||||||
|
/// onto host pages by the implementing memory, so HLE exports (mmap, mprotect)
|
||||||
|
/// reach these operations through <c>ctx.Memory</c> instead of calling host
|
||||||
|
/// APIs directly. Member signatures deliberately mirror the implementation in
|
||||||
|
/// SharpEmu.Core so existing call sites migrate call-for-call.
|
||||||
|
/// </summary>
|
||||||
|
public interface IGuestAddressSpace : IGuestMemoryAllocator
|
||||||
|
{
|
||||||
|
ulong AllocateAt(ulong desiredAddress, ulong size, bool executable = true, bool allowAlternative = true);
|
||||||
|
|
||||||
|
bool TryAllocateAtOrAbove(ulong desiredAddress, ulong size, bool executable, ulong alignment, out ulong actualAddress);
|
||||||
|
|
||||||
|
bool TryProtect(ulong address, ulong size, GuestPageProtection protection);
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ public static class AgcExports
|
|||||||
private const uint ItDispatchDirect = 0x15;
|
private const uint ItDispatchDirect = 0x15;
|
||||||
private const uint ItDispatchIndirect = 0x16;
|
private const uint ItDispatchIndirect = 0x16;
|
||||||
private const uint ItWaitRegMem = 0x3C;
|
private const uint ItWaitRegMem = 0x3C;
|
||||||
|
private const uint ItIndirectBuffer = 0x3F;
|
||||||
private const uint ItEventWrite = 0x46;
|
private const uint ItEventWrite = 0x46;
|
||||||
private const uint ItDmaData = 0x50;
|
private const uint ItDmaData = 0x50;
|
||||||
private const uint ItSetContextReg = 0x69;
|
private const uint ItSetContextReg = 0x69;
|
||||||
@@ -157,7 +158,7 @@ public static class AgcExports
|
|||||||
private static readonly HashSet<uint> _tracedSubmittedDrawOpcodes = new();
|
private static readonly HashSet<uint> _tracedSubmittedDrawOpcodes = new();
|
||||||
private static readonly Dictionary<(ulong Ps, ulong State, Gen5PixelOutputKind Output), byte[]> _pixelSpirvCache = new();
|
private static readonly Dictionary<(ulong Ps, ulong State, Gen5PixelOutputKind Output), byte[]> _pixelSpirvCache = new();
|
||||||
private static readonly Dictionary<
|
private static readonly Dictionary<
|
||||||
(ulong Es, ulong EsState, ulong Ps, ulong PsState, Gen5PixelOutputKind Output, uint Attributes),
|
(ulong Es, ulong EsState, ulong Ps, ulong PsState, string OutputLayout, uint Attributes),
|
||||||
(byte[] Vertex, byte[] Pixel)> _graphicsSpirvCache = new();
|
(byte[] Vertex, byte[] Pixel)> _graphicsSpirvCache = new();
|
||||||
private static readonly Dictionary<
|
private static readonly Dictionary<
|
||||||
(ulong Cs, ulong State, uint LocalX, uint LocalY, uint LocalZ),
|
(ulong Cs, ulong State, uint LocalX, uint LocalY, uint LocalZ),
|
||||||
@@ -1943,6 +1944,96 @@ public static class AgcExports
|
|||||||
return ReturnPointer(ctx, commandAddress);
|
return ReturnPointer(ctx, commandAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Guest draw-command builders: emit valid (skippable) packets and return a live
|
||||||
|
// command pointer so the guest's command-buffer build succeeds; full draw processing is TODO.
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "8N2tmT3jmC8",
|
||||||
|
ExportName = "sceAgcDcbSetIndexCount",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int DcbSetIndexCount(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var dcb = ctx[CpuRegister.Rdi];
|
||||||
|
var indexCount = (uint)ctx[CpuRegister.Rsi];
|
||||||
|
if (dcb == 0)
|
||||||
|
{
|
||||||
|
return ReturnPointer(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryAllocateCommandDwords(ctx, dcb, 2, out var cmd) ||
|
||||||
|
!ctx.TryWriteUInt32(cmd, Pm4(2, ItNop, RZero)) ||
|
||||||
|
!ctx.TryWriteUInt32(cmd + 4, indexCount))
|
||||||
|
{
|
||||||
|
return ReturnPointer(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReturnPointer(ctx, cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "xSAR0LTcRKM",
|
||||||
|
ExportName = "sceAgcDcbJump",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int DcbJump(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var dcb = ctx[CpuRegister.Rdi];
|
||||||
|
var target = ctx[CpuRegister.Rsi];
|
||||||
|
var sizeDwords = (uint)ctx[CpuRegister.Rdx];
|
||||||
|
if (dcb == 0)
|
||||||
|
{
|
||||||
|
return ReturnPointer(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryAllocateCommandDwords(ctx, dcb, 4, out var cmd) ||
|
||||||
|
!ctx.TryWriteUInt32(cmd, Pm4(4, ItIndirectBuffer, RZero)) ||
|
||||||
|
!ctx.TryWriteUInt32(cmd + 4, (uint)(target & 0xFFFF_FFFFUL)) ||
|
||||||
|
!ctx.TryWriteUInt32(cmd + 8, (uint)((target >> 32) & 0xFFFFUL)) ||
|
||||||
|
!ctx.TryWriteUInt32(cmd + 12, sizeDwords & 0xFFFFF))
|
||||||
|
{
|
||||||
|
return ReturnPointer(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReturnPointer(ctx, cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "bbFueFP+J4k",
|
||||||
|
ExportName = "sceAgcDcbSetPredication",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int DcbSetPredication(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var dcb = ctx[CpuRegister.Rdi];
|
||||||
|
var address = ctx[CpuRegister.Rsi];
|
||||||
|
if (dcb == 0)
|
||||||
|
{
|
||||||
|
return ReturnPointer(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryAllocateCommandDwords(ctx, dcb, 3, out var cmd) ||
|
||||||
|
!ctx.TryWriteUInt32(cmd, Pm4(3, ItNop, RZero)) ||
|
||||||
|
!ctx.TryWriteUInt32(cmd + 4, (uint)(address & 0xFFFF_FFFFUL)) ||
|
||||||
|
!ctx.TryWriteUInt32(cmd + 8, (uint)(address >> 32)))
|
||||||
|
{
|
||||||
|
return ReturnPointer(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReturnPointer(ctx, cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "w6Dj1VJt5qY",
|
||||||
|
ExportName = "sceAgcSetPacketPredication",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int SetPacketPredication(CpuContext ctx)
|
||||||
|
{
|
||||||
|
// Global predication toggle on a packet; a no-op is safe for rendering.
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "w2rJhmD+dsE",
|
Nid = "w2rJhmD+dsE",
|
||||||
ExportName = "sceAgcDriverAddEqEvent",
|
ExportName = "sceAgcDriverAddEqEvent",
|
||||||
@@ -3329,12 +3420,13 @@ public static class AgcExports
|
|||||||
textures,
|
textures,
|
||||||
globalMemoryBuffers,
|
globalMemoryBuffers,
|
||||||
translatedDraw.AttributeCount,
|
translatedDraw.AttributeCount,
|
||||||
new VulkanGuestRenderTarget(
|
translatedDraw.RenderTargets.Select(target =>
|
||||||
firstTarget.Address,
|
new VulkanGuestRenderTarget(
|
||||||
firstTarget.Width,
|
target.Address,
|
||||||
firstTarget.Height,
|
target.Width,
|
||||||
firstTarget.Format,
|
target.Height,
|
||||||
firstTarget.NumberType),
|
target.Format,
|
||||||
|
target.NumberType)).ToArray(),
|
||||||
translatedDraw.VertexSpirv,
|
translatedDraw.VertexSpirv,
|
||||||
translatedDraw.VertexCount,
|
translatedDraw.VertexCount,
|
||||||
translatedDraw.InstanceCount,
|
translatedDraw.InstanceCount,
|
||||||
@@ -3462,11 +3554,34 @@ public static class AgcExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var renderTargets = GetRenderTargets(state.CxRegisters)
|
var renderTargets = GetRenderTargets(state.CxRegisters)
|
||||||
.Where(target =>
|
.Where(target => HasPixelColorExport(pixelState, target.Slot))
|
||||||
target.Slot == 0 &&
|
.OrderBy(target => target.Slot)
|
||||||
HasPixelColorExport(pixelState, target.Slot))
|
|
||||||
.ToArray();
|
.ToArray();
|
||||||
var outputKind = GetPixelOutputKind(renderTargets.FirstOrDefault().NumberType);
|
var renderTargetFormats = new VulkanRenderTargetFormat[renderTargets.Length];
|
||||||
|
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 attributeCount = GetInterpolatedAttributeCount(pixelState);
|
||||||
var exportStateFingerprint = ComputeShaderStructureFingerprint(exportEvaluation);
|
var exportStateFingerprint = ComputeShaderStructureFingerprint(exportEvaluation);
|
||||||
var pixelStateFingerprint = ComputeShaderStructureFingerprint(pixelEvaluation);
|
var pixelStateFingerprint = ComputeShaderStructureFingerprint(pixelEvaluation);
|
||||||
@@ -3475,7 +3590,7 @@ public static class AgcExports
|
|||||||
exportStateFingerprint,
|
exportStateFingerprint,
|
||||||
pixelShaderAddress,
|
pixelShaderAddress,
|
||||||
pixelStateFingerprint,
|
pixelStateFingerprint,
|
||||||
outputKind,
|
outputLayout,
|
||||||
attributeCount);
|
attributeCount);
|
||||||
var totalGlobalBuffers =
|
var totalGlobalBuffers =
|
||||||
pixelEvaluation.GlobalMemoryBindings.Count +
|
pixelEvaluation.GlobalMemoryBindings.Count +
|
||||||
@@ -3491,7 +3606,7 @@ public static class AgcExports
|
|||||||
if (!Gen5SpirvTranslator.TryCompilePixelShader(
|
if (!Gen5SpirvTranslator.TryCompilePixelShader(
|
||||||
pixelState,
|
pixelState,
|
||||||
pixelEvaluation,
|
pixelEvaluation,
|
||||||
outputKind,
|
pixelOutputs,
|
||||||
out var pixelShader,
|
out var pixelShader,
|
||||||
out error,
|
out error,
|
||||||
globalBufferBase: 0,
|
globalBufferBase: 0,
|
||||||
@@ -3579,7 +3694,7 @@ public static class AgcExports
|
|||||||
vertexInputs,
|
vertexInputs,
|
||||||
renderTargets,
|
renderTargets,
|
||||||
ApplyTransparentPremultipliedFillClear(
|
ApplyTransparentPremultipliedFillClear(
|
||||||
CreateRenderState(state.CxRegisters, renderTargets.FirstOrDefault()),
|
CreateRenderState(state.CxRegisters, renderTargets, pixelState),
|
||||||
textures,
|
textures,
|
||||||
vertexInputs,
|
vertexInputs,
|
||||||
pixelEvaluation.InitialScalarRegisters));
|
pixelEvaluation.InitialScalarRegisters));
|
||||||
@@ -3595,7 +3710,8 @@ public static class AgcExports
|
|||||||
/// Chowdren resets its effect layers with an untextured transparent-black
|
/// Chowdren resets its effect layers with an untextured transparent-black
|
||||||
/// fill using premultiplied blending. With One/OneMinusSrcAlpha that draw
|
/// fill using premultiplied blending. With One/OneMinusSrcAlpha that draw
|
||||||
/// is otherwise a no-op, causing fog and vignette layers to accumulate.
|
/// is otherwise a no-op, causing fog and vignette layers to accumulate.
|
||||||
/// Treat precisely that draw shape as an overwrite.
|
/// Treat precisely that draw shape as an overwrite only when every MRT
|
||||||
|
/// attachment uses the same premultiplied blend pattern.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static VulkanGuestRenderState ApplyTransparentPremultipliedFillClear(
|
private static VulkanGuestRenderState ApplyTransparentPremultipliedFillClear(
|
||||||
VulkanGuestRenderState renderState,
|
VulkanGuestRenderState renderState,
|
||||||
@@ -3607,13 +3723,7 @@ public static class AgcExports
|
|||||||
textures.Count != 0 ||
|
textures.Count != 0 ||
|
||||||
vertexInputs.Count != 0 ||
|
vertexInputs.Count != 0 ||
|
||||||
pixelUserData.Count < 4 ||
|
pixelUserData.Count < 4 ||
|
||||||
renderState.Blend is not
|
!renderState.Blends.All(IsTransparentPremultipliedFillBlend))
|
||||||
{
|
|
||||||
Enable: true,
|
|
||||||
ColorSrcFactor: 1,
|
|
||||||
ColorDstFactor: 5,
|
|
||||||
ColorFunc: 0,
|
|
||||||
})
|
|
||||||
{
|
{
|
||||||
return renderState;
|
return renderState;
|
||||||
}
|
}
|
||||||
@@ -3628,10 +3738,21 @@ public static class AgcExports
|
|||||||
|
|
||||||
return renderState with
|
return renderState with
|
||||||
{
|
{
|
||||||
Blend = renderState.Blend with { Enable = false },
|
Blends = renderState.Blends
|
||||||
|
.Select(blend => blend with { Enable = false })
|
||||||
|
.ToArray(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsTransparentPremultipliedFillBlend(VulkanGuestBlendState blend) =>
|
||||||
|
blend is
|
||||||
|
{
|
||||||
|
Enable: true,
|
||||||
|
ColorSrcFactor: 1,
|
||||||
|
ColorDstFactor: 5,
|
||||||
|
ColorFunc: 0,
|
||||||
|
};
|
||||||
|
|
||||||
private static VulkanGuestIndexBuffer? CreateVulkanIndexBuffer(
|
private static VulkanGuestIndexBuffer? CreateVulkanIndexBuffer(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
SubmittedDcbState state,
|
SubmittedDcbState state,
|
||||||
@@ -3654,19 +3775,15 @@ public static class AgcExports
|
|||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Gen5PixelOutputKind GetPixelOutputKind(uint numberType) =>
|
|
||||||
numberType switch
|
|
||||||
{
|
|
||||||
4 => Gen5PixelOutputKind.Uint,
|
|
||||||
5 => Gen5PixelOutputKind.Sint,
|
|
||||||
_ => Gen5PixelOutputKind.Float,
|
|
||||||
};
|
|
||||||
|
|
||||||
private static bool HasPixelColorExport(Gen5ShaderState state, uint target) =>
|
private static bool HasPixelColorExport(Gen5ShaderState state, uint target) =>
|
||||||
state.Program.Instructions.Any(instruction =>
|
GetPixelColorExportMask(state, target) != 0;
|
||||||
instruction.Control is Gen5ExportControl export &&
|
|
||||||
export.Target == target &&
|
private static uint GetPixelColorExportMask(Gen5ShaderState state, uint target) =>
|
||||||
export.EnableMask != 0);
|
state.Program.Instructions
|
||||||
|
.Select(instruction => instruction.Control)
|
||||||
|
.OfType<Gen5ExportControl>()
|
||||||
|
.Where(export => export.Target == target)
|
||||||
|
.Aggregate(0u, (mask, export) => mask | (export.EnableMask & 0xFu));
|
||||||
|
|
||||||
private static uint GetInterpolatedAttributeCount(Gen5ShaderState state)
|
private static uint GetInterpolatedAttributeCount(Gen5ShaderState state)
|
||||||
{
|
{
|
||||||
@@ -3824,11 +3941,25 @@ public static class AgcExports
|
|||||||
|
|
||||||
private static VulkanGuestRenderState CreateRenderState(
|
private static VulkanGuestRenderState CreateRenderState(
|
||||||
IReadOnlyDictionary<uint, uint> registers,
|
IReadOnlyDictionary<uint, uint> registers,
|
||||||
RenderTargetDescriptor target)
|
IReadOnlyList<RenderTargetDescriptor> targets,
|
||||||
|
Gen5ShaderState pixelState)
|
||||||
{
|
{
|
||||||
|
if (targets.Count == 0)
|
||||||
|
{
|
||||||
|
return VulkanGuestRenderState.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
var target = targets[0];
|
||||||
var scissor = DecodeScissor(registers, target.Width, target.Height);
|
var scissor = DecodeScissor(registers, target.Width, target.Height);
|
||||||
return new VulkanGuestRenderState(
|
return new VulkanGuestRenderState(
|
||||||
DecodeBlendState(registers, target.Slot),
|
targets.Select(target =>
|
||||||
|
{
|
||||||
|
var blend = DecodeBlendState(registers, target.Slot);
|
||||||
|
return blend with
|
||||||
|
{
|
||||||
|
WriteMask = blend.WriteMask & GetPixelColorExportMask(pixelState, target.Slot),
|
||||||
|
};
|
||||||
|
}).ToArray(),
|
||||||
scissor,
|
scissor,
|
||||||
DecodeViewport(registers, target.Width, target.Height, scissor));
|
DecodeViewport(registers, target.Width, target.Height, scissor));
|
||||||
}
|
}
|
||||||
@@ -3853,7 +3984,7 @@ public static class AgcExports
|
|||||||
(control >> 24) & 0x1Fu,
|
(control >> 24) & 0x1Fu,
|
||||||
(control >> 21) & 0x7u,
|
(control >> 21) & 0x7u,
|
||||||
((control >> 29) & 1u) != 0,
|
((control >> 29) & 1u) != 0,
|
||||||
writeMask == 0 ? 0xFu : writeMask);
|
writeMask);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static VulkanGuestRect? DecodeScissor(
|
private static VulkanGuestRect? DecodeScissor(
|
||||||
@@ -5191,7 +5322,7 @@ public static class AgcExports
|
|||||||
if (Gen5SpirvTranslator.TryCompilePixelShader(
|
if (Gen5SpirvTranslator.TryCompilePixelShader(
|
||||||
pixelState,
|
pixelState,
|
||||||
evaluation,
|
evaluation,
|
||||||
Gen5PixelOutputKind.Float,
|
[new(0, 0, Gen5PixelOutputKind.Float)],
|
||||||
out var compiledPixel,
|
out var compiledPixel,
|
||||||
out var compileError))
|
out var compileError))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ internal enum Gen5PixelOutputKind
|
|||||||
Sint,
|
Sint,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal readonly record struct Gen5PixelOutputBinding(
|
||||||
|
uint GuestSlot,
|
||||||
|
uint HostLocation,
|
||||||
|
Gen5PixelOutputKind Kind);
|
||||||
|
|
||||||
internal enum Gen5SpirvStage
|
internal enum Gen5SpirvStage
|
||||||
{
|
{
|
||||||
Vertex,
|
Vertex,
|
||||||
|
|||||||
@@ -19,13 +19,59 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
int globalBufferBase = 0,
|
int globalBufferBase = 0,
|
||||||
int totalGlobalBufferCount = -1,
|
int totalGlobalBufferCount = -1,
|
||||||
int imageBindingBase = 0,
|
int imageBindingBase = 0,
|
||||||
|
int scalarRegisterBufferIndex = -1) =>
|
||||||
|
TryCompilePixelShader(
|
||||||
|
state,
|
||||||
|
evaluation,
|
||||||
|
[new Gen5PixelOutputBinding(0, 0, outputKind)],
|
||||||
|
out shader,
|
||||||
|
out error,
|
||||||
|
globalBufferBase,
|
||||||
|
totalGlobalBufferCount,
|
||||||
|
imageBindingBase,
|
||||||
|
scalarRegisterBufferIndex);
|
||||||
|
|
||||||
|
public static bool TryCompilePixelShader(
|
||||||
|
Gen5ShaderState state,
|
||||||
|
Gen5ShaderEvaluation evaluation,
|
||||||
|
IReadOnlyList<Gen5PixelOutputBinding> outputs,
|
||||||
|
out Gen5SpirvShader shader,
|
||||||
|
out string error,
|
||||||
|
int globalBufferBase = 0,
|
||||||
|
int totalGlobalBufferCount = -1,
|
||||||
|
int imageBindingBase = 0,
|
||||||
int scalarRegisterBufferIndex = -1)
|
int scalarRegisterBufferIndex = -1)
|
||||||
{
|
{
|
||||||
|
if (outputs.Count > 8 || outputs.Any(output => output.GuestSlot > 7))
|
||||||
|
{
|
||||||
|
shader = default!;
|
||||||
|
error = "pixel outputs must contain at most eight guest slots in the 0..7 range";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outputs.Select(output => output.GuestSlot).Distinct().Count() != outputs.Count ||
|
||||||
|
outputs.Select(output => output.HostLocation).Distinct().Count() != outputs.Count)
|
||||||
|
{
|
||||||
|
shader = default!;
|
||||||
|
error = "pixel output guest slots and host locations must be unique";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!outputs
|
||||||
|
.OrderBy(output => output.HostLocation)
|
||||||
|
.Select((output, index) => output.HostLocation == (uint)index)
|
||||||
|
.All(isDense => isDense))
|
||||||
|
{
|
||||||
|
shader = default!;
|
||||||
|
error = "pixel output host locations must be dense in the 0..N-1 range";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var context = new CompilationContext(
|
var context = new CompilationContext(
|
||||||
Gen5SpirvStage.Pixel,
|
Gen5SpirvStage.Pixel,
|
||||||
state,
|
state,
|
||||||
evaluation,
|
evaluation,
|
||||||
outputKind,
|
outputs,
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -50,7 +96,7 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
Gen5SpirvStage.Vertex,
|
Gen5SpirvStage.Vertex,
|
||||||
state,
|
state,
|
||||||
evaluation,
|
evaluation,
|
||||||
Gen5PixelOutputKind.Float,
|
[],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -74,7 +120,7 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
Gen5SpirvStage.Compute,
|
Gen5SpirvStage.Compute,
|
||||||
state,
|
state,
|
||||||
evaluation,
|
evaluation,
|
||||||
Gen5PixelOutputKind.Float,
|
[],
|
||||||
Math.Max(localSizeX, 1),
|
Math.Max(localSizeX, 1),
|
||||||
Math.Max(localSizeY, 1),
|
Math.Max(localSizeY, 1),
|
||||||
Math.Max(localSizeZ, 1),
|
Math.Max(localSizeZ, 1),
|
||||||
@@ -91,7 +137,7 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
private readonly Gen5SpirvStage _stage;
|
private readonly Gen5SpirvStage _stage;
|
||||||
private readonly Gen5ShaderState _state;
|
private readonly Gen5ShaderState _state;
|
||||||
private readonly Gen5ShaderEvaluation _evaluation;
|
private readonly Gen5ShaderEvaluation _evaluation;
|
||||||
private readonly Gen5PixelOutputKind _outputKind;
|
private readonly IReadOnlyList<Gen5PixelOutputBinding> _pixelOutputBindings;
|
||||||
private readonly uint _localSizeX;
|
private readonly uint _localSizeX;
|
||||||
private readonly uint _localSizeY;
|
private readonly uint _localSizeY;
|
||||||
private readonly uint _localSizeZ;
|
private readonly uint _localSizeZ;
|
||||||
@@ -101,6 +147,7 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
private readonly int _scalarRegisterBufferIndex;
|
private readonly int _scalarRegisterBufferIndex;
|
||||||
private readonly List<uint> _interfaces = [];
|
private readonly List<uint> _interfaces = [];
|
||||||
private readonly Dictionary<uint, uint> _pixelInputs = [];
|
private readonly Dictionary<uint, uint> _pixelInputs = [];
|
||||||
|
private readonly Dictionary<uint, SpirvPixelOutput> _pixelOutputs = [];
|
||||||
private readonly Dictionary<uint, uint> _vertexOutputs = [];
|
private readonly Dictionary<uint, uint> _vertexOutputs = [];
|
||||||
private readonly Dictionary<uint, SpirvVertexInput> _vertexInputsByPc = [];
|
private readonly Dictionary<uint, SpirvVertexInput> _vertexInputsByPc = [];
|
||||||
private readonly List<SpirvImageResource> _imageResources = [];
|
private readonly List<SpirvImageResource> _imageResources = [];
|
||||||
@@ -133,7 +180,6 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
private uint _lds;
|
private uint _lds;
|
||||||
private uint _workgroupUintPointer;
|
private uint _workgroupUintPointer;
|
||||||
private uint _positionOutput;
|
private uint _positionOutput;
|
||||||
private uint _pixelOutput;
|
|
||||||
private uint _vertexIndexInput;
|
private uint _vertexIndexInput;
|
||||||
private uint _instanceIndexInput;
|
private uint _instanceIndexInput;
|
||||||
private uint _fragCoordInput;
|
private uint _fragCoordInput;
|
||||||
@@ -163,11 +209,16 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
uint Type,
|
uint Type,
|
||||||
uint ComponentCount);
|
uint ComponentCount);
|
||||||
|
|
||||||
|
private readonly record struct SpirvPixelOutput(
|
||||||
|
uint Variable,
|
||||||
|
uint Type,
|
||||||
|
Gen5PixelOutputKind Kind);
|
||||||
|
|
||||||
public CompilationContext(
|
public CompilationContext(
|
||||||
Gen5SpirvStage stage,
|
Gen5SpirvStage stage,
|
||||||
Gen5ShaderState state,
|
Gen5ShaderState state,
|
||||||
Gen5ShaderEvaluation evaluation,
|
Gen5ShaderEvaluation evaluation,
|
||||||
Gen5PixelOutputKind outputKind,
|
IReadOnlyList<Gen5PixelOutputBinding> pixelOutputBindings,
|
||||||
uint localSizeX,
|
uint localSizeX,
|
||||||
uint localSizeY,
|
uint localSizeY,
|
||||||
uint localSizeZ,
|
uint localSizeZ,
|
||||||
@@ -179,7 +230,7 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
_stage = stage;
|
_stage = stage;
|
||||||
_state = state;
|
_state = state;
|
||||||
_evaluation = evaluation;
|
_evaluation = evaluation;
|
||||||
_outputKind = outputKind;
|
_pixelOutputBindings = pixelOutputBindings;
|
||||||
_localSizeX = localSizeX;
|
_localSizeX = localSizeX;
|
||||||
_localSizeY = localSizeY;
|
_localSizeY = localSizeY;
|
||||||
_localSizeZ = localSizeZ;
|
_localSizeZ = localSizeZ;
|
||||||
@@ -700,19 +751,24 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
(uint)SpirvBuiltIn.FragCoord);
|
(uint)SpirvBuiltIn.FragCoord);
|
||||||
_interfaces.Add(_fragCoordInput);
|
_interfaces.Add(_fragCoordInput);
|
||||||
|
|
||||||
var outputType = _outputKind switch
|
foreach (var binding in _pixelOutputBindings)
|
||||||
{
|
{
|
||||||
Gen5PixelOutputKind.Uint => _uvec4Type,
|
var outputType = GetPixelOutputType(binding.Kind);
|
||||||
Gen5PixelOutputKind.Sint => _module.TypeVector(_intType, 4),
|
var outputPointer =
|
||||||
_ => _vec4Type,
|
_module.TypePointer(SpirvStorageClass.Output, outputType);
|
||||||
};
|
var variable = _module.AddGlobalVariable(
|
||||||
var outputPointer =
|
outputPointer,
|
||||||
_module.TypePointer(SpirvStorageClass.Output, outputType);
|
SpirvStorageClass.Output);
|
||||||
_pixelOutput = _module.AddGlobalVariable(
|
_module.AddName(variable, $"mrt{binding.GuestSlot}");
|
||||||
outputPointer,
|
_module.AddDecoration(
|
||||||
SpirvStorageClass.Output);
|
variable,
|
||||||
_module.AddDecoration(_pixelOutput, SpirvDecoration.Location, 0);
|
SpirvDecoration.Location,
|
||||||
_interfaces.Add(_pixelOutput);
|
binding.HostLocation);
|
||||||
|
_pixelOutputs.Add(
|
||||||
|
binding.GuestSlot,
|
||||||
|
new SpirvPixelOutput(variable, outputType, binding.Kind));
|
||||||
|
_interfaces.Add(variable);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -831,7 +887,10 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
1);
|
1);
|
||||||
StoreV(2, Bitcast(_uintType, x), guardWithExec: false);
|
StoreV(2, Bitcast(_uintType, x), guardWithExec: false);
|
||||||
StoreV(3, Bitcast(_uintType, y), guardWithExec: false);
|
StoreV(3, Bitcast(_uintType, y), guardWithExec: false);
|
||||||
Store(_pixelOutput, _module.ConstantNull(GetPixelOutputType()));
|
foreach (var output in _pixelOutputs.Values)
|
||||||
|
{
|
||||||
|
Store(output.Variable, _module.ConstantNull(output.Type));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -2084,21 +2143,27 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
|
|
||||||
if (_stage == Gen5SpirvStage.Pixel)
|
if (_stage == Gen5SpirvStage.Pixel)
|
||||||
{
|
{
|
||||||
if (export.Target != 0)
|
if (!_pixelOutputs.TryGetValue(export.Target, out var output))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var outputType = GetPixelOutputType();
|
|
||||||
var values = new uint[4];
|
var values = new uint[4];
|
||||||
for (var component = 0; component < 4; component++)
|
for (var component = 0; component < 4; component++)
|
||||||
{
|
{
|
||||||
var enabled = (export.EnableMask & (1u << component)) != 0;
|
var enabled = (export.EnableMask & (1u << component)) != 0;
|
||||||
if (!enabled)
|
if (!enabled)
|
||||||
{
|
{
|
||||||
values[component] = _outputKind == Gen5PixelOutputKind.Sint
|
values[component] = _module.AddInstruction(
|
||||||
? Bitcast(_intType, UInt(0))
|
SpirvOp.CompositeExtract,
|
||||||
: UInt(0);
|
output.Kind switch
|
||||||
|
{
|
||||||
|
Gen5PixelOutputKind.Uint => _uintType,
|
||||||
|
Gen5PixelOutputKind.Sint => _intType,
|
||||||
|
_ => _floatType,
|
||||||
|
},
|
||||||
|
Load(output.Type, output.Variable),
|
||||||
|
(uint)component);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2107,7 +2172,7 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
var value = LoadCompressedExportComponent(
|
var value = LoadCompressedExportComponent(
|
||||||
instruction,
|
instruction,
|
||||||
component);
|
component);
|
||||||
values[component] = _outputKind switch
|
values[component] = output.Kind switch
|
||||||
{
|
{
|
||||||
Gen5PixelOutputKind.Uint => _module.AddInstruction(
|
Gen5PixelOutputKind.Uint => _module.AddInstruction(
|
||||||
SpirvOp.ConvertFToU,
|
SpirvOp.ConvertFToU,
|
||||||
@@ -2123,7 +2188,7 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
}
|
}
|
||||||
|
|
||||||
var raw = LoadV(instruction.Sources[component].Value);
|
var raw = LoadV(instruction.Sources[component].Value);
|
||||||
values[component] = _outputKind switch
|
values[component] = output.Kind switch
|
||||||
{
|
{
|
||||||
Gen5PixelOutputKind.Uint => raw,
|
Gen5PixelOutputKind.Uint => raw,
|
||||||
Gen5PixelOutputKind.Sint => Bitcast(_intType, raw),
|
Gen5PixelOutputKind.Sint => Bitcast(_intType, raw),
|
||||||
@@ -2133,15 +2198,15 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
|
|
||||||
var vector = _module.AddInstruction(
|
var vector = _module.AddInstruction(
|
||||||
SpirvOp.CompositeConstruct,
|
SpirvOp.CompositeConstruct,
|
||||||
outputType,
|
output.Type,
|
||||||
values);
|
values);
|
||||||
vector = _module.AddInstruction(
|
vector = _module.AddInstruction(
|
||||||
SpirvOp.Select,
|
SpirvOp.Select,
|
||||||
outputType,
|
output.Type,
|
||||||
Load(_boolType, _exec),
|
Load(_boolType, _exec),
|
||||||
vector,
|
vector,
|
||||||
Load(outputType, _pixelOutput));
|
Load(output.Type, output.Variable));
|
||||||
Store(_pixelOutput, vector);
|
Store(output.Variable, vector);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2209,8 +2274,8 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
(uint)(component & 1));
|
(uint)(component & 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
private uint GetPixelOutputType() =>
|
private uint GetPixelOutputType(Gen5PixelOutputKind kind) =>
|
||||||
_outputKind switch
|
kind switch
|
||||||
{
|
{
|
||||||
Gen5PixelOutputKind.Uint => _uvec4Type,
|
Gen5PixelOutputKind.Uint => _uvec4Type,
|
||||||
Gen5PixelOutputKind.Sint => _module.TypeVector(_intType, 4),
|
Gen5PixelOutputKind.Sint => _module.TypeVector(_intType, 4),
|
||||||
|
|||||||
@@ -348,6 +348,18 @@ public static class AmprExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Zi3dBUjgyXI",
|
||||||
|
ExportName = "sceAmprMeasureCommandSizeWriteKernelEventQueueOnCompletion",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAmpr")]
|
||||||
|
public static int MeasureCommandSizeWriteKernelEventQueueOnCompletion(CpuContext ctx)
|
||||||
|
{
|
||||||
|
TraceAmpr(ctx, "measure_write_equeue_complete", 0, KernelEventQueueRecordSize, 0);
|
||||||
|
ctx[CpuRegister.Rax] = KernelEventQueueRecordSize;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "C+IEj+BsAFM",
|
Nid = "C+IEj+BsAFM",
|
||||||
ExportName = "sceAmprMeasureCommandSizeWriteAddressOnCompletion",
|
ExportName = "sceAmprMeasureCommandSizeWriteAddressOnCompletion",
|
||||||
@@ -440,6 +452,40 @@ public static class AmprExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "o67gODLFpls",
|
||||||
|
ExportName = "sceAmprCommandBufferWriteKernelEventQueueOnCompletion",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAmpr")]
|
||||||
|
public static int CommandBufferWriteKernelEventQueueOnCompletion(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var commandBuffer = ctx[CpuRegister.Rdi];
|
||||||
|
var equeue = ctx[CpuRegister.Rsi];
|
||||||
|
var ident = ctx[CpuRegister.Rdx];
|
||||||
|
var completionToken = ctx[CpuRegister.Rcx];
|
||||||
|
var userData = ctx[CpuRegister.R8];
|
||||||
|
|
||||||
|
if (commandBuffer == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!AppendKernelEventQueueRecord(
|
||||||
|
ctx,
|
||||||
|
commandBuffer,
|
||||||
|
equeue,
|
||||||
|
ident,
|
||||||
|
completionToken,
|
||||||
|
userData))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceAmpr(ctx, "write_equeue_complete", commandBuffer, ident, completionToken);
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "sJXyWHjP-F8",
|
Nid = "sJXyWHjP-F8",
|
||||||
ExportName = "sceAmprCommandBufferWriteAddressOnCompletion",
|
ExportName = "sceAmprCommandBufferWriteAddressOnCompletion",
|
||||||
|
|||||||
@@ -54,20 +54,41 @@ internal static class PakDirectoryTracker
|
|||||||
|
|
||||||
if (state.Directory is { } entries)
|
if (state.Directory is { } entries)
|
||||||
{
|
{
|
||||||
|
// Several unrelated archived files can share a byte count (e.g. a head model and a bot
|
||||||
|
// navigation file both 0x3A34 bytes). Directory order alone then picks whichever comes
|
||||||
|
// first, which is wrong when the game reads them out of order. id archives cluster
|
||||||
|
// related assets, and the guest streams them with locality, so disambiguate collisions
|
||||||
|
// by choosing the unconsumed match nearest the running read cursor.
|
||||||
|
DirectoryEntry? best = null;
|
||||||
|
var bestDistance = ulong.MaxValue;
|
||||||
foreach (var entry in entries)
|
foreach (var entry in entries)
|
||||||
{
|
{
|
||||||
if (!entry.Consumed && entry.FileLen == requestedSize)
|
if (entry.Consumed || entry.FileLen != requestedSize)
|
||||||
{
|
{
|
||||||
entry.Consumed = true;
|
continue;
|
||||||
if (_trace)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][TRACE] pak.directory_match: id=0x{fileId:X8} name='{entry.Name}' " +
|
|
||||||
$"filepos=0x{entry.FilePos:X8} filelen=0x{entry.FileLen:X8}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return entry.FilePos;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var distance = entry.FilePos >= state.NextOffset
|
||||||
|
? entry.FilePos - state.NextOffset
|
||||||
|
: state.NextOffset - entry.FilePos;
|
||||||
|
if (distance < bestDistance)
|
||||||
|
{
|
||||||
|
bestDistance = distance;
|
||||||
|
best = entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (best is not null)
|
||||||
|
{
|
||||||
|
best.Consumed = true;
|
||||||
|
if (_trace)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] pak.directory_match: id=0x{fileId:X8} name='{best.Name}' " +
|
||||||
|
$"filepos=0x{best.FilePos:X8} filelen=0x{best.FileLen:X8}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return best.FilePos;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,17 @@ public static class AvPlayerExports
|
|||||||
return unchecked((int)handle);
|
return unchecked((int)handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "KMcEa+rHsIo",
|
||||||
|
ExportName = "sceAvPlayerAddSource",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceAvPlayer")]
|
||||||
|
public static int AvPlayerAddSource(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "JdksQu8pNdQ",
|
Nid = "JdksQu8pNdQ",
|
||||||
ExportName = "sceAvPlayerGetVideoDataEx",
|
ExportName = "sceAvPlayerGetVideoDataEx",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using System.Buffers.Binary;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.CommonDialog;
|
namespace SharpEmu.Libs.CommonDialog;
|
||||||
@@ -10,10 +11,21 @@ public static class MsgDialogExports
|
|||||||
{
|
{
|
||||||
private const int StatusNone = 0;
|
private const int StatusNone = 0;
|
||||||
private const int StatusInitialized = 1;
|
private const int StatusInitialized = 1;
|
||||||
|
private const int StatusRunning = 2;
|
||||||
private const int StatusFinished = 3;
|
private const int StatusFinished = 3;
|
||||||
private const int ResultSize = 0x20;
|
|
||||||
|
|
||||||
private static int _initialized;
|
private const int ErrorOk = 0;
|
||||||
|
private const int ErrorNotInitialized = unchecked((int)0x80B80003);
|
||||||
|
private const int ErrorNotFinished = unchecked((int)0x80B80005);
|
||||||
|
private const int ErrorBusy = unchecked((int)0x80B80007);
|
||||||
|
private const int ErrorNotRunning = unchecked((int)0x80B8000B);
|
||||||
|
private const int ErrorArgNull = unchecked((int)0x80B8000D);
|
||||||
|
|
||||||
|
// Result buffer layout follows the common dialog convention: mode at +0x00,
|
||||||
|
// result at +0x04, buttonId at +0x08. The affirmative button (OK/YES) is 1.
|
||||||
|
private const int ResultSize = 0x20;
|
||||||
|
private const int ButtonIdAffirmative = 1;
|
||||||
|
|
||||||
private static int _status;
|
private static int _status;
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -24,11 +36,10 @@ public static class MsgDialogExports
|
|||||||
public static int MsgDialogInitialize(CpuContext ctx)
|
public static int MsgDialogInitialize(CpuContext ctx)
|
||||||
{
|
{
|
||||||
// Treat repeated initialization as success. The dialog service is process-global in
|
// Treat repeated initialization as success. The dialog service is process-global in
|
||||||
// this HLE implementation and has no per-call resources to recreate.
|
// this HLE implementation and has no per-call resources to recreate. Only promote
|
||||||
Interlocked.Exchange(ref _initialized, 1);
|
// from NONE so re-initializing mid-flow cannot clobber a running/finished dialog.
|
||||||
Interlocked.Exchange(ref _status, StatusInitialized);
|
Interlocked.CompareExchange(ref _status, StatusInitialized, StatusNone);
|
||||||
ctx[CpuRegister.Rax] = 0;
|
return ctx.SetReturn(ErrorOk);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -38,9 +49,12 @@ public static class MsgDialogExports
|
|||||||
LibraryName = "libSceMsgDialog")]
|
LibraryName = "libSceMsgDialog")]
|
||||||
public static int MsgDialogTerminate(CpuContext ctx)
|
public static int MsgDialogTerminate(CpuContext ctx)
|
||||||
{
|
{
|
||||||
Interlocked.Exchange(ref _initialized, 0);
|
if (Interlocked.Exchange(ref _status, StatusNone) == StatusNone)
|
||||||
Interlocked.Exchange(ref _status, StatusNone);
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(ErrorNotInitialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.SetReturn(ErrorOk);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -50,13 +64,29 @@ public static class MsgDialogExports
|
|||||||
LibraryName = "libSceMsgDialog")]
|
LibraryName = "libSceMsgDialog")]
|
||||||
public static int MsgDialogOpen(CpuContext ctx)
|
public static int MsgDialogOpen(CpuContext ctx)
|
||||||
{
|
{
|
||||||
LogDialogMessage(ctx, ctx[CpuRegister.Rdi]);
|
var paramAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (paramAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(ErrorArgNull);
|
||||||
|
}
|
||||||
|
|
||||||
// There is no host popup to actually show. Complete immediately with "finished" so a
|
var status = Volatile.Read(ref _status);
|
||||||
// guest polling loop (GetStatus/UpdateStatus -> GetResult -> Close) sees a dismissed
|
if (status == StatusNone)
|
||||||
// dialog on its very first poll instead of spinning forever waiting for user input.
|
{
|
||||||
Interlocked.Exchange(ref _status, StatusFinished);
|
return ctx.SetReturn(ErrorNotInitialized);
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
}
|
||||||
|
|
||||||
|
if (status == StatusRunning)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(ErrorBusy);
|
||||||
|
}
|
||||||
|
|
||||||
|
LogDialogMessage(ctx, paramAddress);
|
||||||
|
|
||||||
|
// There is no host popup to actually show. Enter RUNNING so close/cancel paths see
|
||||||
|
// a live dialog; the guest's next status poll auto-dismisses it (see PollStatus).
|
||||||
|
Interlocked.Exchange(ref _status, StatusRunning);
|
||||||
|
return ctx.SetReturn(ErrorOk);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Best-effort extraction of the dialog text so fatal-error popups are visible in the
|
// Best-effort extraction of the dialog text so fatal-error popups are visible in the
|
||||||
@@ -65,12 +95,6 @@ public static class MsgDialogExports
|
|||||||
// level deep, then a second level for nested sub-param structs.
|
// level deep, then a second level for nested sub-param structs.
|
||||||
private static void LogDialogMessage(CpuContext ctx, ulong paramAddress)
|
private static void LogDialogMessage(CpuContext ctx, ulong paramAddress)
|
||||||
{
|
{
|
||||||
if (paramAddress == 0)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine("[LOADER][INFO] sceMsgDialogOpen: param=NULL");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] sceMsgDialogOpen: param=0x{paramAddress:X12}");
|
Console.Error.WriteLine($"[LOADER][INFO] sceMsgDialogOpen: param=0x{paramAddress:X12}");
|
||||||
|
|
||||||
Span<byte> head = stackalloc byte[0xA0];
|
Span<byte> head = stackalloc byte[0xA0];
|
||||||
@@ -134,14 +158,24 @@ public static class MsgDialogExports
|
|||||||
ExportName = "sceMsgDialogGetStatus",
|
ExportName = "sceMsgDialogGetStatus",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libSceMsgDialog")]
|
LibraryName = "libSceMsgDialog")]
|
||||||
public static int MsgDialogGetStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
|
public static int MsgDialogGetStatus(CpuContext ctx) => ctx.SetReturn(PollStatus());
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "6fIC3XKt2k0",
|
Nid = "6fIC3XKt2k0",
|
||||||
ExportName = "sceMsgDialogUpdateStatus",
|
ExportName = "sceMsgDialogUpdateStatus",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libSceMsgDialog")]
|
LibraryName = "libSceMsgDialog")]
|
||||||
public static int MsgDialogUpdateStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
|
public static int MsgDialogUpdateStatus(CpuContext ctx) => ctx.SetReturn(PollStatus());
|
||||||
|
|
||||||
|
// With no host UI the dialog cannot wait for user input: the first status poll after
|
||||||
|
// Open observes the dialog as already dismissed. Advancing on both UpdateStatus and
|
||||||
|
// GetStatus keeps every guest polling pattern free of infinite RUNNING loops, while
|
||||||
|
// an Open -> Close sequence with no poll in between still exercises the close path.
|
||||||
|
private static int PollStatus()
|
||||||
|
{
|
||||||
|
Interlocked.CompareExchange(ref _status, StatusFinished, StatusRunning);
|
||||||
|
return Volatile.Read(ref _status);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "Lr8ovHH9l6A",
|
Nid = "Lr8ovHH9l6A",
|
||||||
@@ -153,17 +187,27 @@ public static class MsgDialogExports
|
|||||||
var resultAddress = ctx[CpuRegister.Rdi];
|
var resultAddress = ctx[CpuRegister.Rdi];
|
||||||
if (resultAddress == 0)
|
if (resultAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(ErrorArgNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Volatile.Read(ref _status) != StatusFinished)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(ErrorNotFinished);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report the affirmative button so yes/no prompts take the confirming branch;
|
||||||
|
// buttonId 0 is the "invalid" sentinel and games may treat it as an error.
|
||||||
Span<byte> result = stackalloc byte[ResultSize];
|
Span<byte> result = stackalloc byte[ResultSize];
|
||||||
result.Clear();
|
result.Clear();
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(result[0x04..], 0);
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(result[0x08..], ButtonIdAffirmative);
|
||||||
|
|
||||||
if (!ctx.Memory.TryWrite(resultAddress, result))
|
if (!ctx.Memory.TryWrite(resultAddress, result))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(ErrorOk);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -173,13 +217,44 @@ public static class MsgDialogExports
|
|||||||
LibraryName = "libSceMsgDialog")]
|
LibraryName = "libSceMsgDialog")]
|
||||||
public static int MsgDialogClose(CpuContext ctx)
|
public static int MsgDialogClose(CpuContext ctx)
|
||||||
{
|
{
|
||||||
Interlocked.Exchange(ref _status, StatusFinished);
|
if (Interlocked.CompareExchange(ref _status, StatusFinished, StatusRunning) != StatusRunning)
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
{
|
||||||
|
return ctx.SetReturn(ErrorNotRunning);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.SetReturn(ErrorOk);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int SetReturn(CpuContext ctx, int result)
|
[SysAbiExport(
|
||||||
|
Nid = "wTpfglkmv34",
|
||||||
|
ExportName = "sceMsgDialogProgressBarSetValue",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceMsgDialog")]
|
||||||
|
public static int MsgDialogProgressBarSetValue(CpuContext ctx) => ProgressBarNoOp(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Gc5k1qcK4fs",
|
||||||
|
ExportName = "sceMsgDialogProgressBarInc",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceMsgDialog")]
|
||||||
|
public static int MsgDialogProgressBarInc(CpuContext ctx) => ProgressBarNoOp(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "6H-71OdrpXM",
|
||||||
|
ExportName = "sceMsgDialogProgressBarSetMsg",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceMsgDialog")]
|
||||||
|
public static int MsgDialogProgressBarSetMsg(CpuContext ctx) => ProgressBarNoOp(ctx);
|
||||||
|
|
||||||
|
// There is no visible bar to update. Accept the call whenever the service is alive so
|
||||||
|
// save/install loops that report progress do not abort on an unexpected error.
|
||||||
|
private static int ProgressBarNoOp(CpuContext ctx)
|
||||||
{
|
{
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
if (Volatile.Read(ref _status) == StatusNone)
|
||||||
return result;
|
{
|
||||||
|
return ctx.SetReturn(ErrorNotInitialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.SetReturn(ErrorOk);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,30 @@ public static class JsonExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sce::Json::Initializer::setGlobalNullAccessCallback(const Value& (*)(ValueType, const Value*, void*), void*)
|
||||||
|
// Registers the guest hook invoked when a Value is accessed as the wrong type. Quake calls it
|
||||||
|
// during kexPSNWebAPI::Initialize and treats a non-zero return as a fatal init failure.
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "+drDFyAS6u4",
|
||||||
|
ExportName = "_ZN3sce4Json11Initializer27setGlobalNullAccessCallbackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int InitializerSetGlobalNullAccessCallback(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (thisAddress == 0)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObjectHeap.GlobalNullAccessCallback = ctx[CpuRegister.Rsi];
|
||||||
|
JsonObjectHeap.GlobalNullAccessCallbackContext = ctx[CpuRegister.Rdx];
|
||||||
|
TraceJson("Initializer.setGlobalNullAccessCallback", thisAddress, ctx[CpuRegister.Rsi]);
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "WSOuge5IsCg",
|
Nid = "WSOuge5IsCg",
|
||||||
ExportName = "_ZN3sce4Json14InitParameter2C1Ev",
|
ExportName = "_ZN3sce4Json14InitParameter2C1Ev",
|
||||||
|
|||||||
@@ -0,0 +1,234 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Json;
|
||||||
|
|
||||||
|
// sce::Json::Value and sce::Json::String constructors, setters and destructors. Prospero titles
|
||||||
|
// (Quake among them) build a Value tree and populate it through these before serializing it for a
|
||||||
|
// web request; without them the imports resolve to nothing and the guest faults on the call. The
|
||||||
|
// payload is modelled in JsonObjectHeap; here we only translate the C++ ABI (registers in, `this`
|
||||||
|
// back out) and never write the guest object, so a stack-allocated Value/String is left intact.
|
||||||
|
//
|
||||||
|
// Only the complete-object variants (C1/D1) are bound — those are what standalone locals emit and
|
||||||
|
// what the observed Prospero imports use. The base-object variants (C2/D2) are left for a title
|
||||||
|
// that actually imports them.
|
||||||
|
public static class JsonValueExports
|
||||||
|
{
|
||||||
|
private const int MaxStringLength = 0x10000;
|
||||||
|
|
||||||
|
private static int ReturnThis(CpuContext ctx, ulong thisAddress)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = thisAddress;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ReturnVoid(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double ReadDoubleArg(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx.GetXmmRegister(0, out var low, out _);
|
||||||
|
return BitConverter.Int64BitsToDouble(unchecked((long)low));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadCString(CpuContext ctx, ulong address) =>
|
||||||
|
ctx.TryReadNullTerminatedUtf8(address, MaxStringLength, out var text) ? text : string.Empty;
|
||||||
|
|
||||||
|
// ---- sce::Json::Value constructors ----
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "qBMjqyBn3OM", ExportName = "_ZN3sce4Json5ValueC1Ev",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueDefaultConstructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetValue(thisAddress, JsonValueState.Null);
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "UeuWT+yNdCQ", ExportName = "_ZN3sce4Json5ValueC1Eb",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueBooleanConstructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromBoolean((ctx[CpuRegister.Rsi] & 0xFF) != 0));
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "0lLK8+kDqmE", ExportName = "_ZN3sce4Json5ValueC1El",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueIntegerConstructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromInteger(unchecked((long)ctx[CpuRegister.Rsi])));
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "x4AUdbhpRB0", ExportName = "_ZN3sce4Json5ValueC1Em",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueUnsignedConstructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromUnsignedInteger(ctx[CpuRegister.Rsi]));
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "sOmU4vnx3s0", ExportName = "_ZN3sce4Json5ValueC1Ed",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueRealConstructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromReal(ReadDoubleArg(ctx)));
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "b9V6fmppLXY", ExportName = "_ZN3sce4Json5ValueC1EPKc",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueCStringConstructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromString(ReadCString(ctx, ctx[CpuRegister.Rsi])));
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "CbrT3dwDILo", ExportName = "_ZN3sce4Json5ValueC1ENS0_9ValueTypeE",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueTypeConstructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromExplicitType(unchecked((uint)ctx[CpuRegister.Rsi])));
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "sZIoMRGO+jk", ExportName = "_ZN3sce4Json5ValueC1ERKNS0_6StringE",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueStringConstructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromString(JsonObjectHeap.GetStringOrEmpty(ctx[CpuRegister.Rsi])));
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "WTtYf+cNnXI", ExportName = "_ZN3sce4Json5ValueD1Ev",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueDestructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
JsonObjectHeap.RemoveValue(ctx[CpuRegister.Rdi]);
|
||||||
|
return ReturnVoid(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- sce::Json::Value setters (return Value&, i.e. `this`) ----
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "5yHuiWXo2gg", ExportName = "_ZN3sce4Json5Value3setEb",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueSetBoolean(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromBoolean((ctx[CpuRegister.Rsi] & 0xFF) != 0));
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "QxVVYhP-mvg", ExportName = "_ZN3sce4Json5Value3setEl",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueSetInteger(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromInteger(unchecked((long)ctx[CpuRegister.Rsi])));
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "SIe1ZmW7e7s", ExportName = "_ZN3sce4Json5Value3setEm",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueSetUnsigned(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromUnsignedInteger(ctx[CpuRegister.Rsi]));
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "BSmWDIkV4w4", ExportName = "_ZN3sce4Json5Value3setEd",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueSetReal(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromReal(ReadDoubleArg(ctx)));
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "IKQimvG9Wqs", ExportName = "_ZN3sce4Json5Value3setENS0_9ValueTypeE",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueSetType(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromExplicitType(unchecked((uint)ctx[CpuRegister.Rsi])));
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "n6FC+l9DU70", ExportName = "_ZN3sce4Json5Value3setEPKc",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueSetCString(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromString(ReadCString(ctx, ctx[CpuRegister.Rsi])));
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "6l3Bv2gysNc", ExportName = "_ZN3sce4Json5Value3setERKNS0_6StringE",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueSetString(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromString(JsonObjectHeap.GetStringOrEmpty(ctx[CpuRegister.Rsi])));
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "FIjXN2TkuTs", ExportName = "_ZN3sce4Json5Value5clearEv",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int ValueClear(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetValue(thisAddress, JsonValueState.Null);
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- sce::Json::String ----
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "9KUZFjI1IxA", ExportName = "_ZN3sce4Json6StringC1EPKc",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int StringCStringConstructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetString(thisAddress, ReadCString(ctx, ctx[CpuRegister.Rsi]));
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "qSmqLXXCPas", ExportName = "_ZN3sce4Json6StringC1Ev",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int StringDefaultConstructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetString(thisAddress, string.Empty);
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "0CAesfH963Q", ExportName = "_ZN3sce4Json6StringC1ERKS1_",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int StringCopyConstructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
JsonObjectHeap.SetString(thisAddress, JsonObjectHeap.GetStringOrEmpty(ctx[CpuRegister.Rsi]));
|
||||||
|
return ReturnThis(ctx, thisAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "cG1VE2HMl6c", ExportName = "_ZN3sce4Json6StringD1Ev",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||||
|
public static int StringDestructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
JsonObjectHeap.RemoveString(ctx[CpuRegister.Rdi]);
|
||||||
|
return ReturnVoid(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Json;
|
||||||
|
|
||||||
|
// sce::Json::Value is an opaque variant type (null / bool / signed / unsigned / real / string /
|
||||||
|
// array / object). Games build one, populate it through set()/ctors and later serialize it. We
|
||||||
|
// model the payload host-side keyed by the guest `this` pointer instead of writing into the guest
|
||||||
|
// object: the object is often stack-allocated and its real byte layout is unknown, so writing a
|
||||||
|
// guessed layout risks smashing an adjacent stack canary (the same failure the AudioOut2 context
|
||||||
|
// param sizing note in this project already ran into). The guest reaches the payload only through
|
||||||
|
// libSceJson methods, so shadowing by address is enough for the build path.
|
||||||
|
internal enum JsonValueKind : byte
|
||||||
|
{
|
||||||
|
Null = 0,
|
||||||
|
Boolean = 1,
|
||||||
|
Integer = 2,
|
||||||
|
UInteger = 3,
|
||||||
|
Real = 4,
|
||||||
|
String = 5,
|
||||||
|
|
||||||
|
// set(ValueType) / Value(ValueType): the guest chose the type itself. We keep its raw enum
|
||||||
|
// value verbatim rather than mapping it, because the canonical ValueType constants are not
|
||||||
|
// known from clean-room evidence and round-tripping the guest's own value is what matters.
|
||||||
|
ExplicitType = 6,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal readonly struct JsonValueState
|
||||||
|
{
|
||||||
|
private JsonValueState(
|
||||||
|
JsonValueKind kind,
|
||||||
|
bool boolean = false,
|
||||||
|
long integer = 0,
|
||||||
|
ulong unsignedInteger = 0,
|
||||||
|
double real = 0,
|
||||||
|
string? text = null,
|
||||||
|
uint explicitType = 0)
|
||||||
|
{
|
||||||
|
Kind = kind;
|
||||||
|
Boolean = boolean;
|
||||||
|
Integer = integer;
|
||||||
|
UnsignedInteger = unsignedInteger;
|
||||||
|
Real = real;
|
||||||
|
Text = text;
|
||||||
|
ExplicitType = explicitType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonValueKind Kind { get; }
|
||||||
|
public bool Boolean { get; }
|
||||||
|
public long Integer { get; }
|
||||||
|
public ulong UnsignedInteger { get; }
|
||||||
|
public double Real { get; }
|
||||||
|
public string? Text { get; }
|
||||||
|
public uint ExplicitType { get; }
|
||||||
|
|
||||||
|
public static JsonValueState Null { get; } = new(JsonValueKind.Null);
|
||||||
|
|
||||||
|
public static JsonValueState FromBoolean(bool value) => new(JsonValueKind.Boolean, boolean: value);
|
||||||
|
|
||||||
|
public static JsonValueState FromInteger(long value) => new(JsonValueKind.Integer, integer: value);
|
||||||
|
|
||||||
|
public static JsonValueState FromUnsignedInteger(ulong value) =>
|
||||||
|
new(JsonValueKind.UInteger, unsignedInteger: value);
|
||||||
|
|
||||||
|
public static JsonValueState FromReal(double value) => new(JsonValueKind.Real, real: value);
|
||||||
|
|
||||||
|
public static JsonValueState FromString(string value) => new(JsonValueKind.String, text: value);
|
||||||
|
|
||||||
|
public static JsonValueState FromExplicitType(uint value) =>
|
||||||
|
new(JsonValueKind.ExplicitType, explicitType: value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shared host-side heap for the libSceJson object shadows. Keyed by the guest object address;
|
||||||
|
// constructors overwrite and destructors remove, so guest stack-address reuse stays correct.
|
||||||
|
internal static class JsonObjectHeap
|
||||||
|
{
|
||||||
|
public static ConcurrentDictionary<ulong, JsonValueState> Values { get; } = new();
|
||||||
|
|
||||||
|
public static ConcurrentDictionary<ulong, string> Strings { get; } = new();
|
||||||
|
|
||||||
|
// Guest function the library should call when a Value is read as the wrong type. This HLE
|
||||||
|
// never dereferences missing members (shadows degrade to defaults), so the hook is stored for
|
||||||
|
// fidelity but not invoked.
|
||||||
|
public static ulong GlobalNullAccessCallback;
|
||||||
|
|
||||||
|
public static ulong GlobalNullAccessCallbackContext;
|
||||||
|
|
||||||
|
public static void SetValue(ulong address, JsonValueState state) => Values[address] = state;
|
||||||
|
|
||||||
|
public static void RemoveValue(ulong address) => Values.TryRemove(address, out _);
|
||||||
|
|
||||||
|
public static void SetString(ulong address, string text) => Strings[address] = text;
|
||||||
|
|
||||||
|
public static void RemoveString(ulong address) => Strings.TryRemove(address, out _);
|
||||||
|
|
||||||
|
// A missing shadow (temporary the compiler built without an out-of-line ctor, or a copy we did
|
||||||
|
// not track) degrades to the empty string rather than faulting.
|
||||||
|
public static string GetStringOrEmpty(ulong address) =>
|
||||||
|
Strings.TryGetValue(address, out var text) ? text : string.Empty;
|
||||||
|
|
||||||
|
internal static void ResetForTests()
|
||||||
|
{
|
||||||
|
Values.Clear();
|
||||||
|
Strings.Clear();
|
||||||
|
GlobalNullAccessCallback = 0;
|
||||||
|
GlobalNullAccessCallbackContext = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
using SharpEmu.Libs.Ampr;
|
using SharpEmu.Libs.Ampr;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
@@ -52,14 +53,13 @@ public static class KernelMemoryCompatExports
|
|||||||
private const ulong FlexibleMemorySizeBytes = 448UL * 1024 * 1024;
|
private const ulong FlexibleMemorySizeBytes = 448UL * 1024 * 1024;
|
||||||
private const int OrbisVirtualQueryInfoSize = 72;
|
private const int OrbisVirtualQueryInfoSize = 72;
|
||||||
private const int OrbisKernelMaximumNameLength = 32;
|
private const int OrbisKernelMaximumNameLength = 32;
|
||||||
private const uint MemCommit = 0x1000;
|
// Raw Windows PAGE_* values used only against HostRegionInfo.RawProtection,
|
||||||
private const uint MemReserve = 0x2000;
|
// which by contract carries the untranslated protection word of the host
|
||||||
private const uint MemRelease = 0x8000;
|
// platform in use (see IHostMemory).
|
||||||
private const uint HostPageNoAccess = 0x01;
|
private const uint HostPageNoAccess = 0x01;
|
||||||
private const uint HostPageReadOnly = 0x02;
|
private const uint HostPageReadOnly = 0x02;
|
||||||
private const uint HostPageReadWrite = 0x04;
|
private const uint HostPageReadWrite = 0x04;
|
||||||
private const uint HostPageWriteCopy = 0x08;
|
private const uint HostPageWriteCopy = 0x08;
|
||||||
private const uint HostPageExecute = 0x10;
|
|
||||||
private const uint HostPageExecuteRead = 0x20;
|
private const uint HostPageExecuteRead = 0x20;
|
||||||
private const uint HostPageExecuteReadWrite = 0x40;
|
private const uint HostPageExecuteReadWrite = 0x40;
|
||||||
private const uint HostPageExecuteWriteCopy = 0x80;
|
private const uint HostPageExecuteWriteCopy = 0x80;
|
||||||
@@ -136,31 +136,9 @@ public static class KernelMemoryCompatExports
|
|||||||
private static string? _cachedApp0Root;
|
private static string? _cachedApp0Root;
|
||||||
private static string? _cachedDownload0Root;
|
private static string? _cachedDownload0Root;
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
// Property (not a cached field) so merely touching this type never resolves
|
||||||
private struct MemoryBasicInformation
|
// the platform backend; non-Windows hosts only throw if a call is reached.
|
||||||
{
|
private static IHostMemory HostMemory => HostPlatform.Current.Memory;
|
||||||
public nint BaseAddress;
|
|
||||||
public nint AllocationBase;
|
|
||||||
public uint AllocationProtect;
|
|
||||||
public nuint RegionSize;
|
|
||||||
public uint State;
|
|
||||||
public uint Protect;
|
|
||||||
public uint Type;
|
|
||||||
}
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
private static extern nuint VirtualQuery(nint lpAddress, out MemoryBasicInformation lpBuffer, nuint dwLength);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
|
||||||
private static extern bool VirtualProtect(nint lpAddress, nuint dwSize, uint flNewProtect, out uint lpflOldProtect);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
private static extern nint VirtualAlloc(nint lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
|
||||||
private static extern bool VirtualFree(nint lpAddress, nuint dwSize, uint dwFreeType);
|
|
||||||
|
|
||||||
private sealed class OpenDirectory
|
private sealed class OpenDirectory
|
||||||
{
|
{
|
||||||
@@ -200,6 +178,40 @@ public static class KernelMemoryCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool TryUnregisterGuestPathMount(string guestMountPoint)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(guestMountPoint))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedMountPoint = NormalizeGuestStatCachePath(guestMountPoint);
|
||||||
|
if (normalizedMountPoint is null || normalizedMountPoint == "/")
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var removed = false;
|
||||||
|
lock (_guestMountGate)
|
||||||
|
{
|
||||||
|
removed = _guestMounts.Remove(normalizedMountPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!removed)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_statCacheGate)
|
||||||
|
{
|
||||||
|
_negativeStatCache.RemoveWhere(path =>
|
||||||
|
string.Equals(path, normalizedMountPoint, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
path.StartsWith(normalizedMountPoint + "/", StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
internal static bool TryAllocateHleData(
|
internal static bool TryAllocateHleData(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
ulong length,
|
ulong length,
|
||||||
@@ -1822,6 +1834,45 @@ public static class KernelMemoryCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "fgIsQ10xYVA",
|
||||||
|
ExportName = "sceKernelChmod",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelChmod(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var pathAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var mode = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||||
|
if (pathAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadNullTerminatedUtf8(ctx, pathAddress, MaxGuestStringLength, out var guestPath))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hostPath = ResolveGuestPath(guestPath);
|
||||||
|
if (IsReadOnlyGuestMutationPath(guestPath))
|
||||||
|
{
|
||||||
|
LogOpenTrace($"chmod readonly path='{guestPath}' host='{hostPath}' mode=0x{mode:X}");
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(hostPath) && !Directory.Exists(hostPath))
|
||||||
|
{
|
||||||
|
AddNegativeStatCacheForGuestPath(guestPath);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
// POSIX permission bits have no host equivalent on Windows; accept the call
|
||||||
|
// so guests that chmod their freshly created files/directories can proceed.
|
||||||
|
LogOpenTrace($"chmod path='{guestPath}' host='{hostPath}' mode=0x{mode:X}");
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "NNtFaKJbPt0",
|
Nid = "NNtFaKJbPt0",
|
||||||
ExportName = "_close",
|
ExportName = "_close",
|
||||||
@@ -3529,7 +3580,7 @@ public static class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryProtectHostRange(alignedAddress, alignedLength, protection))
|
if (!TryProtectHostRange(ctx, alignedAddress, alignedLength, protection))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
@@ -3563,7 +3614,7 @@ public static class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryProtectHostRange(alignedAddress, alignedLength, protection))
|
if (!TryProtectHostRange(ctx, alignedAddress, alignedLength, protection))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
@@ -5763,42 +5814,40 @@ public static class KernelMemoryCompatExports
|
|||||||
return alignedLength != 0;
|
return alignedLength != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryProtectHostRange(ulong address, ulong length, int orbisProtection)
|
private static bool TryProtectHostRange(CpuContext ctx, ulong address, ulong length, int orbisProtection)
|
||||||
{
|
{
|
||||||
if (length == 0 || length > nuint.MaxValue)
|
if (length == 0 || length > nuint.MaxValue)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var hostProtection = ResolveHostProtection(orbisProtection);
|
if (!KernelVirtualRangeAllocator.TryResolveAddressSpace(ctx.Memory, out var addressSpace))
|
||||||
if (!VirtualProtect((nint)address, (nuint)length, hostProtection, out _))
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return addressSpace.TryProtect(address, length, ResolveGuestProtection(orbisProtection));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static uint ResolveHostProtection(int orbisProtection)
|
private static GuestPageProtection ResolveGuestProtection(int orbisProtection)
|
||||||
{
|
{
|
||||||
var read = (orbisProtection & (OrbisProtCpuRead | OrbisProtGpuRead)) != 0;
|
var protection = GuestPageProtection.None;
|
||||||
var write = (orbisProtection & (OrbisProtCpuWrite | OrbisProtGpuWrite)) != 0;
|
if ((orbisProtection & (OrbisProtCpuRead | OrbisProtGpuRead)) != 0)
|
||||||
var execute = (orbisProtection & OrbisProtCpuExec) != 0;
|
|
||||||
|
|
||||||
if (execute)
|
|
||||||
{
|
{
|
||||||
return write
|
protection |= GuestPageProtection.Read;
|
||||||
? HostPageExecuteReadWrite
|
|
||||||
: read
|
|
||||||
? HostPageExecuteRead
|
|
||||||
: HostPageExecute;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return write
|
if ((orbisProtection & (OrbisProtCpuWrite | OrbisProtGpuWrite)) != 0)
|
||||||
? HostPageReadWrite
|
{
|
||||||
: read
|
protection |= GuestPageProtection.Write;
|
||||||
? HostPageReadOnly
|
}
|
||||||
: HostPageNoAccess;
|
|
||||||
|
if ((orbisProtection & OrbisProtCpuExec) != 0)
|
||||||
|
{
|
||||||
|
protection |= GuestPageProtection.Execute;
|
||||||
|
}
|
||||||
|
|
||||||
|
return protection;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryFindVirtualQueryRegionLocked(ulong queryAddress, bool findNext, out MappedRegion region)
|
private static bool TryFindVirtualQueryRegionLocked(ulong queryAddress, bool findNext, out MappedRegion region)
|
||||||
@@ -6184,7 +6233,7 @@ public static class KernelMemoryCompatExports
|
|||||||
var effectiveAlignment = Math.Max(alignment, pageSize);
|
var effectiveAlignment = Math.Max(alignment, pageSize);
|
||||||
var usableSize = checked((nuint)AlignUp((ulong)actualSize, (ulong)pageSize));
|
var usableSize = checked((nuint)AlignUp((ulong)actualSize, (ulong)pageSize));
|
||||||
var reservationSize = checked(pageSize + effectiveAlignment - 1 + usableSize + pageSize);
|
var reservationSize = checked(pageSize + effectiveAlignment - 1 + usableSize + pageSize);
|
||||||
var baseAddress = VirtualAlloc(0, reservationSize, MemCommit | MemReserve, HostPageReadWrite);
|
var baseAddress = unchecked((nint)HostMemory.Allocate(0, reservationSize, HostPageProtection.ReadWrite));
|
||||||
if (baseAddress == 0)
|
if (baseAddress == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -6192,9 +6241,9 @@ public static class KernelMemoryCompatExports
|
|||||||
|
|
||||||
var alignedAddress = AlignUp(unchecked((ulong)baseAddress) + (ulong)pageSize, (ulong)effectiveAlignment);
|
var alignedAddress = AlignUp(unchecked((ulong)baseAddress) + (ulong)pageSize, (ulong)effectiveAlignment);
|
||||||
var guardAddress = alignedAddress + (ulong)usableSize;
|
var guardAddress = alignedAddress + (ulong)usableSize;
|
||||||
if (!VirtualProtect((nint)guardAddress, pageSize, HostPageNoAccess, out _))
|
if (!HostMemory.Protect(guardAddress, pageSize, HostPageProtection.NoAccess, out _))
|
||||||
{
|
{
|
||||||
_ = VirtualFree(baseAddress, 0, MemRelease);
|
_ = HostMemory.Free(unchecked((ulong)baseAddress));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6329,7 +6378,7 @@ public static class KernelMemoryCompatExports
|
|||||||
|
|
||||||
if (allocation.IsGuarded)
|
if (allocation.IsGuarded)
|
||||||
{
|
{
|
||||||
_ = VirtualFree(allocation.BaseAddress, 0, MemRelease);
|
_ = HostMemory.Free(unchecked((ulong)allocation.BaseAddress));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -6425,7 +6474,7 @@ public static class KernelMemoryCompatExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryQueryHostPage(address, out var startInfo) || !HasRequiredProtection(startInfo.Protect, writeAccess))
|
if (!TryQueryHostPage(address, out var startInfo) || !HasRequiredProtection(startInfo.RawProtection, writeAccess))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -6436,7 +6485,7 @@ public static class KernelMemoryCompatExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryQueryHostPage(endAddress, out var endInfo) || !HasRequiredProtection(endInfo.Protect, writeAccess))
|
if (!TryQueryHostPage(endAddress, out var endInfo) || !HasRequiredProtection(endInfo.RawProtection, writeAccess))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -6444,16 +6493,14 @@ public static class KernelMemoryCompatExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryQueryHostPage(ulong address, out MemoryBasicInformation info)
|
private static bool TryQueryHostPage(ulong address, out HostRegionInfo info)
|
||||||
{
|
{
|
||||||
info = default;
|
if (!HostMemory.Query(address, out info))
|
||||||
var size = (nuint)Marshal.SizeOf<MemoryBasicInformation>();
|
|
||||||
if (VirtualQuery((nint)address, out info, size) == 0)
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return info.State == MemCommit;
|
return info.State == HostRegionState.Committed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool HasRequiredProtection(uint protect, bool writeAccess)
|
private static bool HasRequiredProtection(uint protect, bool writeAccess)
|
||||||
@@ -6947,15 +6994,4 @@ public static class KernelMemoryCompatExports
|
|||||||
sum = left + right;
|
sum = left + right;
|
||||||
return sum >= left;
|
return sum >= left;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "KMcEa+rHsIo",
|
|
||||||
ExportName = "sceKernelMapMemory",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int KernelMapMemory(CpuContext ctx)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -382,6 +382,47 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "oIRFTjoILbg",
|
||||||
|
ExportName = "scePthreadSetschedparam",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadSetschedparam(CpuContext ctx) => PosixPthreadSetschedparam(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "P41kTWUS3EI",
|
||||||
|
ExportName = "scePthreadGetschedparam",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadGetschedparam(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thread = ctx[CpuRegister.Rdi];
|
||||||
|
var outPolicyAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var outSchedParamAddress = ctx[CpuRegister.Rdx];
|
||||||
|
if (thread == 0 || outPolicyAddress == 0 || outSchedParamAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
int policy;
|
||||||
|
int priority;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
var state = GetOrCreateThreadStateLocked(thread);
|
||||||
|
policy = state.Attributes.SchedPolicy;
|
||||||
|
priority = state.Priority;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryWriteInt32(outPolicyAddress, policy) ||
|
||||||
|
!ctx.TryWriteInt32(outSchedParamAddress, priority))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "nsYoNRywwNg",
|
Nid = "nsYoNRywwNg",
|
||||||
ExportName = "scePthreadAttrInit",
|
ExportName = "scePthreadAttrInit",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
using SharpEmu.Libs.Fiber;
|
using SharpEmu.Libs.Fiber;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
@@ -48,9 +49,6 @@ public static class KernelRuntimeCompatExports
|
|||||||
private const int MapFlagFixed = 0x10;
|
private const int MapFlagFixed = 0x10;
|
||||||
private const ulong DefaultVirtualRangeAlignment = 0x4000UL;
|
private const ulong DefaultVirtualRangeAlignment = 0x4000UL;
|
||||||
private const int AioInitParamSize = 0x3C;
|
private const int AioInitParamSize = 0x3C;
|
||||||
private const uint MemCommit = 0x1000;
|
|
||||||
private const uint MemReserve = 0x2000;
|
|
||||||
private const uint PageExecuteReadWrite = 0x40;
|
|
||||||
private static readonly object _stateGate = new();
|
private static readonly object _stateGate = new();
|
||||||
private static readonly long _processStartCounter = Stopwatch.GetTimestamp();
|
private static readonly long _processStartCounter = Stopwatch.GetTimestamp();
|
||||||
private static readonly RdtscDelegate? _rdtscReader = CreateRdtscReader();
|
private static readonly RdtscDelegate? _rdtscReader = CreateRdtscReader();
|
||||||
@@ -1893,7 +1891,7 @@ public static class KernelRuntimeCompatExports
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
nint stubAddress = VirtualAlloc(nint.Zero, (nuint)16, MemCommit | MemReserve, PageExecuteReadWrite);
|
nint stubAddress = unchecked((nint)HostPlatform.Current.Memory.Allocate(0, 16, HostPageProtection.ReadWriteExecute));
|
||||||
if (stubAddress == 0)
|
if (stubAddress == 0)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
@@ -1923,9 +1921,6 @@ public static class KernelRuntimeCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
private static extern nint VirtualAlloc(nint lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
|
||||||
|
|
||||||
private static bool TryReserveVirtualRange(
|
private static bool TryReserveVirtualRange(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
ulong desiredAddress,
|
ulong desiredAddress,
|
||||||
|
|||||||
@@ -3,15 +3,12 @@
|
|||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Kernel;
|
namespace SharpEmu.Libs.Kernel;
|
||||||
|
|
||||||
internal static class KernelVirtualRangeAllocator
|
internal static class KernelVirtualRangeAllocator
|
||||||
{
|
{
|
||||||
private static readonly ConcurrentDictionary<Type, Accessor> _accessors = new();
|
|
||||||
|
|
||||||
public static bool TryReserve(
|
public static bool TryReserve(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
ulong desiredAddress,
|
ulong desiredAddress,
|
||||||
@@ -31,38 +28,24 @@ internal static class KernelVirtualRangeAllocator
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!TryResolveAccessor(ctx.Memory, out var target, out var accessor))
|
if (!TryResolveAddressSpace(ctx.Memory, out var addressSpace))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt missing on {ctx.Memory.GetType().FullName}");
|
Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt missing on {ctx.Memory.GetType().FullName}");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allowSearch && accessor.AllocateAtOrAbove is not null)
|
if (allowSearch &&
|
||||||
|
addressSpace.TryAllocateAtOrAbove(desiredAddress, length, executable, alignment, out var searchedAddress) &&
|
||||||
|
searchedAddress != 0)
|
||||||
{
|
{
|
||||||
var searchArgs = new object[] { desiredAddress, length, executable, alignment, 0UL };
|
mappedAddress = searchedAddress;
|
||||||
var searchResult = accessor.AllocateAtOrAbove.Invoke(target, searchArgs);
|
return true;
|
||||||
if (searchResult is bool trueValue && trueValue &&
|
|
||||||
searchArgs[4] is ulong searchedAddress && searchedAddress != 0)
|
|
||||||
{
|
|
||||||
mappedAddress = searchedAddress;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accessor.AllocateAt is null)
|
var allocated = addressSpace.AllocateAt(desiredAddress, length, executable, allowAllocateAtAlternative);
|
||||||
|
if (allocated == 0)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt missing on {target.GetType().FullName}");
|
Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt returned {typeof(ulong).FullName} value=0");
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var invokeArgs = accessor.AllocateAtHasAllowAlternativeArg
|
|
||||||
? new object[] { desiredAddress, length, executable, allowAllocateAtAlternative }
|
|
||||||
: new object[] { desiredAddress, length, executable };
|
|
||||||
var result = accessor.AllocateAt.Invoke(target, invokeArgs);
|
|
||||||
if (result is not ulong allocated || allocated == 0)
|
|
||||||
{
|
|
||||||
var resultType = result?.GetType().FullName ?? "null";
|
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt returned {resultType} value={result ?? "null"}");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,84 +59,36 @@ internal static class KernelVirtualRangeAllocator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryResolveAccessor(object rootMemory, out object target, out Accessor accessor)
|
/// <summary>
|
||||||
|
/// Finds the <see cref="IGuestAddressSpace"/> behind <paramref name="rootMemory"/>,
|
||||||
|
/// unwrapping decorators (bounded, like the reflection walker this replaced).
|
||||||
|
/// </summary>
|
||||||
|
public static bool TryResolveAddressSpace(ICpuMemory rootMemory, [NotNullWhen(true)] out IGuestAddressSpace? addressSpace)
|
||||||
{
|
{
|
||||||
target = rootMemory;
|
var target = rootMemory;
|
||||||
accessor = default;
|
|
||||||
|
|
||||||
for (var depth = 0; depth < 4; depth++)
|
for (var depth = 0; depth < 4; depth++)
|
||||||
{
|
{
|
||||||
accessor = _accessors.GetOrAdd(target.GetType(), DiscoverAccessor);
|
if (target is IGuestAddressSpace resolved)
|
||||||
if (accessor.AllocateAt is not null || accessor.AllocateAtOrAbove is not null)
|
|
||||||
{
|
{
|
||||||
|
addressSpace = resolved;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accessor.InnerProperty is null)
|
if (target is not ICpuMemoryWrapper wrapper)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
var innerValue = accessor.InnerProperty.GetValue(target);
|
var inner = wrapper.Inner;
|
||||||
if (innerValue is null || ReferenceEquals(innerValue, target))
|
if (inner is null || ReferenceEquals(inner, target))
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
target = innerValue;
|
target = inner;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addressSpace = null;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Accessor DiscoverAccessor(Type type)
|
|
||||||
{
|
|
||||||
MethodInfo? allocateAt = null;
|
|
||||||
MethodInfo? allocateAtOrAbove = null;
|
|
||||||
var allocateAtHasAllowAlternativeArg = false;
|
|
||||||
|
|
||||||
foreach (var candidate in type.GetMethods(BindingFlags.Public | BindingFlags.Instance))
|
|
||||||
{
|
|
||||||
var parameters = candidate.GetParameters();
|
|
||||||
if (string.Equals(candidate.Name, "TryAllocateAtOrAbove", StringComparison.Ordinal) &&
|
|
||||||
parameters.Length == 5 &&
|
|
||||||
parameters[0].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[1].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[2].ParameterType == typeof(bool) &&
|
|
||||||
parameters[3].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[4].ParameterType == typeof(ulong).MakeByRefType())
|
|
||||||
{
|
|
||||||
allocateAtOrAbove = candidate;
|
|
||||||
}
|
|
||||||
else if (string.Equals(candidate.Name, "AllocateAt", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
if (parameters.Length == 3 &&
|
|
||||||
parameters[0].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[1].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[2].ParameterType == typeof(bool))
|
|
||||||
{
|
|
||||||
allocateAt = candidate;
|
|
||||||
allocateAtHasAllowAlternativeArg = false;
|
|
||||||
}
|
|
||||||
else if (parameters.Length == 4 &&
|
|
||||||
parameters[0].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[1].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[2].ParameterType == typeof(bool) &&
|
|
||||||
parameters[3].ParameterType == typeof(bool))
|
|
||||||
{
|
|
||||||
allocateAt = candidate;
|
|
||||||
allocateAtHasAllowAlternativeArg = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var innerProperty = type.GetProperty("Inner", BindingFlags.Public | BindingFlags.Instance);
|
|
||||||
return new Accessor(allocateAt, allocateAtOrAbove, allocateAtHasAllowAlternativeArg, innerProperty);
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly record struct Accessor(
|
|
||||||
MethodInfo? AllocateAt,
|
|
||||||
MethodInfo? AllocateAtOrAbove,
|
|
||||||
bool AllocateAtHasAllowAlternativeArg,
|
|
||||||
PropertyInfo? InnerProperty);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,17 @@ namespace SharpEmu.Libs.Mouse;
|
|||||||
|
|
||||||
public static class MouseExports
|
public static class MouseExports
|
||||||
{
|
{
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Qs0wWulgl7U",
|
||||||
|
ExportName = "sceMouseInit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceMouse")]
|
||||||
|
public static int MouseInit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
// Returns 0 read entries: no mouse is connected. This NID was previously misbound
|
// Returns 0 read entries: no mouse is connected. This NID was previously misbound
|
||||||
// as an sceNgs2VoiceGetState alias.
|
// as an sceNgs2VoiceGetState alias.
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
|
|||||||
@@ -146,6 +146,13 @@ public static class NetCtlExports
|
|||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK, typeof(long));
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK, typeof(long));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "1NE9OWdBIww",
|
||||||
|
ExportName = "sceNetCtlRegisterCallbackV6",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNetCtl")]
|
||||||
|
public static int NetCtlRegisterCallbackV6(CpuContext ctx) => NetCtlRegisterCallback(ctx);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "obuxdTiwkF8",
|
Nid = "obuxdTiwkF8",
|
||||||
ExportName = "sceNetCtlGetInfo",
|
ExportName = "sceNetCtlGetInfo",
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Np;
|
||||||
|
|
||||||
|
// Stub for sce::Np::CppWebApi: titles abort PS5-component startup if
|
||||||
|
// Common::initialize returns a negative SCE error, so no-op success is required to boot.
|
||||||
|
public static class NpCppWebApiExports
|
||||||
|
{
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "UYPxv8MIzGo",
|
||||||
|
ExportName = "_ZN3sce2Np9CppWebApi6Common10initializeERKNS2_10InitParamsERNS2_10LibContextE",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpCppWebApi")]
|
||||||
|
public static int CppWebApiCommonInitialize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
// int Common::initialize(const InitParams&, LibContext&) — 0 on success.
|
||||||
|
TraceCppWebApi("common_initialize", ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi]);
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TraceCppWebApi(string operation, ulong arg0, ulong arg1)
|
||||||
|
{
|
||||||
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NP"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] np_cppwebapi.{operation} arg0=0x{arg0:X16} arg1=0x{arg1:X16}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -59,7 +59,7 @@ public static class NpManagerExports
|
|||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "rbknaUjpqWo",
|
Nid = "rbknaUjpqWo",
|
||||||
ExportName = "sceNpGetOnlineIdA",
|
ExportName = "sceNpGetAccountIdA",
|
||||||
Target = Generation.Gen5,
|
Target = Generation.Gen5,
|
||||||
LibraryName = "libSceNpManager")]
|
LibraryName = "libSceNpManager")]
|
||||||
public static int NpGetOnlineIdA(CpuContext ctx)
|
public static int NpGetOnlineIdA(CpuContext ctx)
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ namespace SharpEmu.Libs.Np;
|
|||||||
public static class NpUniversalDataSystemExports
|
public static class NpUniversalDataSystemExports
|
||||||
{
|
{
|
||||||
private const int NpUniversalDataSystemErrorInvalidArgument = unchecked((int)0x80553102);
|
private const int NpUniversalDataSystemErrorInvalidArgument = unchecked((int)0x80553102);
|
||||||
|
private static readonly object _eventGate = new();
|
||||||
|
private static readonly HashSet<int> _createdEvents = [];
|
||||||
private static int _nextHandle = 1;
|
private static int _nextHandle = 1;
|
||||||
|
private static int _nextEvent = 1;
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "sjaobBgqeB4",
|
Nid = "sjaobBgqeB4",
|
||||||
@@ -67,6 +70,114 @@ public static class NpUniversalDataSystemExports
|
|||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "p+GcLqwpL9M",
|
||||||
|
ExportName = "sceNpUniversalDataSystemCreateEvent",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpUniversalDataSystem")]
|
||||||
|
public static int NpUniversalDataSystemCreateEvent(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var parameterAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (parameterAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(NpUniversalDataSystemErrorInvalidArgument, typeof(long));
|
||||||
|
}
|
||||||
|
|
||||||
|
var eventId = Interlocked.Increment(ref _nextEvent);
|
||||||
|
lock (_eventGate)
|
||||||
|
{
|
||||||
|
_createdEvents.Add(eventId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.TryWriteInt32(ctx[CpuRegister.Rdx], eventId, checkNil: true) ||
|
||||||
|
ctx.TryWriteInt32(ctx[CpuRegister.Rcx], eventId, checkNil: true))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(0, typeof(long));
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_eventGate)
|
||||||
|
{
|
||||||
|
_createdEvents.Remove(eventId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "wG+84pnNIuo",
|
||||||
|
ExportName = "sceNpUniversalDataSystemDestroyEvent",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpUniversalDataSystem")]
|
||||||
|
public static int NpUniversalDataSystemDestroyEvent(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var eventId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
lock (_eventGate)
|
||||||
|
{
|
||||||
|
_createdEvents.Remove(eventId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.SetReturn(0, typeof(long));
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "MfDb+4Nln64",
|
||||||
|
ExportName = "sceNpUniversalDataSystemEventPropertyObjectSetString",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpUniversalDataSystem")]
|
||||||
|
public static int NpUniversalDataSystemEventPropertyObjectSetString(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var propertyObjectAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var valueAddress = ctx[CpuRegister.Rdx];
|
||||||
|
if (propertyObjectAddress == 0 || valueAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(NpUniversalDataSystemErrorInvalidArgument, typeof(long));
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> probe = stackalloc byte[1];
|
||||||
|
return ctx.Memory.TryRead(propertyObjectAddress, probe) &&
|
||||||
|
ctx.Memory.TryRead(valueAddress, probe)
|
||||||
|
? ctx.SetReturn(0, typeof(long))
|
||||||
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Wxbg5x3pTXA",
|
||||||
|
ExportName = "sceNpUniversalDataSystemEventPropertyObjectSetArray",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpUniversalDataSystem")]
|
||||||
|
public static int NpUniversalDataSystemEventPropertyObjectSetArray(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var propertyObjectAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var valueAddress = ctx[CpuRegister.Rdx];
|
||||||
|
if (propertyObjectAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(NpUniversalDataSystemErrorInvalidArgument, typeof(long));
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> probe = stackalloc byte[1];
|
||||||
|
if (!ctx.Memory.TryRead(propertyObjectAddress, probe))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueAddress != 0 && !ctx.Memory.TryRead(valueAddress, probe))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.SetReturn(0, typeof(long));
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "CzkKf7ahIyU",
|
||||||
|
ExportName = "sceNpUniversalDataSystemPostEvent",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpUniversalDataSystem")]
|
||||||
|
public static int NpUniversalDataSystemPostEvent(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(0, typeof(long));
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "tpFJ8LIKvPw",
|
Nid = "tpFJ8LIKvPw",
|
||||||
ExportName = "sceNpUniversalDataSystemRegisterContext",
|
ExportName = "sceNpUniversalDataSystemRegisterContext",
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ public static class NpWebApi2Exports
|
|||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "WV1GwM32NgY",
|
Nid = "WV1GwM32NgY",
|
||||||
ExportName = "sceNpWebApi2InitializeForToolkit",
|
ExportName = "sceNpWebApi2PushEventCreateHandle",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libSceNpWebApi2")]
|
LibraryName = "libSceNpWebApi2")]
|
||||||
public static int NpWebApi2InitializeAlt(CpuContext ctx)
|
public static int NpWebApi2InitializeAlt(CpuContext ctx)
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Pad;
|
||||||
|
|
||||||
|
// Minimal libSceBluetoothHid stub: no host Bluetooth passthrough, so report
|
||||||
|
// success and let the Pad path provide input (SHARPEMU_BTHID_UNAVAILABLE=1 fails instead).
|
||||||
|
public static class BluetoothHidExports
|
||||||
|
{
|
||||||
|
private const int BluetoothHidUnavailable = unchecked((int)0x80960001);
|
||||||
|
|
||||||
|
private static readonly bool _reportUnavailable = string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_BTHID_UNAVAILABLE"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
|
||||||
|
private static int Result(CpuContext ctx) =>
|
||||||
|
ctx.SetReturn(_reportUnavailable ? BluetoothHidUnavailable : 0);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "tul3-GzejQc",
|
||||||
|
ExportName = "sceBluetoothHidInit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceBluetoothHid")]
|
||||||
|
public static int BluetoothHidInit(CpuContext ctx) => Result(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "4FUZ+c52d2k",
|
||||||
|
ExportName = "sceBluetoothHidRegisterDevice",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceBluetoothHid")]
|
||||||
|
public static int BluetoothHidRegisterDevice(CpuContext ctx) => Result(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "4Ypfo9RIwfM",
|
||||||
|
ExportName = "sceBluetoothHidRegisterCallback",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceBluetoothHid")]
|
||||||
|
public static int BluetoothHidRegisterCallback(CpuContext ctx) => Result(ctx);
|
||||||
|
}
|
||||||
@@ -48,7 +48,18 @@ public static class PadExports
|
|||||||
ExportName = "scePadOpen",
|
ExportName = "scePadOpen",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libScePad")]
|
LibraryName = "libScePad")]
|
||||||
public static int PadOpen(CpuContext ctx)
|
public static int PadOpen(CpuContext ctx) => PadOpenCore(ctx, extended: false);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "WFIiSfXGUq8",
|
||||||
|
ExportName = "scePadOpenExt",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePad")]
|
||||||
|
public static int PadOpenExt(CpuContext ctx) => PadOpenCore(ctx, extended: true);
|
||||||
|
|
||||||
|
// scePadOpen rejects a non-null 4th arg and non-standard ports; scePadOpenExt accepts a
|
||||||
|
// ScePadOpenExtParam* plus ports 1/2 (racing titles retry scePadOpenExt(type=2) forever if rejected).
|
||||||
|
private static int PadOpenCore(CpuContext ctx, bool extended)
|
||||||
{
|
{
|
||||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
var type = unchecked((int)ctx[CpuRegister.Rsi]);
|
var type = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
@@ -64,7 +75,8 @@ public static class PadExports
|
|||||||
return ctx.SetReturn(OrbisPadErrorDeviceNoHandle);
|
return ctx.SetReturn(OrbisPadErrorDeviceNoHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userId != PrimaryUserId || type != StandardPortType || index != 0 || parameterAddress != 0)
|
var typeAccepted = extended ? type is 0 or 1 or 2 : type == StandardPortType;
|
||||||
|
if (userId != PrimaryUserId || !typeAccepted || index != 0 || (!extended && parameterAddress != 0))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
|
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
|
||||||
}
|
}
|
||||||
@@ -83,6 +95,19 @@ public static class PadExports
|
|||||||
return ctx.SetReturn(PrimaryPadHandle);
|
return ctx.SetReturn(PrimaryPadHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "6ncge5+l5Qs",
|
||||||
|
ExportName = "scePadClose",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePad")]
|
||||||
|
public static int PadClose(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
return handle == PrimaryPadHandle
|
||||||
|
? ctx.SetReturn(0)
|
||||||
|
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "clVvL4ZDntw",
|
Nid = "clVvL4ZDntw",
|
||||||
ExportName = "scePadSetMotionSensorState",
|
ExportName = "scePadSetMotionSensorState",
|
||||||
@@ -131,6 +156,47 @@ public static class PadExports
|
|||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "hGbf2QTBmqc",
|
||||||
|
ExportName = "scePadGetExtControllerInformation",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePad")]
|
||||||
|
public static int PadGetExtControllerInformation(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var informationAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (handle != PrimaryPadHandle)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (informationAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base ScePadControllerInformation + device-class/connection fields: report a connected
|
||||||
|
// DualSense so the guest's open -> get-ext-info -> close probe loop resolves.
|
||||||
|
Span<byte> information = stackalloc byte[0x40];
|
||||||
|
information.Clear();
|
||||||
|
BinaryPrimitives.WriteSingleLittleEndian(information[0x00..], 44.86f);
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(information[0x04..], 1920);
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(information[0x06..], 943);
|
||||||
|
information[0x08] = 30;
|
||||||
|
information[0x09] = 30;
|
||||||
|
information[0x0A] = StandardPortType;
|
||||||
|
information[0x0B] = 1; // connected count
|
||||||
|
information[0x0C] = 1; // connected
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(information[0x10..], 0);
|
||||||
|
information[0x1C] = 0; // deviceClass: 0 = standard controller / DualSense
|
||||||
|
information[0x1D] = 1; // connected (ext)
|
||||||
|
information[0x1E] = 0; // connectionType: local
|
||||||
|
|
||||||
|
return ctx.Memory.TryWrite(informationAddress, information)
|
||||||
|
? ctx.SetReturn(0)
|
||||||
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "YndgXqQVV7c",
|
Nid = "YndgXqQVV7c",
|
||||||
ExportName = "scePadReadState",
|
ExportName = "scePadReadState",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ public static class SaveDataDialogExports
|
|||||||
private const int ErrorArgNull = unchecked((int)0x80B8000D);
|
private const int ErrorArgNull = unchecked((int)0x80B8000D);
|
||||||
|
|
||||||
private const int ResultSize = 0x48;
|
private const int ResultSize = 0x48;
|
||||||
|
private const int ButtonIdAffirmative = 1;
|
||||||
private static int _status;
|
private static int _status;
|
||||||
private static int _lastMode;
|
private static int _lastMode;
|
||||||
private static ulong _lastUserData;
|
private static ulong _lastUserData;
|
||||||
@@ -61,10 +62,10 @@ public static class SaveDataDialogExports
|
|||||||
_lastMode = TryReadInt32(ctx, paramAddress, out var mode) ? mode : 0;
|
_lastMode = TryReadInt32(ctx, paramAddress, out var mode) ? mode : 0;
|
||||||
_lastUserData = ctx.TryReadUInt64(paramAddress + 0xC8, out var userData) ? userData : 0;
|
_lastUserData = ctx.TryReadUInt64(paramAddress + 0xC8, out var userData) ? userData : 0;
|
||||||
|
|
||||||
// There is no host save dialog yet. Complete immediately with OK so
|
// There is no host save dialog yet. Enter RUNNING so the close path sees a live
|
||||||
// guest polling sees a finished dialog instead of spinning forever.
|
// dialog; the guest's next status poll auto-dismisses it (see PollStatus).
|
||||||
Interlocked.Exchange(ref _status, StatusFinished);
|
Interlocked.Exchange(ref _status, StatusRunning);
|
||||||
TraceSaveDataDialog($"open mode={_lastMode} userData=0x{_lastUserData:X16} -> finished");
|
TraceSaveDataDialog($"open mode={_lastMode} userData=0x{_lastUserData:X16} -> running");
|
||||||
return ctx.SetReturn(ErrorOk);
|
return ctx.SetReturn(ErrorOk);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,14 +74,24 @@ public static class SaveDataDialogExports
|
|||||||
ExportName = "sceSaveDataDialogGetStatus",
|
ExportName = "sceSaveDataDialogGetStatus",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libSceSaveDataDialog")]
|
LibraryName = "libSceSaveDataDialog")]
|
||||||
public static int SaveDataDialogGetStatus(CpuContext ctx) => ctx.SetReturn(Volatile.Read(ref _status));
|
public static int SaveDataDialogGetStatus(CpuContext ctx) => ctx.SetReturn(PollStatus());
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "KK3Bdg1RWK0",
|
Nid = "KK3Bdg1RWK0",
|
||||||
ExportName = "sceSaveDataDialogUpdateStatus",
|
ExportName = "sceSaveDataDialogUpdateStatus",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libSceSaveDataDialog")]
|
LibraryName = "libSceSaveDataDialog")]
|
||||||
public static int SaveDataDialogUpdateStatus(CpuContext ctx) => ctx.SetReturn(Volatile.Read(ref _status));
|
public static int SaveDataDialogUpdateStatus(CpuContext ctx) => ctx.SetReturn(PollStatus());
|
||||||
|
|
||||||
|
// With no host UI the dialog cannot wait for user input: the first status poll after
|
||||||
|
// Open observes the dialog as already dismissed. Advancing on both UpdateStatus and
|
||||||
|
// GetStatus keeps every guest polling pattern free of infinite RUNNING loops, while
|
||||||
|
// an Open -> Close sequence with no poll in between still exercises the close path.
|
||||||
|
private static int PollStatus()
|
||||||
|
{
|
||||||
|
Interlocked.CompareExchange(ref _status, StatusFinished, StatusRunning);
|
||||||
|
return Volatile.Read(ref _status);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "en7gNVnh878",
|
Nid = "en7gNVnh878",
|
||||||
@@ -107,11 +118,13 @@ public static class SaveDataDialogExports
|
|||||||
return ctx.SetReturn(ErrorNotFinished);
|
return ctx.SetReturn(ErrorNotFinished);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Report the affirmative button so save prompts take the confirming branch;
|
||||||
|
// buttonId 0 is the "invalid" sentinel and games may treat it as an error.
|
||||||
Span<byte> result = stackalloc byte[ResultSize];
|
Span<byte> result = stackalloc byte[ResultSize];
|
||||||
result.Clear();
|
result.Clear();
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(result[0x00..], _lastMode);
|
BinaryPrimitives.WriteInt32LittleEndian(result[0x00..], _lastMode);
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(result[0x04..], 0);
|
BinaryPrimitives.WriteInt32LittleEndian(result[0x04..], 0);
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(result[0x08..], 0);
|
BinaryPrimitives.WriteInt32LittleEndian(result[0x08..], ButtonIdAffirmative);
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(result[0x20..], _lastUserData);
|
BinaryPrimitives.WriteUInt64LittleEndian(result[0x20..], _lastUserData);
|
||||||
|
|
||||||
if (!ctx.Memory.TryWrite(resultAddress, result))
|
if (!ctx.Memory.TryWrite(resultAddress, result))
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ public static class SaveDataExports
|
|||||||
private const int MountResultSize = 0x40;
|
private const int MountResultSize = 0x40;
|
||||||
private static readonly object _stateGate = new();
|
private static readonly object _stateGate = new();
|
||||||
private static readonly HashSet<int> _transactionResources = [];
|
private static readonly HashSet<int> _transactionResources = [];
|
||||||
|
private static readonly HashSet<int> _preparedTransactionResources = [];
|
||||||
private static string? _titleId;
|
private static string? _titleId;
|
||||||
private static int _nextTransactionResource;
|
private static int _nextTransactionResource;
|
||||||
|
|
||||||
@@ -40,6 +41,7 @@ public static class SaveDataExports
|
|||||||
{
|
{
|
||||||
_titleId = string.IsNullOrWhiteSpace(titleId) ? null : SanitizePathSegment(titleId.Trim());
|
_titleId = string.IsNullOrWhiteSpace(titleId) ? null : SanitizePathSegment(titleId.Trim());
|
||||||
_transactionResources.Clear();
|
_transactionResources.Clear();
|
||||||
|
_preparedTransactionResources.Clear();
|
||||||
_nextTransactionResource = 0;
|
_nextTransactionResource = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,12 +280,104 @@ public static class SaveDataExports
|
|||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
_transactionResources.Remove(resource);
|
_transactionResources.Remove(resource);
|
||||||
|
_preparedTransactionResources.Remove(resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceSaveData($"delete_transaction_resource resource={resource}");
|
TraceSaveData($"delete_transaction_resource resource={resource}");
|
||||||
return ctx.SetReturn(0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "sDCBrmc61XU",
|
||||||
|
ExportName = "sceSaveDataPrepare",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSaveData")]
|
||||||
|
public static int SaveDataPrepare(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var mountPointAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var resource = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
|
if (mountPointAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadFixedAscii(ctx, mountPointAddress, 16, out var mountPoint))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(mountPoint))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
if (resource != 0)
|
||||||
|
{
|
||||||
|
_preparedTransactionResources.Add(resource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceSaveData($"prepare mount_point={mountPoint} resource={resource}");
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "ie7qhZ4X0Cc",
|
||||||
|
ExportName = "sceSaveDataCommit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSaveData")]
|
||||||
|
public static int SaveDataCommit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var commitAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (commitAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
_preparedTransactionResources.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceSaveData($"commit commit=0x{commitAddress:X16}");
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "uW4vfTwMQVo",
|
||||||
|
ExportName = "sceSaveDataUmount2",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSaveData")]
|
||||||
|
public static int SaveDataUmount2(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var mountPointAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (mountPointAddress == 0)
|
||||||
|
{
|
||||||
|
mountPointAddress = ctx[CpuRegister.Rsi];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mountPointAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadFixedAscii(ctx, mountPointAddress, 16, out var mountPoint))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(mountPoint))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
var unmounted = KernelMemoryCompatExports.TryUnregisterGuestPathMount(mountPoint);
|
||||||
|
TraceSaveData($"umount2 mount_point={mountPoint} unregistered={unmounted}");
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryReadSearchCond(CpuContext ctx, ulong address, out SearchCond cond)
|
private static bool TryReadSearchCond(CpuContext ctx, ulong address, out SearchCond cond)
|
||||||
{
|
{
|
||||||
cond = default;
|
cond = default;
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<ProjectReference Include="..\SharpEmu.HLE\SharpEmu.HLE.csproj" />
|
<ProjectReference Include="..\SharpEmu.HLE\SharpEmu.HLE.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="SharpEmu.Libs.Tests" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Silk.NET.Vulkan" />
|
<PackageReference Include="Silk.NET.Vulkan" />
|
||||||
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" />
|
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" />
|
||||||
|
|||||||
@@ -185,6 +185,31 @@ public static class UserServiceExports
|
|||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "woNpu+45RLk",
|
||||||
|
ExportName = "sceUserServiceGetAgeLevel",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceUserService")]
|
||||||
|
public static int UserServiceGetAgeLevel(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var ageLevelAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (userId != 1000)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisUserServiceErrorInvalidParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ageLevelAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report an adult account so titles skip parental-restriction paths.
|
||||||
|
return ctx.TryWriteInt32(ageLevelAddress, 21)
|
||||||
|
? ctx.SetReturn(0)
|
||||||
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
private static void TraceUserService(string message)
|
private static void TraceUserService(string message)
|
||||||
{
|
{
|
||||||
if (_traceUserService)
|
if (_traceUserService)
|
||||||
|
|||||||
@@ -403,6 +403,34 @@ public static class VideoOutExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "+I4K03i3EL0",
|
||||||
|
ExportName = "sceVideoOutInitializeOutputOptions",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceVideoOut")]
|
||||||
|
public static int VideoOutInitializeOutputOptions(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "w0hLuNarQxY",
|
||||||
|
ExportName = "sceVideoOutConfigureOutput",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceVideoOut")]
|
||||||
|
public static int VideoOutConfigureOutput(CpuContext ctx)
|
||||||
|
{
|
||||||
|
// Accept the requested output configuration; the presenter always renders
|
||||||
|
// at the display buffer's native size.
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
if (!TryGetPort(handle, out _))
|
||||||
|
{
|
||||||
|
return OrbisVideoOutErrorInvalidHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "utPrVdxio-8",
|
Nid = "utPrVdxio-8",
|
||||||
ExportName = "sceVideoOutGetOutputStatus",
|
ExportName = "sceVideoOutGetOutputStatus",
|
||||||
|
|||||||
@@ -104,14 +104,17 @@ internal readonly record struct VulkanGuestBlendState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
internal sealed record VulkanGuestRenderState(
|
internal sealed record VulkanGuestRenderState(
|
||||||
VulkanGuestBlendState Blend,
|
IReadOnlyList<VulkanGuestBlendState> Blends,
|
||||||
VulkanGuestRect? Scissor,
|
VulkanGuestRect? Scissor,
|
||||||
VulkanGuestViewport? Viewport)
|
VulkanGuestViewport? Viewport)
|
||||||
{
|
{
|
||||||
public static VulkanGuestRenderState Default { get; } = new(
|
public static VulkanGuestRenderState Default { get; } = new(
|
||||||
VulkanGuestBlendState.Default,
|
[VulkanGuestBlendState.Default],
|
||||||
Scissor: null,
|
Scissor: null,
|
||||||
Viewport: null);
|
Viewport: null);
|
||||||
|
|
||||||
|
public VulkanGuestBlendState Blend =>
|
||||||
|
Blends.Count == 0 ? VulkanGuestBlendState.Default : Blends[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed record VulkanGuestRenderTarget(
|
internal sealed record VulkanGuestRenderTarget(
|
||||||
@@ -122,6 +125,13 @@ internal sealed record VulkanGuestRenderTarget(
|
|||||||
uint NumberType,
|
uint NumberType,
|
||||||
uint MipLevels = 1);
|
uint MipLevels = 1);
|
||||||
|
|
||||||
|
internal readonly record struct VulkanRenderTargetFormat(
|
||||||
|
Format Format,
|
||||||
|
Gen5PixelOutputKind OutputKind)
|
||||||
|
{
|
||||||
|
public bool IsInteger => OutputKind is Gen5PixelOutputKind.Uint or Gen5PixelOutputKind.Sint;
|
||||||
|
}
|
||||||
|
|
||||||
internal sealed record VulkanTranslatedGuestDraw(
|
internal sealed record VulkanTranslatedGuestDraw(
|
||||||
byte[] VertexSpirv,
|
byte[] VertexSpirv,
|
||||||
byte[] PixelSpirv,
|
byte[] PixelSpirv,
|
||||||
@@ -137,7 +147,7 @@ internal sealed record VulkanTranslatedGuestDraw(
|
|||||||
|
|
||||||
internal sealed record VulkanOffscreenGuestDraw(
|
internal sealed record VulkanOffscreenGuestDraw(
|
||||||
VulkanTranslatedGuestDraw Draw,
|
VulkanTranslatedGuestDraw Draw,
|
||||||
VulkanGuestRenderTarget Target,
|
IReadOnlyList<VulkanGuestRenderTarget> Targets,
|
||||||
bool PublishTarget);
|
bool PublishTarget);
|
||||||
|
|
||||||
internal sealed record VulkanComputeGuestDispatch(
|
internal sealed record VulkanComputeGuestDispatch(
|
||||||
@@ -463,20 +473,70 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
VulkanGuestIndexBuffer? indexBuffer = null,
|
VulkanGuestIndexBuffer? indexBuffer = null,
|
||||||
IReadOnlyList<VulkanGuestVertexBuffer>? vertexBuffers = null,
|
IReadOnlyList<VulkanGuestVertexBuffer>? vertexBuffers = null,
|
||||||
VulkanGuestRenderState? renderState = null)
|
VulkanGuestRenderState? renderState = null)
|
||||||
|
{
|
||||||
|
SubmitOffscreenTranslatedDraw(
|
||||||
|
pixelSpirv,
|
||||||
|
textures,
|
||||||
|
globalMemoryBuffers,
|
||||||
|
attributeCount,
|
||||||
|
[target],
|
||||||
|
vertexSpirv,
|
||||||
|
vertexCount,
|
||||||
|
instanceCount,
|
||||||
|
primitiveType,
|
||||||
|
indexBuffer,
|
||||||
|
vertexBuffers,
|
||||||
|
renderState);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SubmitOffscreenTranslatedDraw(
|
||||||
|
byte[] pixelSpirv,
|
||||||
|
IReadOnlyList<VulkanGuestDrawTexture> textures,
|
||||||
|
IReadOnlyList<VulkanGuestMemoryBuffer> globalMemoryBuffers,
|
||||||
|
uint attributeCount,
|
||||||
|
IReadOnlyList<VulkanGuestRenderTarget> targets,
|
||||||
|
byte[]? vertexSpirv = null,
|
||||||
|
uint vertexCount = 3,
|
||||||
|
uint instanceCount = 1,
|
||||||
|
uint primitiveType = 4,
|
||||||
|
VulkanGuestIndexBuffer? indexBuffer = null,
|
||||||
|
IReadOnlyList<VulkanGuestVertexBuffer>? vertexBuffers = null,
|
||||||
|
VulkanGuestRenderState? renderState = null)
|
||||||
{
|
{
|
||||||
if (pixelSpirv.Length == 0 ||
|
if (pixelSpirv.Length == 0 ||
|
||||||
target.Address == 0 ||
|
targets.Count == 0 ||
|
||||||
target.Width == 0 ||
|
targets.Count > 8 ||
|
||||||
target.Height == 0)
|
targets.Any(target =>
|
||||||
|
target.Address == 0 || target.Width == 0 || target.Height == 0))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var firstTarget = targets[0];
|
||||||
|
if (targets.Any(target =>
|
||||||
|
target.Width != firstTarget.Width || target.Height != firstTarget.Height) ||
|
||||||
|
targets.Select(target => target.Address).Distinct().Count() != targets.Count)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][WARN] Vulkan skipped MRT draw with mismatched dimensions or aliased targets.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
|
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][TRACE] vk.submit_call kind=SubmitOffscreenTranslatedDraw " +
|
$"[LOADER][TRACE] vk.submit_call kind=SubmitOffscreenTranslatedDraw " +
|
||||||
$"target=0x{target.Address:X16} {target.Width}x{target.Height} textures={textures.Count}");
|
$"targets={targets.Count} first=0x{firstTarget.Address:X16} " +
|
||||||
|
$"{firstTarget.Width}x{firstTarget.Height} textures={textures.Count}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var effectiveRenderState = renderState ?? VulkanGuestRenderState.Default;
|
||||||
|
if (effectiveRenderState.Blends.Count == 1 && targets.Count > 1)
|
||||||
|
{
|
||||||
|
effectiveRenderState = effectiveRenderState with
|
||||||
|
{
|
||||||
|
Blends = Enumerable.Repeat(effectiveRenderState.Blends[0], targets.Count).ToArray(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
@@ -486,12 +546,15 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var guestTextureFormat = GetGuestTextureFormat(
|
foreach (var target in targets)
|
||||||
target.Format,
|
|
||||||
target.NumberType);
|
|
||||||
if (guestTextureFormat != 0)
|
|
||||||
{
|
{
|
||||||
_availableGuestImages[target.Address] = guestTextureFormat;
|
var guestTextureFormat = GetGuestTextureFormat(
|
||||||
|
target.Format,
|
||||||
|
target.NumberType);
|
||||||
|
if (guestTextureFormat != 0)
|
||||||
|
{
|
||||||
|
_availableGuestImages[target.Address] = guestTextureFormat;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
EnqueueGuestWorkLocked(
|
EnqueueGuestWorkLocked(
|
||||||
@@ -507,8 +570,8 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
instanceCount,
|
instanceCount,
|
||||||
primitiveType,
|
primitiveType,
|
||||||
indexBuffer,
|
indexBuffer,
|
||||||
renderState ?? VulkanGuestRenderState.Default),
|
effectiveRenderState),
|
||||||
target,
|
targets.ToArray(),
|
||||||
PublishTarget: true));
|
PublishTarget: true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -550,12 +613,12 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
4,
|
4,
|
||||||
null,
|
null,
|
||||||
VulkanGuestRenderState.Default),
|
VulkanGuestRenderState.Default),
|
||||||
new VulkanGuestRenderTarget(
|
[new VulkanGuestRenderTarget(
|
||||||
Address: 0,
|
Address: 0,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
Format: 12,
|
Format: 12,
|
||||||
NumberType: 7),
|
NumberType: 7)],
|
||||||
PublishTarget: false));
|
PublishTarget: false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -785,25 +848,100 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static uint GetGuestTextureFormat(uint format, uint numberType) =>
|
private static uint GetGuestTextureFormat(uint format, uint numberType)
|
||||||
(format, numberType) switch
|
{
|
||||||
|
if (!TryDecodeRenderTargetFormat(format, numberType, out var decoded))
|
||||||
{
|
{
|
||||||
(9, _) => 9,
|
return 0;
|
||||||
(4, 4) => GuestFormatR32Uint,
|
}
|
||||||
(4, 5) => GuestFormatR32Sint,
|
|
||||||
(4, 7) => GuestFormatR32Sfloat,
|
return decoded.Format switch
|
||||||
(5, 4) => GuestFormatR16G16Uint,
|
{
|
||||||
(5, 5) => GuestFormatR16G16Sint,
|
Format.R8Unorm => 36,
|
||||||
(5, 7) => GuestFormatR16G16Sfloat,
|
Format.R8Uint => 49,
|
||||||
(10, 4) => GuestFormatR8G8B8A8Uint,
|
Format.R8G8Unorm => 3,
|
||||||
(10, 5) => GuestFormatR8G8B8A8Sint,
|
Format.A2R10G10B10UnormPack32 => 9,
|
||||||
(10, _) => 56,
|
Format.B10G11R11UfloatPack32 => 7,
|
||||||
(12, 4) => GuestFormatR16G16B16A16Uint,
|
Format.R32Uint => GuestFormatR32Uint,
|
||||||
(12, 5) => GuestFormatR16G16B16A16Sint,
|
Format.R32Sint => GuestFormatR32Sint,
|
||||||
(12, 7) => 71,
|
Format.R32Sfloat => GuestFormatR32Sfloat,
|
||||||
(_, 0) when IsKnownGuestTextureFormat(format) => format,
|
Format.R16G16Unorm => 5,
|
||||||
|
Format.R16G16Uint => GuestFormatR16G16Uint,
|
||||||
|
Format.R16G16Sint => GuestFormatR16G16Sint,
|
||||||
|
Format.R16G16Sfloat => GuestFormatR16G16Sfloat,
|
||||||
|
Format.R32G32Sfloat => 75,
|
||||||
|
Format.R8G8B8A8Unorm => 56,
|
||||||
|
Format.R8G8B8A8Uint => GuestFormatR8G8B8A8Uint,
|
||||||
|
Format.R8G8B8A8Sint => GuestFormatR8G8B8A8Sint,
|
||||||
|
Format.R16G16B16A16Unorm => 12,
|
||||||
|
Format.R16G16B16A16Uint => GuestFormatR16G16B16A16Uint,
|
||||||
|
Format.R16G16B16A16Sint => GuestFormatR16G16B16A16Sint,
|
||||||
|
Format.R16G16B16A16Sfloat => 71,
|
||||||
|
Format.R32G32B32A32Sfloat => 14,
|
||||||
_ => 0,
|
_ => 0,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool TryDecodeRenderTargetFormat(
|
||||||
|
uint dataFormat,
|
||||||
|
uint numberType,
|
||||||
|
out VulkanRenderTargetFormat result)
|
||||||
|
{
|
||||||
|
var format = (dataFormat, numberType) switch
|
||||||
|
{
|
||||||
|
(4, 4) => Format.R32Uint,
|
||||||
|
(4, 5) => Format.R32Sint,
|
||||||
|
(4, 7) => Format.R32Sfloat,
|
||||||
|
(5, 4) => Format.R16G16Uint,
|
||||||
|
(5, 5) => Format.R16G16Sint,
|
||||||
|
(5, 7) => Format.R16G16Sfloat,
|
||||||
|
(9, _) => Format.A2R10G10B10UnormPack32,
|
||||||
|
(10, 4) => Format.R8G8B8A8Uint,
|
||||||
|
(10, 5) => Format.R8G8B8A8Sint,
|
||||||
|
(10, _) => Format.R8G8B8A8Unorm,
|
||||||
|
(12, 4) => Format.R16G16B16A16Uint,
|
||||||
|
(12, 5) => Format.R16G16B16A16Sint,
|
||||||
|
(12, 7) => Format.R16G16B16A16Sfloat,
|
||||||
|
(GuestFormatR32Uint, _) or (20, 0) => Format.R32Uint,
|
||||||
|
(GuestFormatR32Sint, _) => Format.R32Sint,
|
||||||
|
(GuestFormatR32Sfloat, _) or (29, 0) or (4, 0) => Format.R32Sfloat,
|
||||||
|
(GuestFormatR16G16Uint, _) => Format.R16G16Uint,
|
||||||
|
(GuestFormatR16G16Sint, _) => Format.R16G16Sint,
|
||||||
|
(GuestFormatR16G16Sfloat, _) => Format.R16G16Sfloat,
|
||||||
|
(GuestFormatR8G8B8A8Uint, _) => Format.R8G8B8A8Uint,
|
||||||
|
(GuestFormatR8G8B8A8Sint, _) => Format.R8G8B8A8Sint,
|
||||||
|
(GuestFormatR16G16B16A16Uint, _) => Format.R16G16B16A16Uint,
|
||||||
|
(GuestFormatR16G16B16A16Sint, _) => Format.R16G16B16A16Sint,
|
||||||
|
(1, 0) or (36, 0) => Format.R8Unorm,
|
||||||
|
(49, 0) => Format.R8Uint,
|
||||||
|
(3, 0) => Format.R8G8Unorm,
|
||||||
|
(5, 0) => Format.R16G16Unorm,
|
||||||
|
(7, 0) => Format.B10G11R11UfloatPack32,
|
||||||
|
(12, 0) => Format.R16G16B16A16Unorm,
|
||||||
|
(13, 0) or (14, 0) => Format.R32G32B32A32Sfloat,
|
||||||
|
(22, 0) or (71, 0) => Format.R16G16B16A16Sfloat,
|
||||||
|
(56, 0) or (62, 0) or (64, 0) => Format.R8G8B8A8Unorm,
|
||||||
|
(75, 0) => Format.R32G32Sfloat,
|
||||||
|
_ => Format.Undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (format == Format.Undefined)
|
||||||
|
{
|
||||||
|
result = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var outputKind = format switch
|
||||||
|
{
|
||||||
|
Format.R8Uint or Format.R32Uint or Format.R16G16Uint or
|
||||||
|
Format.R8G8B8A8Uint or Format.R16G16B16A16Uint => Gen5PixelOutputKind.Uint,
|
||||||
|
Format.R32Sint or Format.R16G16Sint or Format.R8G8B8A8Sint or
|
||||||
|
Format.R16G16B16A16Sint => Gen5PixelOutputKind.Sint,
|
||||||
|
_ => Gen5PixelOutputKind.Float,
|
||||||
|
};
|
||||||
|
result = new VulkanRenderTargetFormat(format, outputKind);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsKnownGuestTextureFormat(uint format) =>
|
private static bool IsKnownGuestTextureFormat(uint format) =>
|
||||||
format is 4 or 5 or 7 or 9 or 13 or 14 or 22 or 29 or 36 or 56 or 62 or 64 or 71;
|
format is 4 or 5 or 7 or 9 or 13 or 14 or 22 or 29 or 36 or 56 or 62 or 64 or 71;
|
||||||
@@ -948,6 +1086,8 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
private DebugUtilsMessengerEXT _debugMessenger;
|
private DebugUtilsMessengerEXT _debugMessenger;
|
||||||
private ExtDebugUtils? _debugUtils;
|
private ExtDebugUtils? _debugUtils;
|
||||||
private PhysicalDevice _physicalDevice;
|
private PhysicalDevice _physicalDevice;
|
||||||
|
private bool _supportsIndependentBlend;
|
||||||
|
private uint _maxColorAttachments;
|
||||||
private Device _device;
|
private Device _device;
|
||||||
private PipelineCache _pipelineCache;
|
private PipelineCache _pipelineCache;
|
||||||
private string? _pipelineCachePath;
|
private string? _pipelineCachePath;
|
||||||
@@ -1024,9 +1164,9 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
private readonly record struct GraphicsPipelineKey(
|
private readonly record struct GraphicsPipelineKey(
|
||||||
string VertexShader,
|
string VertexShader,
|
||||||
string FragmentShader,
|
string FragmentShader,
|
||||||
Format RenderTargetFormat,
|
string RenderTargetLayout,
|
||||||
PrimitiveTopology Topology,
|
PrimitiveTopology Topology,
|
||||||
VulkanGuestBlendState Blend,
|
string BlendLayout,
|
||||||
string ResourceLayout,
|
string ResourceLayout,
|
||||||
string VertexLayout);
|
string VertexLayout);
|
||||||
|
|
||||||
@@ -1066,9 +1206,11 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
public uint VertexCount = 3;
|
public uint VertexCount = 3;
|
||||||
public uint InstanceCount = 1;
|
public uint InstanceCount = 1;
|
||||||
public PrimitiveTopology Topology = PrimitiveTopology.TriangleList;
|
public PrimitiveTopology Topology = PrimitiveTopology.TriangleList;
|
||||||
public VulkanGuestBlendState Blend = VulkanGuestBlendState.Default;
|
public VulkanGuestBlendState[] Blends = [VulkanGuestBlendState.Default];
|
||||||
public VulkanGuestRect? Scissor;
|
public VulkanGuestRect? Scissor;
|
||||||
public VulkanGuestViewport? Viewport;
|
public VulkanGuestViewport? Viewport;
|
||||||
|
public RenderPass TransientRenderPass;
|
||||||
|
public Framebuffer TransientFramebuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class TextureResource
|
private sealed class TextureResource
|
||||||
@@ -1604,6 +1746,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
|
|
||||||
_vk.GetPhysicalDeviceProperties(_physicalDevice, out var selected);
|
_vk.GetPhysicalDeviceProperties(_physicalDevice, out var selected);
|
||||||
|
_maxColorAttachments = selected.Limits.MaxColorAttachments;
|
||||||
var selectedName = SilkMarshal.PtrToString((nint)selected.DeviceName) ?? "unknown";
|
var selectedName = SilkMarshal.PtrToString((nint)selected.DeviceName) ?? "unknown";
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][INFO] Vulkan device: {selectedName} ({selected.DeviceType})");
|
$"[LOADER][INFO] Vulkan device: {selectedName} ({selected.DeviceType})");
|
||||||
@@ -1650,8 +1793,10 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
PQueuePriorities = &priority,
|
PQueuePriorities = &priority,
|
||||||
};
|
};
|
||||||
_vk.GetPhysicalDeviceFeatures(_physicalDevice, out var supportedFeatures);
|
_vk.GetPhysicalDeviceFeatures(_physicalDevice, out var supportedFeatures);
|
||||||
|
_supportsIndependentBlend = supportedFeatures.IndependentBlend;
|
||||||
var enabledFeatures = new PhysicalDeviceFeatures
|
var enabledFeatures = new PhysicalDeviceFeatures
|
||||||
{
|
{
|
||||||
|
IndependentBlend = supportedFeatures.IndependentBlend,
|
||||||
VertexPipelineStoresAndAtomics = supportedFeatures.VertexPipelineStoresAndAtomics,
|
VertexPipelineStoresAndAtomics = supportedFeatures.VertexPipelineStoresAndAtomics,
|
||||||
FragmentStoresAndAtomics = supportedFeatures.FragmentStoresAndAtomics,
|
FragmentStoresAndAtomics = supportedFeatures.FragmentStoresAndAtomics,
|
||||||
ShaderInt64 = supportedFeatures.ShaderInt64,
|
ShaderInt64 = supportedFeatures.ShaderInt64,
|
||||||
@@ -2150,6 +2295,14 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void WaitForAllGuestSubmissions()
|
||||||
|
{
|
||||||
|
while (_pendingGuestSubmissions.Count != 0)
|
||||||
|
{
|
||||||
|
CollectCompletedGuestSubmissions(waitForOldest: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void CollectCompletedGuestSubmissions(bool waitForOldest)
|
private void CollectCompletedGuestSubmissions(bool waitForOldest)
|
||||||
{
|
{
|
||||||
if (waitForOldest && _pendingGuestSubmissions.TryPeek(out var oldest))
|
if (waitForOldest && _pendingGuestSubmissions.TryPeek(out var oldest))
|
||||||
@@ -2193,13 +2346,15 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
private IReadOnlyList<GuestImageResource> GetTraceImages(
|
private IReadOnlyList<GuestImageResource> GetTraceImages(
|
||||||
TranslatedDrawResources resources,
|
TranslatedDrawResources resources,
|
||||||
GuestImageResource? renderTarget = null)
|
IReadOnlyList<GuestImageResource>? renderTargets = null)
|
||||||
{
|
{
|
||||||
var images = new HashSet<GuestImageResource>();
|
var images = new HashSet<GuestImageResource>();
|
||||||
if (renderTarget is not null &&
|
foreach (var renderTarget in renderTargets ?? [])
|
||||||
ShouldTraceGuestImageContents(renderTarget))
|
|
||||||
{
|
{
|
||||||
images.Add(renderTarget);
|
if (ShouldTraceGuestImageContents(renderTarget))
|
||||||
|
{
|
||||||
|
images.Add(renderTarget);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var texture in resources.Textures)
|
foreach (var texture in resources.Textures)
|
||||||
@@ -2430,10 +2585,16 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
private TranslatedDrawResources CreateTranslatedDrawResources(
|
private TranslatedDrawResources CreateTranslatedDrawResources(
|
||||||
VulkanTranslatedGuestDraw draw,
|
VulkanTranslatedGuestDraw draw,
|
||||||
RenderPass renderPass,
|
RenderPass renderPass,
|
||||||
Format renderTargetFormat,
|
IReadOnlyList<Format> renderTargetFormats,
|
||||||
Extent2D extent)
|
Extent2D extent)
|
||||||
{
|
{
|
||||||
var vertexSpirv = draw.VertexSpirv;
|
var vertexSpirv = draw.VertexSpirv;
|
||||||
|
if (draw.RenderState.Blends.Count != renderTargetFormats.Count)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"color attachment formats and blend states must have matching counts");
|
||||||
|
}
|
||||||
|
|
||||||
if (vertexSpirv.Length == 0 &&
|
if (vertexSpirv.Length == 0 &&
|
||||||
!TryCompileFullscreenVertexShader(
|
!TryCompileFullscreenVertexShader(
|
||||||
draw.AttributeCount,
|
draw.AttributeCount,
|
||||||
@@ -2453,7 +2614,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
VertexCount = GetDrawVertexCount(draw.PrimitiveType, draw.VertexCount, draw.IndexBuffer),
|
VertexCount = GetDrawVertexCount(draw.PrimitiveType, draw.VertexCount, draw.IndexBuffer),
|
||||||
InstanceCount = Math.Max(draw.InstanceCount, 1),
|
InstanceCount = Math.Max(draw.InstanceCount, 1),
|
||||||
Topology = GetPrimitiveTopology(draw.PrimitiveType),
|
Topology = GetPrimitiveTopology(draw.PrimitiveType),
|
||||||
Blend = draw.RenderState.Blend,
|
Blends = draw.RenderState.Blends.ToArray(),
|
||||||
Scissor = draw.RenderState.Scissor,
|
Scissor = draw.RenderState.Scissor,
|
||||||
Viewport = draw.RenderState.Viewport,
|
Viewport = draw.RenderState.Viewport,
|
||||||
};
|
};
|
||||||
@@ -2502,7 +2663,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
vertexSpirv,
|
vertexSpirv,
|
||||||
draw.PixelSpirv,
|
draw.PixelSpirv,
|
||||||
renderPass,
|
renderPass,
|
||||||
renderTargetFormat,
|
renderTargetFormats,
|
||||||
extent);
|
extent);
|
||||||
return resources;
|
return resources;
|
||||||
}
|
}
|
||||||
@@ -2799,15 +2960,18 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
byte[] vertexSpirv,
|
byte[] vertexSpirv,
|
||||||
byte[] fragmentSpirv,
|
byte[] fragmentSpirv,
|
||||||
RenderPass renderPass,
|
RenderPass renderPass,
|
||||||
Format renderTargetFormat,
|
IReadOnlyList<Format> renderTargetFormats,
|
||||||
Extent2D extent)
|
Extent2D extent)
|
||||||
{
|
{
|
||||||
var pipelineKey = new GraphicsPipelineKey(
|
var pipelineKey = new GraphicsPipelineKey(
|
||||||
GetShaderDigest(vertexSpirv),
|
GetShaderDigest(vertexSpirv),
|
||||||
GetShaderDigest(fragmentSpirv),
|
GetShaderDigest(fragmentSpirv),
|
||||||
renderTargetFormat,
|
string.Join(',', renderTargetFormats.Select(format => (uint)format)),
|
||||||
resources.Topology,
|
resources.Topology,
|
||||||
resources.Blend,
|
string.Join(';', resources.Blends.Select(blend =>
|
||||||
|
$"{(blend.Enable ? 1 : 0)}:{blend.ColorSrcFactor}:{blend.ColorDstFactor}:" +
|
||||||
|
$"{blend.ColorFunc}:{blend.AlphaSrcFactor}:{blend.AlphaDstFactor}:" +
|
||||||
|
$"{blend.AlphaFunc}:{(blend.SeparateAlphaBlend ? 1 : 0)}:{blend.WriteMask}")),
|
||||||
GetResourceLayoutKey(resources),
|
GetResourceLayoutKey(resources),
|
||||||
GetVertexLayoutKey(resources));
|
GetVertexLayoutKey(resources));
|
||||||
if (_graphicsPipelines.TryGetValue(pipelineKey, out var cachedPipeline))
|
if (_graphicsPipelines.TryGetValue(pipelineKey, out var cachedPipeline))
|
||||||
@@ -2908,29 +3072,33 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
SType = StructureType.PipelineMultisampleStateCreateInfo,
|
SType = StructureType.PipelineMultisampleStateCreateInfo,
|
||||||
RasterizationSamples = SampleCountFlags.Count1Bit,
|
RasterizationSamples = SampleCountFlags.Count1Bit,
|
||||||
};
|
};
|
||||||
var colorBlendAttachment = new PipelineColorBlendAttachmentState
|
var colorBlendAttachments = stackalloc PipelineColorBlendAttachmentState[resources.Blends.Length];
|
||||||
|
for (var index = 0; index < resources.Blends.Length; index++)
|
||||||
{
|
{
|
||||||
BlendEnable = resources.Blend.Enable,
|
var blend = resources.Blends[index];
|
||||||
SrcColorBlendFactor = ToVkBlendFactor(resources.Blend.ColorSrcFactor),
|
colorBlendAttachments[index] = new PipelineColorBlendAttachmentState
|
||||||
DstColorBlendFactor = ToVkBlendFactor(resources.Blend.ColorDstFactor),
|
{
|
||||||
ColorBlendOp = ToVkBlendOp(resources.Blend.ColorFunc),
|
BlendEnable = blend.Enable,
|
||||||
SrcAlphaBlendFactor = resources.Blend.SeparateAlphaBlend
|
SrcColorBlendFactor = ToVkBlendFactor(blend.ColorSrcFactor),
|
||||||
? ToVkBlendFactor(resources.Blend.AlphaSrcFactor)
|
DstColorBlendFactor = ToVkBlendFactor(blend.ColorDstFactor),
|
||||||
: ToVkBlendFactor(resources.Blend.ColorSrcFactor),
|
ColorBlendOp = ToVkBlendOp(blend.ColorFunc),
|
||||||
DstAlphaBlendFactor = resources.Blend.SeparateAlphaBlend
|
SrcAlphaBlendFactor = blend.SeparateAlphaBlend
|
||||||
? ToVkBlendFactor(resources.Blend.AlphaDstFactor)
|
? ToVkBlendFactor(blend.AlphaSrcFactor)
|
||||||
: ToVkBlendFactor(resources.Blend.ColorDstFactor),
|
: ToVkBlendFactor(blend.ColorSrcFactor),
|
||||||
AlphaBlendOp = resources.Blend.SeparateAlphaBlend
|
DstAlphaBlendFactor = blend.SeparateAlphaBlend
|
||||||
? ToVkBlendOp(resources.Blend.AlphaFunc)
|
? ToVkBlendFactor(blend.AlphaDstFactor)
|
||||||
: ToVkBlendOp(resources.Blend.ColorFunc),
|
: ToVkBlendFactor(blend.ColorDstFactor),
|
||||||
ColorWriteMask =
|
AlphaBlendOp = blend.SeparateAlphaBlend
|
||||||
ToVkColorWriteMask(resources.Blend.WriteMask),
|
? ToVkBlendOp(blend.AlphaFunc)
|
||||||
};
|
: ToVkBlendOp(blend.ColorFunc),
|
||||||
|
ColorWriteMask = ToVkColorWriteMask(blend.WriteMask),
|
||||||
|
};
|
||||||
|
}
|
||||||
var colorBlend = new PipelineColorBlendStateCreateInfo
|
var colorBlend = new PipelineColorBlendStateCreateInfo
|
||||||
{
|
{
|
||||||
SType = StructureType.PipelineColorBlendStateCreateInfo,
|
SType = StructureType.PipelineColorBlendStateCreateInfo,
|
||||||
AttachmentCount = 1,
|
AttachmentCount = (uint)resources.Blends.Length,
|
||||||
PAttachments = &colorBlendAttachment,
|
PAttachments = colorBlendAttachments,
|
||||||
};
|
};
|
||||||
var dynamicStateValues = stackalloc DynamicState[2];
|
var dynamicStateValues = stackalloc DynamicState[2];
|
||||||
dynamicStateValues[0] = DynamicState.Viewport;
|
dynamicStateValues[0] = DynamicState.Viewport;
|
||||||
@@ -4372,6 +4540,12 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
: checked(((ulong)width + 3) / 4 * (((ulong)height + 3) / 4) * blockBytes);
|
: checked(((ulong)width + 3) / 4 * (((ulong)height + 3) / 4) * blockBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool SupportsColorAttachment(Format format)
|
||||||
|
{
|
||||||
|
_vk.GetPhysicalDeviceFormatProperties(_physicalDevice, format, out var properties);
|
||||||
|
return (properties.OptimalTilingFeatures & FormatFeatureFlags.ColorAttachmentBit) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
private static Format GetTextureFormat(uint format, uint numberType) =>
|
private static Format GetTextureFormat(uint format, uint numberType) =>
|
||||||
(format, numberType) switch
|
(format, numberType) switch
|
||||||
{
|
{
|
||||||
@@ -4434,26 +4608,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
_ => Format.R8G8B8A8Unorm,
|
_ => Format.R8G8B8A8Unorm,
|
||||||
};
|
};
|
||||||
|
|
||||||
private static Format GetRenderTargetFormat(uint format, uint numberType) =>
|
|
||||||
(format, numberType) switch
|
|
||||||
{
|
|
||||||
(4, 4) => Format.R32Uint,
|
|
||||||
(4, 5) => Format.R32Sint,
|
|
||||||
(4, 7) => Format.R32Sfloat,
|
|
||||||
(5, 4) => Format.R16G16Uint,
|
|
||||||
(5, 5) => Format.R16G16Sint,
|
|
||||||
(5, 7) => Format.R16G16Sfloat,
|
|
||||||
(9, _) => Format.A2R10G10B10UnormPack32,
|
|
||||||
(10, 4) => Format.R8G8B8A8Uint,
|
|
||||||
(10, 5) => Format.R8G8B8A8Sint,
|
|
||||||
(10, _) => Format.R8G8B8A8Unorm,
|
|
||||||
(12, 4) => Format.R16G16B16A16Uint,
|
|
||||||
(12, 5) => Format.R16G16B16A16Sint,
|
|
||||||
(12, 7) => Format.R16G16B16A16Sfloat,
|
|
||||||
(_, 0) => GetTextureFormat(format, numberType),
|
|
||||||
_ => Format.Undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
private static bool IsBlockCompressedFormat(Format format) =>
|
private static bool IsBlockCompressedFormat(Format format) =>
|
||||||
format is Format.BC1RgbaUnormBlock or
|
format is Format.BC1RgbaUnormBlock or
|
||||||
Format.BC1RgbaSrgbBlock or
|
Format.BC1RgbaSrgbBlock or
|
||||||
@@ -4694,47 +4848,114 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
private void ExecuteOffscreenDraw(VulkanOffscreenGuestDraw work)
|
private void ExecuteOffscreenDraw(VulkanOffscreenGuestDraw work)
|
||||||
{
|
{
|
||||||
if (_deviceLost)
|
if (_deviceLost || work.Targets.Count == 0)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var format = GetRenderTargetFormat(work.Target.Format, work.Target.NumberType);
|
if (work.Targets.Count > _maxColorAttachments)
|
||||||
if (format == Format.Undefined)
|
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][WARN] Vulkan skipped unsupported render target " +
|
$"[LOADER][WARN] Vulkan skipped MRT draw requesting {work.Targets.Count} color attachments; " +
|
||||||
$"addr=0x{work.Target.Address:X16} format={work.Target.Format} " +
|
$"the selected device supports {_maxColorAttachments}.");
|
||||||
$"number={work.Target.NumberType}");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var targetFormats = new VulkanRenderTargetFormat[work.Targets.Count];
|
||||||
|
for (var index = 0; index < targetFormats.Length; index++)
|
||||||
|
{
|
||||||
|
var target = work.Targets[index];
|
||||||
|
if (!TryDecodeRenderTargetFormat(target.Format, target.NumberType, out targetFormats[index]) ||
|
||||||
|
!SupportsColorAttachment(targetFormats[index].Format))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] Vulkan skipped MRT draw with unsupported color target " +
|
||||||
|
$"format={target.Format} number_type={target.NumberType}.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (work.Draw.RenderState.Blends.Count != targetFormats.Length)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][WARN] Vulkan skipped MRT draw with mismatched attachment/blend counts.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetFormats
|
||||||
|
.Select((format, index) => format.IsInteger && work.Draw.RenderState.Blends[index].Enable)
|
||||||
|
.Any(invalid => invalid))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][WARN] Vulkan skipped MRT draw with blending enabled for an integer attachment.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_supportsIndependentBlend &&
|
||||||
|
work.Draw.RenderState.Blends.Skip(1).Any(blend => blend != work.Draw.RenderState.Blends[0]))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][WARN] Vulkan skipped MRT draw requiring unsupported independentBlend.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var formats = targetFormats.Select(target => target.Format).ToArray();
|
||||||
|
|
||||||
|
var targetAddresses = work.Targets
|
||||||
|
.Where(target => target.Address != 0)
|
||||||
|
.Select(target => target.Address)
|
||||||
|
.ToHashSet();
|
||||||
if (work.Draw.Textures.Any(texture =>
|
if (work.Draw.Textures.Any(texture =>
|
||||||
texture.Address == work.Target.Address &&
|
targetAddresses.Contains(texture.Address)))
|
||||||
texture.Address != 0))
|
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][WARN] Vulkan skipped render-target feedback loop " +
|
$"[LOADER][WARN] Vulkan skipped render-target feedback loop " +
|
||||||
$"addr=0x{work.Target.Address:X16}");
|
$"targets={string.Join(',', targetAddresses.Select(address => $"0x{address:X16}"))}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var target = GetOrCreateGuestImage(work.Target, format);
|
var targets = new GuestImageResource[work.Targets.Count];
|
||||||
|
EnsureGuestSubmissionCapacity();
|
||||||
|
for (var index = 0; index < targets.Length; index++)
|
||||||
|
{
|
||||||
|
targets[index] = GetOrCreateGuestImage(work.Targets[index], formats[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
var firstTarget = targets[0];
|
||||||
TranslatedDrawResources? resources = null;
|
TranslatedDrawResources? resources = null;
|
||||||
CommandBuffer commandBuffer = default;
|
CommandBuffer commandBuffer = default;
|
||||||
var submitted = false;
|
var submitted = false;
|
||||||
|
RenderPass transientRenderPass = default;
|
||||||
|
Framebuffer transientFramebuffer = default;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
EnsureGuestSubmissionCapacity();
|
var extent = new Extent2D(firstTarget.Width, firstTarget.Height);
|
||||||
var extent = new Extent2D(target.Width, target.Height);
|
var renderPass = firstTarget.RenderPass;
|
||||||
|
var framebuffer = firstTarget.Framebuffer;
|
||||||
|
if (targets.Length > 1)
|
||||||
|
{
|
||||||
|
(renderPass, framebuffer) = CreateRenderPassAndFramebuffer(
|
||||||
|
formats,
|
||||||
|
targets.Select(target => target.MipViews[0]).ToArray(),
|
||||||
|
firstTarget.Width,
|
||||||
|
firstTarget.Height);
|
||||||
|
transientRenderPass = renderPass;
|
||||||
|
transientFramebuffer = framebuffer;
|
||||||
|
}
|
||||||
|
|
||||||
resources = CreateTranslatedDrawResources(
|
resources = CreateTranslatedDrawResources(
|
||||||
work.Draw,
|
work.Draw,
|
||||||
target.RenderPass,
|
renderPass,
|
||||||
target.Format,
|
formats,
|
||||||
extent);
|
extent);
|
||||||
|
resources.TransientRenderPass = transientRenderPass;
|
||||||
|
resources.TransientFramebuffer = transientFramebuffer;
|
||||||
|
transientRenderPass = default;
|
||||||
|
transientFramebuffer = default;
|
||||||
resources.DebugName =
|
resources.DebugName =
|
||||||
$"SharpEmu offscreen rt=0x{work.Target.Address:X16} " +
|
$"SharpEmu offscreen mrt={targets.Length} " +
|
||||||
$"{work.Target.Width}x{work.Target.Height} fmt{work.Target.Format}";
|
$"first=0x{work.Targets[0].Address:X16} " +
|
||||||
|
$"{firstTarget.Width}x{firstTarget.Height}";
|
||||||
|
|
||||||
commandBuffer = AllocateGuestCommandBuffer();
|
commandBuffer = AllocateGuestCommandBuffer();
|
||||||
_commandBuffer = commandBuffer;
|
_commandBuffer = commandBuffer;
|
||||||
@@ -4751,24 +4972,30 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
RecordTextureUploads(resources, PipelineStageFlags.FragmentShaderBit);
|
RecordTextureUploads(resources, PipelineStageFlags.FragmentShaderBit);
|
||||||
RecordStorageImagesForWrite(resources, PipelineStageFlags.FragmentShaderBit);
|
RecordStorageImagesForWrite(resources, PipelineStageFlags.FragmentShaderBit);
|
||||||
|
|
||||||
var targetHasPriorContents = target.Initialized || target.InitialUploadPending;
|
var toColorAttachments = stackalloc ImageMemoryBarrier[targets.Length];
|
||||||
var toColorAttachment = new ImageMemoryBarrier
|
var anyPriorContents = false;
|
||||||
|
for (var index = 0; index < targets.Length; index++)
|
||||||
{
|
{
|
||||||
SType = StructureType.ImageMemoryBarrier,
|
var hasPriorContents = targets[index].Initialized || targets[index].InitialUploadPending;
|
||||||
SrcAccessMask = targetHasPriorContents ? AccessFlags.ShaderReadBit : 0,
|
anyPriorContents |= hasPriorContents;
|
||||||
DstAccessMask = AccessFlags.ColorAttachmentWriteBit,
|
toColorAttachments[index] = new ImageMemoryBarrier
|
||||||
OldLayout = targetHasPriorContents
|
{
|
||||||
? ImageLayout.ShaderReadOnlyOptimal
|
SType = StructureType.ImageMemoryBarrier,
|
||||||
: ImageLayout.Undefined,
|
SrcAccessMask = hasPriorContents ? AccessFlags.ShaderReadBit : 0,
|
||||||
NewLayout = ImageLayout.ColorAttachmentOptimal,
|
DstAccessMask = AccessFlags.ColorAttachmentWriteBit,
|
||||||
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
OldLayout = hasPriorContents
|
||||||
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
? ImageLayout.ShaderReadOnlyOptimal
|
||||||
Image = target.Image,
|
: ImageLayout.Undefined,
|
||||||
SubresourceRange = ColorSubresourceRange(),
|
NewLayout = ImageLayout.ColorAttachmentOptimal,
|
||||||
};
|
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||||
|
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||||
|
Image = targets[index].Image,
|
||||||
|
SubresourceRange = ColorSubresourceRange(),
|
||||||
|
};
|
||||||
|
}
|
||||||
_vk.CmdPipelineBarrier(
|
_vk.CmdPipelineBarrier(
|
||||||
_commandBuffer,
|
_commandBuffer,
|
||||||
targetHasPriorContents
|
anyPriorContents
|
||||||
? PipelineStageFlags.AllCommandsBit
|
? PipelineStageFlags.AllCommandsBit
|
||||||
: PipelineStageFlags.TopOfPipeBit,
|
: PipelineStageFlags.TopOfPipeBit,
|
||||||
PipelineStageFlags.ColorAttachmentOutputBit,
|
PipelineStageFlags.ColorAttachmentOutputBit,
|
||||||
@@ -4777,28 +5004,32 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
null,
|
null,
|
||||||
0,
|
0,
|
||||||
null,
|
null,
|
||||||
1,
|
(uint)targets.Length,
|
||||||
&toColorAttachment);
|
toColorAttachments);
|
||||||
|
|
||||||
RecordTranslatedGraphicsPass(
|
RecordTranslatedGraphicsPass(
|
||||||
resources,
|
resources,
|
||||||
target.RenderPass,
|
renderPass,
|
||||||
target.Framebuffer,
|
framebuffer,
|
||||||
extent);
|
extent);
|
||||||
RecordStorageImagesForRead(resources, PipelineStageFlags.FragmentShaderBit);
|
RecordStorageImagesForRead(resources, PipelineStageFlags.FragmentShaderBit);
|
||||||
|
|
||||||
var toShaderRead = new ImageMemoryBarrier
|
var toShaderRead = stackalloc ImageMemoryBarrier[targets.Length];
|
||||||
|
for (var index = 0; index < targets.Length; index++)
|
||||||
{
|
{
|
||||||
SType = StructureType.ImageMemoryBarrier,
|
toShaderRead[index] = new ImageMemoryBarrier
|
||||||
SrcAccessMask = AccessFlags.ColorAttachmentWriteBit,
|
{
|
||||||
DstAccessMask = AccessFlags.ShaderReadBit,
|
SType = StructureType.ImageMemoryBarrier,
|
||||||
OldLayout = ImageLayout.ColorAttachmentOptimal,
|
SrcAccessMask = AccessFlags.ColorAttachmentWriteBit,
|
||||||
NewLayout = ImageLayout.ShaderReadOnlyOptimal,
|
DstAccessMask = AccessFlags.ShaderReadBit,
|
||||||
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
OldLayout = ImageLayout.ColorAttachmentOptimal,
|
||||||
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
NewLayout = ImageLayout.ShaderReadOnlyOptimal,
|
||||||
Image = target.Image,
|
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||||
SubresourceRange = ColorSubresourceRange(),
|
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||||
};
|
Image = targets[index].Image,
|
||||||
|
SubresourceRange = ColorSubresourceRange(),
|
||||||
|
};
|
||||||
|
}
|
||||||
_vk.CmdPipelineBarrier(
|
_vk.CmdPipelineBarrier(
|
||||||
_commandBuffer,
|
_commandBuffer,
|
||||||
PipelineStageFlags.ColorAttachmentOutputBit,
|
PipelineStageFlags.ColorAttachmentOutputBit,
|
||||||
@@ -4808,55 +5039,70 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
null,
|
null,
|
||||||
0,
|
0,
|
||||||
null,
|
null,
|
||||||
1,
|
(uint)targets.Length,
|
||||||
&toShaderRead);
|
toShaderRead);
|
||||||
EndDebugLabel(_commandBuffer);
|
EndDebugLabel(_commandBuffer);
|
||||||
|
|
||||||
Check(_vk.EndCommandBuffer(_commandBuffer), "vkEndCommandBuffer(offscreen)");
|
Check(_vk.EndCommandBuffer(_commandBuffer), "vkEndCommandBuffer(offscreen)");
|
||||||
SubmitGuestCommandBuffer(
|
SubmitGuestCommandBuffer(
|
||||||
commandBuffer,
|
commandBuffer,
|
||||||
resources,
|
resources,
|
||||||
GetTraceImages(resources, target));
|
GetTraceImages(resources, targets));
|
||||||
submitted = true;
|
submitted = true;
|
||||||
target.Initialized = true;
|
foreach (var target in targets)
|
||||||
|
{
|
||||||
|
target.Initialized = true;
|
||||||
|
}
|
||||||
MarkSampledImagesInitialized(resources);
|
MarkSampledImagesInitialized(resources);
|
||||||
MarkStorageImagesInitialized(resources, traceContents: false);
|
MarkStorageImagesInitialized(resources, traceContents: false);
|
||||||
|
|
||||||
var guestTextureFormat = VulkanVideoPresenter.GetGuestTextureFormat(
|
if (work.PublishTarget)
|
||||||
work.Target.Format,
|
|
||||||
work.Target.NumberType);
|
|
||||||
if (work.PublishTarget && guestTextureFormat != 0)
|
|
||||||
{
|
{
|
||||||
lock (_gate)
|
for (var index = 0; index < targets.Length; index++)
|
||||||
{
|
{
|
||||||
_availableGuestImages[target.Address] = guestTextureFormat;
|
var guestTextureFormat = VulkanVideoPresenter.GetGuestTextureFormat(
|
||||||
_gpuGuestImages[target.Address] = guestTextureFormat;
|
work.Targets[index].Format,
|
||||||
|
work.Targets[index].NumberType);
|
||||||
|
if (guestTextureFormat == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
_availableGuestImages[targets[index].Address] = guestTextureFormat;
|
||||||
|
_gpuGuestImages[targets[index].Address] = guestTextureFormat;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ShouldTraceGuestImageWriteForDiagnostics(target.Address))
|
|
||||||
|
foreach (var target in targets)
|
||||||
{
|
{
|
||||||
var writeCount = _tracedGuestWriteCounts.TryGetValue(
|
if (ShouldTraceGuestImageWriteForDiagnostics(target.Address))
|
||||||
target.Address,
|
|
||||||
out var previousCount)
|
|
||||||
? previousCount + 1
|
|
||||||
: 1;
|
|
||||||
_tracedGuestWriteCounts[target.Address] = writeCount;
|
|
||||||
if (writeCount <= 3)
|
|
||||||
{
|
{
|
||||||
_commandBuffer = _presentationCommandBuffer;
|
var writeCount = _tracedGuestWriteCounts.TryGetValue(
|
||||||
Check(
|
target.Address,
|
||||||
_vk.QueueWaitIdle(_queue),
|
out var previousCount)
|
||||||
"vkQueueWaitIdle(guest write trace)");
|
? previousCount + 1
|
||||||
Console.Error.WriteLine(
|
: 1;
|
||||||
$"[LOADER][TRACE] vk.guest_write_sample " +
|
_tracedGuestWriteCounts[target.Address] = writeCount;
|
||||||
$"addr=0x{target.Address:X16} write={writeCount} " +
|
if (writeCount <= 3)
|
||||||
$"ps_bytes={work.Draw.PixelSpirv.Length}");
|
{
|
||||||
TraceGuestImageContents(target);
|
_commandBuffer = _presentationCommandBuffer;
|
||||||
|
Check(
|
||||||
|
_vk.QueueWaitIdle(_queue),
|
||||||
|
"vkQueueWaitIdle(guest write trace)");
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] vk.guest_write_sample " +
|
||||||
|
$"addr=0x{target.Address:X16} write={writeCount} " +
|
||||||
|
$"ps_bytes={work.Draw.PixelSpirv.Length}");
|
||||||
|
TraceGuestImageContents(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TraceVulkanShader(
|
TraceVulkanShader(
|
||||||
$"vk.offscreen_draw addr=0x{target.Address:X16} " +
|
$"vk.offscreen_draw mrt={targets.Length} " +
|
||||||
$"size={target.Width}x{target.Height} format={target.Format} " +
|
$"size={firstTarget.Width}x{firstTarget.Height} " +
|
||||||
$"textures={work.Draw.Textures.Count}");
|
$"textures={work.Draw.Textures.Count}");
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
@@ -4866,19 +5112,22 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_guestImages.TryGetValue(work.Target.Address, out var failedTarget) ||
|
lock (_gate)
|
||||||
!failedTarget.Initialized)
|
|
||||||
{
|
{
|
||||||
lock (_gate)
|
foreach (var target in work.Targets)
|
||||||
{
|
{
|
||||||
_availableGuestImages.Remove(work.Target.Address);
|
if (!_guestImages.TryGetValue(target.Address, out var failedTarget) ||
|
||||||
_gpuGuestImages.Remove(work.Target.Address);
|
!failedTarget.Initialized)
|
||||||
|
{
|
||||||
|
_availableGuestImages.Remove(target.Address);
|
||||||
|
_gpuGuestImages.Remove(target.Address);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][ERROR] Vulkan offscreen draw failed " +
|
$"[LOADER][ERROR] Vulkan offscreen draw failed " +
|
||||||
$"addr=0x{work.Target.Address:X16}: {exception.Message}");
|
$"mrt={work.Targets.Count}: {exception.Message}");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -4896,6 +5145,16 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
{
|
{
|
||||||
DestroyTranslatedDrawResources(resources);
|
DestroyTranslatedDrawResources(resources);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (transientFramebuffer.Handle != 0)
|
||||||
|
{
|
||||||
|
_vk.DestroyFramebuffer(_device, transientFramebuffer, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transientRenderPass.Handle != 0)
|
||||||
|
{
|
||||||
|
_vk.DestroyRenderPass(_device, transientRenderPass, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4934,6 +5193,8 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CompletePendingPresentation(wait: true);
|
||||||
|
WaitForAllGuestSubmissions();
|
||||||
DestroyGuestImage(existing);
|
DestroyGuestImage(existing);
|
||||||
_guestImages.Remove(target.Address);
|
_guestImages.Remove(target.Address);
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
@@ -5052,35 +5313,55 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
Format format,
|
Format format,
|
||||||
ImageView attachmentView,
|
ImageView attachmentView,
|
||||||
uint width,
|
uint width,
|
||||||
|
uint height) =>
|
||||||
|
CreateRenderPassAndFramebuffer([format], [attachmentView], width, height);
|
||||||
|
|
||||||
|
private (RenderPass RenderPass, Framebuffer Framebuffer) CreateRenderPassAndFramebuffer(
|
||||||
|
IReadOnlyList<Format> formats,
|
||||||
|
IReadOnlyList<ImageView> attachmentViews,
|
||||||
|
uint width,
|
||||||
uint height)
|
uint height)
|
||||||
{
|
{
|
||||||
var colorAttachment = new AttachmentDescription
|
if (formats.Count == 0 || formats.Count != attachmentViews.Count)
|
||||||
{
|
{
|
||||||
Format = format,
|
throw new InvalidOperationException("render target formats and views must have matching counts");
|
||||||
Samples = SampleCountFlags.Count1Bit,
|
}
|
||||||
LoadOp = AttachmentLoadOp.Load,
|
|
||||||
StoreOp = AttachmentStoreOp.Store,
|
var colorAttachments = stackalloc AttachmentDescription[formats.Count];
|
||||||
StencilLoadOp = AttachmentLoadOp.DontCare,
|
var colorReferences = stackalloc AttachmentReference[formats.Count];
|
||||||
StencilStoreOp = AttachmentStoreOp.DontCare,
|
var views = stackalloc ImageView[formats.Count];
|
||||||
InitialLayout = ImageLayout.ColorAttachmentOptimal,
|
for (var index = 0; index < formats.Count; index++)
|
||||||
FinalLayout = ImageLayout.ColorAttachmentOptimal,
|
|
||||||
};
|
|
||||||
var colorReference = new AttachmentReference
|
|
||||||
{
|
{
|
||||||
Attachment = 0,
|
colorAttachments[index] = new AttachmentDescription
|
||||||
Layout = ImageLayout.ColorAttachmentOptimal,
|
{
|
||||||
};
|
Format = formats[index],
|
||||||
|
Samples = SampleCountFlags.Count1Bit,
|
||||||
|
LoadOp = AttachmentLoadOp.Load,
|
||||||
|
StoreOp = AttachmentStoreOp.Store,
|
||||||
|
StencilLoadOp = AttachmentLoadOp.DontCare,
|
||||||
|
StencilStoreOp = AttachmentStoreOp.DontCare,
|
||||||
|
InitialLayout = ImageLayout.ColorAttachmentOptimal,
|
||||||
|
FinalLayout = ImageLayout.ColorAttachmentOptimal,
|
||||||
|
};
|
||||||
|
colorReferences[index] = new AttachmentReference
|
||||||
|
{
|
||||||
|
Attachment = (uint)index,
|
||||||
|
Layout = ImageLayout.ColorAttachmentOptimal,
|
||||||
|
};
|
||||||
|
views[index] = attachmentViews[index];
|
||||||
|
}
|
||||||
|
|
||||||
var subpass = new SubpassDescription
|
var subpass = new SubpassDescription
|
||||||
{
|
{
|
||||||
PipelineBindPoint = PipelineBindPoint.Graphics,
|
PipelineBindPoint = PipelineBindPoint.Graphics,
|
||||||
ColorAttachmentCount = 1,
|
ColorAttachmentCount = (uint)formats.Count,
|
||||||
PColorAttachments = &colorReference,
|
PColorAttachments = colorReferences,
|
||||||
};
|
};
|
||||||
var renderPassInfo = new RenderPassCreateInfo
|
var renderPassInfo = new RenderPassCreateInfo
|
||||||
{
|
{
|
||||||
SType = StructureType.RenderPassCreateInfo,
|
SType = StructureType.RenderPassCreateInfo,
|
||||||
AttachmentCount = 1,
|
AttachmentCount = (uint)formats.Count,
|
||||||
PAttachments = &colorAttachment,
|
PAttachments = colorAttachments,
|
||||||
SubpassCount = 1,
|
SubpassCount = 1,
|
||||||
PSubpasses = &subpass,
|
PSubpasses = &subpass,
|
||||||
};
|
};
|
||||||
@@ -5088,13 +5369,12 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
_vk.CreateRenderPass(_device, &renderPassInfo, null, out var renderPass),
|
_vk.CreateRenderPass(_device, &renderPassInfo, null, out var renderPass),
|
||||||
"vkCreateRenderPass(offscreen)");
|
"vkCreateRenderPass(offscreen)");
|
||||||
|
|
||||||
var attachment = attachmentView;
|
|
||||||
var framebufferInfo = new FramebufferCreateInfo
|
var framebufferInfo = new FramebufferCreateInfo
|
||||||
{
|
{
|
||||||
SType = StructureType.FramebufferCreateInfo,
|
SType = StructureType.FramebufferCreateInfo,
|
||||||
RenderPass = renderPass,
|
RenderPass = renderPass,
|
||||||
AttachmentCount = 1,
|
AttachmentCount = (uint)formats.Count,
|
||||||
PAttachments = &attachment,
|
PAttachments = views,
|
||||||
Width = width,
|
Width = width,
|
||||||
Height = height,
|
Height = height,
|
||||||
Layers = 1,
|
Layers = 1,
|
||||||
@@ -5632,7 +5912,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
translatedResources = CreateTranslatedDrawResources(
|
translatedResources = CreateTranslatedDrawResources(
|
||||||
translatedDraw,
|
translatedDraw,
|
||||||
_renderPass,
|
_renderPass,
|
||||||
_swapchainFormat,
|
[_swapchainFormat],
|
||||||
_extent);
|
_extent);
|
||||||
if (ShouldTracePresentedGuestImageContentsForDiagnostics() &&
|
if (ShouldTracePresentedGuestImageContentsForDiagnostics() &&
|
||||||
!_firstGuestDrawPresented)
|
!_firstGuestDrawPresented)
|
||||||
@@ -6527,6 +6807,16 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
private void DestroyTranslatedDrawResources(TranslatedDrawResources resources)
|
private void DestroyTranslatedDrawResources(TranslatedDrawResources resources)
|
||||||
{
|
{
|
||||||
|
if (resources.TransientFramebuffer.Handle != 0)
|
||||||
|
{
|
||||||
|
_vk.DestroyFramebuffer(_device, resources.TransientFramebuffer, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resources.TransientRenderPass.Handle != 0)
|
||||||
|
{
|
||||||
|
_vk.DestroyRenderPass(_device, resources.TransientRenderPass, null);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var texture in resources.Textures)
|
foreach (var texture in resources.Textures)
|
||||||
{
|
{
|
||||||
if (texture is null)
|
if (texture is null)
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Text;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.Ampr;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Tests.Ampr;
|
||||||
|
|
||||||
|
// PakDirectoryTracker reconstructs the absolute pak offset for a "next sequential chunk" read
|
||||||
|
// (offset -1) from the requested byte count. When several archived files share that byte count,
|
||||||
|
// picking by directory order alone mis-resolves out-of-order reads: Quake requested progs/h_ogre.mdl
|
||||||
|
// (0x3A34 bytes) but the tracker returned bots/navigation/death32c.nav, which shares the size and
|
||||||
|
// sits earlier in the directory, so the guest parsed NAV2 data as a brush model and aborted.
|
||||||
|
public sealed class PakDirectoryTrackerTests
|
||||||
|
{
|
||||||
|
private const int EntrySize = 64;
|
||||||
|
private const int NameLength = 56;
|
||||||
|
|
||||||
|
private readonly record struct PakEntry(string Name, uint FilePos, uint FileLen);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveSequentialOffset_SizeCollision_PicksEntryNearestReadCursor()
|
||||||
|
{
|
||||||
|
const uint fileId = 0x5AA5_0001;
|
||||||
|
const uint collidingLen = 0x3A34;
|
||||||
|
var memory = new FakeCpuMemory(0x1_0000_0000, 0x1000);
|
||||||
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||||
|
|
||||||
|
// "far" collides in size but lives early in the pak; "near" is what the guest actually wants.
|
||||||
|
LoadDirectory(ctx, fileId, memory, 0x1_0000_0000, dirFileOffset: 0x400, new[]
|
||||||
|
{
|
||||||
|
new PakEntry("far.dat", FilePos: 0x1000, FileLen: collidingLen),
|
||||||
|
new PakEntry("near.dat", FilePos: 0x9000, FileLen: collidingLen),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Advance the read cursor next to the "near" entry, mimicking a burst of nearby asset reads.
|
||||||
|
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination: 0x1_0000_0000, fileOffset: 0x8800, bytesRead: 0x400);
|
||||||
|
|
||||||
|
Assert.Equal(0x9000UL, PakDirectoryTracker.ResolveSequentialOffset(fileId, collidingLen));
|
||||||
|
// Once the near entry is consumed, the same size resolves to the remaining match.
|
||||||
|
Assert.Equal(0x1000UL, PakDirectoryTracker.ResolveSequentialOffset(fileId, collidingLen));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveSequentialOffset_ContiguousSameSizeRun_StaysInOrder()
|
||||||
|
{
|
||||||
|
const uint fileId = 0x5AA5_0002;
|
||||||
|
const uint runLen = 0x608;
|
||||||
|
var memory = new FakeCpuMemory(0x1_0000_0000, 0x1000);
|
||||||
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||||
|
|
||||||
|
// A contiguous run of equal-size lumps (like Quake's gfx/weapons/ww_*.lmp) must still resolve
|
||||||
|
// in packed order when the guest streams them sequentially.
|
||||||
|
LoadDirectory(ctx, fileId, memory, 0x1_0000_0000, dirFileOffset: 0x400, new[]
|
||||||
|
{
|
||||||
|
new PakEntry("lump_a", FilePos: 0x1000, FileLen: runLen),
|
||||||
|
new PakEntry("lump_b", FilePos: 0x1000 + runLen, FileLen: runLen),
|
||||||
|
new PakEntry("lump_c", FilePos: 0x1000 + (runLen * 2), FileLen: runLen),
|
||||||
|
});
|
||||||
|
|
||||||
|
var first = PakDirectoryTracker.ResolveSequentialOffset(fileId, runLen);
|
||||||
|
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination: 0x1_0000_0000, fileOffset: first, bytesRead: runLen);
|
||||||
|
var second = PakDirectoryTracker.ResolveSequentialOffset(fileId, runLen);
|
||||||
|
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination: 0x1_0000_0000, fileOffset: second, bytesRead: runLen);
|
||||||
|
var third = PakDirectoryTracker.ResolveSequentialOffset(fileId, runLen);
|
||||||
|
|
||||||
|
Assert.Equal(0x1000UL, first);
|
||||||
|
Assert.Equal(0x1000UL + runLen, second);
|
||||||
|
Assert.Equal(0x1000UL + (runLen * 2), third);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Feeds the tracker a synthetic PACK header + directory table exactly as the AMPR read path does:
|
||||||
|
// first the 12-byte header (which arms directory parsing), then the directory records themselves.
|
||||||
|
private static void LoadDirectory(
|
||||||
|
CpuContext ctx,
|
||||||
|
uint fileId,
|
||||||
|
FakeCpuMemory memory,
|
||||||
|
ulong destination,
|
||||||
|
ulong dirFileOffset,
|
||||||
|
PakEntry[] entries)
|
||||||
|
{
|
||||||
|
Span<byte> header = stackalloc byte[12];
|
||||||
|
header[0] = (byte)'P';
|
||||||
|
header[1] = (byte)'A';
|
||||||
|
header[2] = (byte)'C';
|
||||||
|
header[3] = (byte)'K';
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(header[4..8], (uint)dirFileOffset);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(header[8..12], (uint)(entries.Length * EntrySize));
|
||||||
|
memory.TryWrite(destination, header);
|
||||||
|
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination, fileOffset: 0, bytesRead: 12);
|
||||||
|
|
||||||
|
var table = new byte[entries.Length * EntrySize];
|
||||||
|
for (var i = 0; i < entries.Length; i++)
|
||||||
|
{
|
||||||
|
var record = table.AsSpan(i * EntrySize, EntrySize);
|
||||||
|
var name = Encoding.ASCII.GetBytes(entries[i].Name);
|
||||||
|
name.AsSpan(0, Math.Min(name.Length, NameLength)).CopyTo(record);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(record.Slice(56, 4), entries[i].FilePos);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(record.Slice(60, 4), entries[i].FileLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
memory.TryWrite(destination, table);
|
||||||
|
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination, dirFileOffset, (ulong)table.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Tests;
|
||||||
|
|
||||||
|
// A single contiguous guest region backed by a byte[]. Enough to hand C strings and small
|
||||||
|
// structures to HLE exports under test without a live guest.
|
||||||
|
internal sealed class FakeCpuMemory : ICpuMemory
|
||||||
|
{
|
||||||
|
private readonly ulong _base;
|
||||||
|
private readonly byte[] _storage;
|
||||||
|
|
||||||
|
public FakeCpuMemory(ulong baseAddress, int size)
|
||||||
|
{
|
||||||
|
_base = baseAddress;
|
||||||
|
_storage = new byte[size];
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryRead(ulong virtualAddress, Span<byte> destination)
|
||||||
|
{
|
||||||
|
if (!TryResolve(virtualAddress, destination.Length, out var offset))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_storage.AsSpan(offset, destination.Length).CopyTo(destination);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
|
||||||
|
{
|
||||||
|
if (!TryResolve(virtualAddress, source.Length, out var offset))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
source.CopyTo(_storage.AsSpan(offset, source.Length));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ulong WriteCString(ulong virtualAddress, string text)
|
||||||
|
{
|
||||||
|
var bytes = System.Text.Encoding.UTF8.GetBytes(text);
|
||||||
|
TryWrite(virtualAddress, bytes);
|
||||||
|
TryWrite(virtualAddress + (ulong)bytes.Length, stackalloc byte[] { 0 });
|
||||||
|
return virtualAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryResolve(ulong virtualAddress, int length, out int offset)
|
||||||
|
{
|
||||||
|
offset = 0;
|
||||||
|
if (virtualAddress < _base)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var relative = virtualAddress - _base;
|
||||||
|
if (relative + (ulong)length > (ulong)_storage.Length)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset = (int)relative;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.Json;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Tests.Json;
|
||||||
|
|
||||||
|
// These NIDs came back "unresolved" in the Quake (PPSA01880) import log right before its
|
||||||
|
// access violation. This asserts they now resolve to the Json handlers and dispatch cleanly,
|
||||||
|
// which is the plumbing the direct-call tests cannot cover.
|
||||||
|
[Collection("JsonObjectHeap")]
|
||||||
|
public sealed class JsonExportRegistrationTests
|
||||||
|
{
|
||||||
|
private static readonly (string Nid, string Name)[] ExpectedExports =
|
||||||
|
{
|
||||||
|
("qBMjqyBn3OM", "_ZN3sce4Json5ValueC1Ev"),
|
||||||
|
("5yHuiWXo2gg", "_ZN3sce4Json5Value3setEb"),
|
||||||
|
("QxVVYhP-mvg", "_ZN3sce4Json5Value3setEl"),
|
||||||
|
("SIe1ZmW7e7s", "_ZN3sce4Json5Value3setEm"),
|
||||||
|
("BSmWDIkV4w4", "_ZN3sce4Json5Value3setEd"),
|
||||||
|
("IKQimvG9Wqs", "_ZN3sce4Json5Value3setENS0_9ValueTypeE"),
|
||||||
|
("6l3Bv2gysNc", "_ZN3sce4Json5Value3setERKNS0_6StringE"),
|
||||||
|
("9KUZFjI1IxA", "_ZN3sce4Json6StringC1EPKc"),
|
||||||
|
("cG1VE2HMl6c", "_ZN3sce4Json6StringD1Ev"),
|
||||||
|
("+drDFyAS6u4", "_ZN3sce4Json11Initializer27setGlobalNullAccessCallbackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_"),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static ModuleManager CreateRegisteredManager()
|
||||||
|
{
|
||||||
|
var manager = new ModuleManager();
|
||||||
|
manager.RegisterFromAssembly(typeof(JsonValueExports).Assembly, Generation.Gen5);
|
||||||
|
return manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void QuakeUnresolvedJsonNids_ResolveToJsonExports()
|
||||||
|
{
|
||||||
|
var manager = CreateRegisteredManager();
|
||||||
|
|
||||||
|
foreach (var (nid, name) in ExpectedExports)
|
||||||
|
{
|
||||||
|
Assert.True(manager.TryGetExport(nid, out var export), $"NID {nid} did not register.");
|
||||||
|
Assert.Equal(name, export.Name);
|
||||||
|
Assert.Equal("libSceJson", export.LibraryName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetGlobalNullAccessCallback_StoresHookAndReturnsOk()
|
||||||
|
{
|
||||||
|
JsonObjectHeap.ResetForTests();
|
||||||
|
var manager = CreateRegisteredManager();
|
||||||
|
var ctx = new CpuContext(new FakeCpuMemory(0x1_0000_0000, 0x1000), Generation.Gen5);
|
||||||
|
ctx[CpuRegister.Rdi] = 0x1_0000_0000; // Initializer instance
|
||||||
|
ctx[CpuRegister.Rsi] = 0x8_0012_3456; // guest callback
|
||||||
|
ctx[CpuRegister.Rdx] = 0x1_0000_0800; // user context
|
||||||
|
|
||||||
|
Assert.True(manager.TryDispatch("+drDFyAS6u4", ctx, out var result));
|
||||||
|
Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, result);
|
||||||
|
Assert.Equal(0UL, ctx[CpuRegister.Rax]);
|
||||||
|
Assert.Equal(0x8_0012_3456UL, JsonObjectHeap.GlobalNullAccessCallback);
|
||||||
|
Assert.Equal(0x1_0000_0800UL, JsonObjectHeap.GlobalNullAccessCallbackContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DispatchValueConstructor_RunsHandlerAndReturnsThis()
|
||||||
|
{
|
||||||
|
JsonObjectHeap.ResetForTests();
|
||||||
|
var manager = CreateRegisteredManager();
|
||||||
|
var ctx = new CpuContext(new FakeCpuMemory(0x1_0000_0000, 0x1000), Generation.Gen5);
|
||||||
|
ctx[CpuRegister.Rdi] = 0x1_0000_0000;
|
||||||
|
|
||||||
|
Assert.True(manager.TryDispatch("qBMjqyBn3OM", ctx, out var result));
|
||||||
|
Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, result);
|
||||||
|
Assert.Equal(0x1_0000_0000UL, ctx[CpuRegister.Rax]);
|
||||||
|
Assert.Equal(JsonValueKind.Null, JsonObjectHeap.Values[0x1_0000_0000].Kind);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.Json;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Tests.Json;
|
||||||
|
|
||||||
|
// JsonObjectHeap is shared static state; both Json test classes join one collection so xUnit
|
||||||
|
// does not run them in parallel against it.
|
||||||
|
[Collection("JsonObjectHeap")]
|
||||||
|
public sealed class JsonValueExportsTests
|
||||||
|
{
|
||||||
|
private const ulong ThisAddress = 0x1_0000_0000;
|
||||||
|
private const ulong StringAddress = 0x1_0000_1000;
|
||||||
|
private const ulong TextAddress = 0x1_0000_2000;
|
||||||
|
|
||||||
|
private readonly FakeCpuMemory _memory = new(0x1_0000_0000, 0x10000);
|
||||||
|
private readonly CpuContext _ctx;
|
||||||
|
|
||||||
|
public JsonValueExportsTests()
|
||||||
|
{
|
||||||
|
JsonObjectHeap.ResetForTests();
|
||||||
|
_ctx = new CpuContext(_memory, Generation.Gen5);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValueDefaultConstructor_RegistersNullAndReturnsThis()
|
||||||
|
{
|
||||||
|
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||||
|
|
||||||
|
JsonValueExports.ValueDefaultConstructor(_ctx);
|
||||||
|
|
||||||
|
Assert.Equal(ThisAddress, _ctx[CpuRegister.Rax]);
|
||||||
|
Assert.Equal(JsonValueKind.Null, JsonObjectHeap.Values[ThisAddress].Kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0UL, false)]
|
||||||
|
[InlineData(1UL, true)]
|
||||||
|
[InlineData(0xFFFF_FF00UL, false)] // only the low byte is the bool; 0x00 low byte => false
|
||||||
|
public void ValueSetBoolean_StoresLowByte(ulong raw, bool expected)
|
||||||
|
{
|
||||||
|
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||||
|
_ctx[CpuRegister.Rsi] = raw;
|
||||||
|
|
||||||
|
JsonValueExports.ValueSetBoolean(_ctx);
|
||||||
|
|
||||||
|
var state = JsonObjectHeap.Values[ThisAddress];
|
||||||
|
Assert.Equal(JsonValueKind.Boolean, state.Kind);
|
||||||
|
Assert.Equal(expected, state.Boolean);
|
||||||
|
Assert.Equal(ThisAddress, _ctx[CpuRegister.Rax]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValueSetInteger_RoundTripsSignedValue()
|
||||||
|
{
|
||||||
|
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||||
|
_ctx[CpuRegister.Rsi] = unchecked((ulong)-42L);
|
||||||
|
|
||||||
|
JsonValueExports.ValueSetInteger(_ctx);
|
||||||
|
|
||||||
|
var state = JsonObjectHeap.Values[ThisAddress];
|
||||||
|
Assert.Equal(JsonValueKind.Integer, state.Kind);
|
||||||
|
Assert.Equal(-42L, state.Integer);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValueSetUnsigned_RoundTripsFullWidth()
|
||||||
|
{
|
||||||
|
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||||
|
_ctx[CpuRegister.Rsi] = ulong.MaxValue;
|
||||||
|
|
||||||
|
JsonValueExports.ValueSetUnsigned(_ctx);
|
||||||
|
|
||||||
|
var state = JsonObjectHeap.Values[ThisAddress];
|
||||||
|
Assert.Equal(JsonValueKind.UInteger, state.Kind);
|
||||||
|
Assert.Equal(ulong.MaxValue, state.UnsignedInteger);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValueSetReal_ReadsDoubleFromXmm0()
|
||||||
|
{
|
||||||
|
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||||
|
_ctx.SetXmmRegister(0, unchecked((ulong)BitConverter.DoubleToInt64Bits(3.14159)), 0);
|
||||||
|
|
||||||
|
JsonValueExports.ValueSetReal(_ctx);
|
||||||
|
|
||||||
|
var state = JsonObjectHeap.Values[ThisAddress];
|
||||||
|
Assert.Equal(JsonValueKind.Real, state.Kind);
|
||||||
|
Assert.Equal(3.14159, state.Real, precision: 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValueSetCString_ReadsGuestString()
|
||||||
|
{
|
||||||
|
_memory.WriteCString(TextAddress, "hello json");
|
||||||
|
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||||
|
_ctx[CpuRegister.Rsi] = TextAddress;
|
||||||
|
|
||||||
|
JsonValueExports.ValueSetCString(_ctx);
|
||||||
|
|
||||||
|
var state = JsonObjectHeap.Values[ThisAddress];
|
||||||
|
Assert.Equal(JsonValueKind.String, state.Kind);
|
||||||
|
Assert.Equal("hello json", state.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValueSetType_KeepsRawGuestEnumValue()
|
||||||
|
{
|
||||||
|
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||||
|
_ctx[CpuRegister.Rsi] = 7;
|
||||||
|
|
||||||
|
JsonValueExports.ValueSetType(_ctx);
|
||||||
|
|
||||||
|
var state = JsonObjectHeap.Values[ThisAddress];
|
||||||
|
Assert.Equal(JsonValueKind.ExplicitType, state.Kind);
|
||||||
|
Assert.Equal(7u, state.ExplicitType);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StringConstructThenValueSetString_CopiesText()
|
||||||
|
{
|
||||||
|
_memory.WriteCString(TextAddress, "from string object");
|
||||||
|
_ctx[CpuRegister.Rdi] = StringAddress;
|
||||||
|
_ctx[CpuRegister.Rsi] = TextAddress;
|
||||||
|
JsonValueExports.StringCStringConstructor(_ctx);
|
||||||
|
|
||||||
|
Assert.Equal("from string object", JsonObjectHeap.Strings[StringAddress]);
|
||||||
|
Assert.Equal(StringAddress, _ctx[CpuRegister.Rax]);
|
||||||
|
|
||||||
|
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||||
|
_ctx[CpuRegister.Rsi] = StringAddress;
|
||||||
|
JsonValueExports.ValueSetString(_ctx);
|
||||||
|
|
||||||
|
var state = JsonObjectHeap.Values[ThisAddress];
|
||||||
|
Assert.Equal(JsonValueKind.String, state.Kind);
|
||||||
|
Assert.Equal("from string object", state.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValueSetString_MissingStringShadow_DegradesToEmpty()
|
||||||
|
{
|
||||||
|
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||||
|
_ctx[CpuRegister.Rsi] = StringAddress; // never constructed
|
||||||
|
|
||||||
|
JsonValueExports.ValueSetString(_ctx);
|
||||||
|
|
||||||
|
var state = JsonObjectHeap.Values[ThisAddress];
|
||||||
|
Assert.Equal(JsonValueKind.String, state.Kind);
|
||||||
|
Assert.Equal(string.Empty, state.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Destructors_RemoveShadowState()
|
||||||
|
{
|
||||||
|
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||||
|
JsonValueExports.ValueDefaultConstructor(_ctx);
|
||||||
|
_ctx[CpuRegister.Rdi] = StringAddress;
|
||||||
|
JsonValueExports.StringDefaultConstructor(_ctx);
|
||||||
|
|
||||||
|
Assert.True(JsonObjectHeap.Values.ContainsKey(ThisAddress));
|
||||||
|
Assert.True(JsonObjectHeap.Strings.ContainsKey(StringAddress));
|
||||||
|
|
||||||
|
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||||
|
JsonValueExports.ValueDestructor(_ctx);
|
||||||
|
_ctx[CpuRegister.Rdi] = StringAddress;
|
||||||
|
JsonValueExports.StringDestructor(_ctx);
|
||||||
|
|
||||||
|
Assert.False(JsonObjectHeap.Values.ContainsKey(ThisAddress));
|
||||||
|
Assert.False(JsonObjectHeap.Strings.ContainsKey(StringAddress));
|
||||||
|
Assert.Equal(0UL, _ctx[CpuRegister.Rax]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValueSetCString_FaultingPointer_DegradesToEmptyString()
|
||||||
|
{
|
||||||
|
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||||
|
_ctx[CpuRegister.Rsi] = 0xDEAD_0000_0000; // outside the mapped region
|
||||||
|
|
||||||
|
JsonValueExports.ValueSetCString(_ctx);
|
||||||
|
|
||||||
|
var state = JsonObjectHeap.Values[ThisAddress];
|
||||||
|
Assert.Equal(JsonValueKind.String, state.Kind);
|
||||||
|
Assert.Equal(string.Empty, state.Text);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
-->
|
||||||
|
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\SharpEmu.Libs\SharpEmu.Libs.csproj" />
|
||||||
|
<ProjectReference Include="..\..\src\SharpEmu.HLE\SharpEmu.HLE.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
|
<PackageReference Include="xunit" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"dependencies": {
|
||||||
|
"net10.0": {
|
||||||
|
"Microsoft.NET.Test.Sdk": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[17.14.1, )",
|
||||||
|
"resolved": "17.14.1",
|
||||||
|
"contentHash": "HJKqKOE+vshXra2aEHpi2TlxYX7Z9VFYkr+E5rwEvHC8eIXiyO+K9kNm8vmNom3e2rA56WqxU+/N9NJlLGXsJQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.CodeCoverage": "17.14.1",
|
||||||
|
"Microsoft.TestPlatform.TestHost": "17.14.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"xunit": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[2.9.3, )",
|
||||||
|
"resolved": "2.9.3",
|
||||||
|
"contentHash": "TlXQBinK35LpOPKHAqbLY4xlEen9TBafjs0V5KnA4wZsoQLQJiirCR4CbIXvOH8NzkW4YeJKP5P/Bnrodm0h9Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"xunit.analyzers": "1.18.0",
|
||||||
|
"xunit.assert": "2.9.3",
|
||||||
|
"xunit.core": "[2.9.3]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"xunit.runner.visualstudio": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[3.1.1, )",
|
||||||
|
"resolved": "3.1.1",
|
||||||
|
"contentHash": "gNu2zhnuwjq5vQlU4S7yK/lfaKZDLmtcu+vTjnhfTlMAUYn+Hmgu8IIX0UCwWepYkk+Szx03DHx1bDnc9Fd+9w=="
|
||||||
|
},
|
||||||
|
"Microsoft.CodeCoverage": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "17.14.1",
|
||||||
|
"contentHash": "pmTrhfFIoplzFVbhVwUquT+77CbGH+h4/3mBpdmIlYtBi9nAB+kKI6dN3A/nV4DFi3wLLx/BlHIPK+MkbQ6Tpg=="
|
||||||
|
},
|
||||||
|
"Microsoft.DotNet.PlatformAbstractions": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "3.1.6",
|
||||||
|
"contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg=="
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.DependencyModel": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "9.0.9",
|
||||||
|
"contentHash": "fNGvKct2De8ghm0Bpfq0iWthtzIWabgOTi+gJhNOPhNJIowXNEUE2eZNW/zNCzrHVA3PXg2yZ+3cWZndC2IqYA=="
|
||||||
|
},
|
||||||
|
"Microsoft.TestPlatform.ObjectModel": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "17.14.1",
|
||||||
|
"contentHash": "xTP1W6Mi6SWmuxd3a+jj9G9UoC850WGwZUps1Wah9r1ZxgXhdJfj1QqDLJkFjHDCvN42qDL2Ps5KjQYWUU0zcQ=="
|
||||||
|
},
|
||||||
|
"Microsoft.TestPlatform.TestHost": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "17.14.1",
|
||||||
|
"contentHash": "d78LPzGKkJwsJXAQwsbJJ7LE7D1wB+rAyhHHAaODF+RDSQ0NgMjDFkSA1Djw18VrxO76GlKAjRUhl+H8NL8Z+Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.TestPlatform.ObjectModel": "17.14.1",
|
||||||
|
"Newtonsoft.Json": "13.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Newtonsoft.Json": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "13.0.3",
|
||||||
|
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
|
||||||
|
},
|
||||||
|
"Silk.NET.Core": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "D7AT/nnwlB+4RZ84XY8QNGBZMJI5z9l4CSSETIJ1wCfRJzRt/341y3MRZ4HbnFz4r/IGaWOEZr86iE+0/65yyQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.DotNet.PlatformAbstractions": "3.1.6",
|
||||||
|
"Microsoft.Extensions.DependencyModel": "9.0.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Silk.NET.GLFW": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "UIs4sH57xlPUNHQ/1bt9rymPWlGy8IMDCNv86h0iM4TOA1CkIx0XM/n/tA4AReh1zQkNrvkxPEdZ3Blvy1dyXg==",
|
||||||
|
"dependencies": {
|
||||||
|
"Silk.NET.Core": "2.23.0",
|
||||||
|
"Ultz.Native.GLFW": "3.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Silk.NET.Maths": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "r8PdIVzME8EH0qAgbmRPO87I4GfgR2j8TofT7EMuRJDf1QluoQwnVypDoFJjQ2ZBSRsGYk5unYxxogI05Ogsmw=="
|
||||||
|
},
|
||||||
|
"Silk.NET.Windowing.Common": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "ThStSinmY9KQI8DGiF5XEhkLJVnBcgRTBTzL9ijg1wMZAYuckz7ykrNw04fjRm2Gryh6tCNGbvz2XaY0efeFzg==",
|
||||||
|
"dependencies": {
|
||||||
|
"Silk.NET.Core": "2.23.0",
|
||||||
|
"Silk.NET.Maths": "2.23.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Silk.NET.Windowing.Glfw": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "aYBudKmENmvLRn9p15HbdvlQTnnXskcDfTfbYwSb/4fr263rGLwYuDw/txUEc2jihHJiWCp5+75Y7z5wTJWl7g==",
|
||||||
|
"dependencies": {
|
||||||
|
"Silk.NET.GLFW": "2.23.0",
|
||||||
|
"Silk.NET.Windowing.Common": "2.23.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Ultz.Native.GLFW": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "3.4.0",
|
||||||
|
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||||
|
},
|
||||||
|
"xunit.abstractions": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.0.3",
|
||||||
|
"contentHash": "pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg=="
|
||||||
|
},
|
||||||
|
"xunit.analyzers": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "1.18.0",
|
||||||
|
"contentHash": "OtFMHN8yqIcYP9wcVIgJrq01AfTxijjAqVDy/WeQVSyrDC1RzBWeQPztL49DN2syXRah8TYnfvk035s7L95EZQ=="
|
||||||
|
},
|
||||||
|
"xunit.assert": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.9.3",
|
||||||
|
"contentHash": "/Kq28fCE7MjOV42YLVRAJzRF0WmEqsmflm0cfpMjGtzQ2lR5mYVj1/i0Y8uDAOLczkL3/jArrwehfMD0YogMAA=="
|
||||||
|
},
|
||||||
|
"xunit.core": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.9.3",
|
||||||
|
"contentHash": "BiAEvqGvyme19wE0wTKdADH+NloYqikiU0mcnmiNyXaF9HyHmE6sr/3DC5vnBkgsWaE6yPyWszKSPSApWdRVeQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"xunit.extensibility.core": "[2.9.3]",
|
||||||
|
"xunit.extensibility.execution": "[2.9.3]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"xunit.extensibility.core": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.9.3",
|
||||||
|
"contentHash": "kf3si0YTn2a8J8eZNb+zFpwfoyvIrQ7ivNk5ZYA5yuYk1bEtMe4DxJ2CF/qsRgmEnDr7MnW1mxylBaHTZ4qErA==",
|
||||||
|
"dependencies": {
|
||||||
|
"xunit.abstractions": "2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"xunit.extensibility.execution": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.9.3",
|
||||||
|
"contentHash": "yMb6vMESlSrE3Wfj7V6cjQ3S4TXdXpRqYeNEI3zsX31uTsGMJjEw6oD5F5u1cHnMptjhEECnmZSsPxB6ChZHDQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"xunit.extensibility.core": "[2.9.3]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sharpemu.hle": {
|
||||||
|
"type": "Project",
|
||||||
|
"dependencies": {
|
||||||
|
"SharpEmu.Logging": "[1.0.0, )"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sharpemu.libs": {
|
||||||
|
"type": "Project",
|
||||||
|
"dependencies": {
|
||||||
|
"SharpEmu.HLE": "[1.0.0, )",
|
||||||
|
"Silk.NET.Vulkan": "[2.23.0, )",
|
||||||
|
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
|
||||||
|
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
|
||||||
|
"Silk.NET.Windowing": "[2.23.0, )"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sharpemu.logging": {
|
||||||
|
"type": "Project"
|
||||||
|
},
|
||||||
|
"Silk.NET.Vulkan": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[2.23.0, )",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "3/irtlSWXZ3eTi8N6nelI6L34NTB8ZJHpqVMNzZx2aX7Ek9YEQ34NoQW8/Tljrtmkg8KRhHW8hKTEzZaKV8PgA==",
|
||||||
|
"dependencies": {
|
||||||
|
"Silk.NET.Core": "2.23.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Silk.NET.Vulkan.Extensions.EXT": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[2.23.0, )",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==",
|
||||||
|
"dependencies": {
|
||||||
|
"Silk.NET.Core": "2.23.0",
|
||||||
|
"Silk.NET.Vulkan": "2.23.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Silk.NET.Vulkan.Extensions.KHR": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[2.23.0, )",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "uRaf4j+SmH3DumjSSSUbFg33BnsGZUyXGj93O9NgGKZSJN3OTmNmQDxRew+/KiVLcgH6qzbto8aNGZ++j9GFWg==",
|
||||||
|
"dependencies": {
|
||||||
|
"Silk.NET.Core": "2.23.0",
|
||||||
|
"Silk.NET.Vulkan": "2.23.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Silk.NET.Windowing": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[2.23.0, )",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "OPNPmt/lRyUKVYrFLQXVxyATqD3MKLc1iY1oKx1/2GppgmZxVZPwN12tekrQ4C7408kgB1L5JD1Wnirqqeb2kg==",
|
||||||
|
"dependencies": {
|
||||||
|
"Silk.NET.Windowing.Common": "2.23.0",
|
||||||
|
"Silk.NET.Windowing.Glfw": "2.23.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,412 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
// Executes the SharpEmu-emitted "exec" conformance shader on a real Vulkan
|
||||||
|
// device and compares the buffer results against CPU-computed expected values.
|
||||||
|
//
|
||||||
|
// The shader (exec-cs.spv, produced by SharpEmu.Tools.ShaderDump) was
|
||||||
|
// translated by SharpEmu from hand-assembled Gen5 instruction words and stores
|
||||||
|
// results to guestBuffers[0]:
|
||||||
|
// [0] v_fmac_f32 -> fma(1.5f, 2.25f, 10.0f)
|
||||||
|
// [1] v_mul_hi_i32 -> high 32 bits of (int)0x7FFFFFFF * (int)0x00010003
|
||||||
|
// [2] v_mul_lo_i32 -> low 32 bits of the same product
|
||||||
|
// [3] store attempted with EXEC=0 -> must NOT land (sentinel remains)
|
||||||
|
// [4] store after EXEC restored -> 1.5f (0x3FC00000)
|
||||||
|
// Every other word of the buffer must still hold the sentinel afterwards.
|
||||||
|
//
|
||||||
|
// Creating the compute pipeline doubles as a driver-acceptance check for the
|
||||||
|
// emitted SPIR-V; the dispatch then verifies the arithmetic numerically.
|
||||||
|
//
|
||||||
|
// Usage: SharpEmu.Tools.GpuConformance <path-to-exec-cs.spv>
|
||||||
|
|
||||||
|
using Silk.NET.Core.Native;
|
||||||
|
using Silk.NET.Vulkan;
|
||||||
|
|
||||||
|
const uint Sentinel = 0xCAFEBABE;
|
||||||
|
|
||||||
|
// Must match the 64-byte global-memory binding ShaderDump constructs for the
|
||||||
|
// exec program.
|
||||||
|
const ulong BufferSize = 64;
|
||||||
|
|
||||||
|
var expectedFma = BitConverter.SingleToUInt32Bits(
|
||||||
|
MathF.FusedMultiplyAdd(1.5f, 2.25f, 10.0f));
|
||||||
|
var product = (long)0x7FFFFFFF * 0x00010003;
|
||||||
|
var expectedHi = (uint)(product >> 32);
|
||||||
|
var expectedLo = (uint)product;
|
||||||
|
var expectedRestored = BitConverter.SingleToUInt32Bits(1.5f);
|
||||||
|
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
var spvPath = args.Length > 0
|
||||||
|
? args[0]
|
||||||
|
: throw new InvalidOperationException(
|
||||||
|
"usage: SharpEmu.Tools.GpuConformance <path-to-exec-cs.spv>");
|
||||||
|
var code = File.ReadAllBytes(spvPath);
|
||||||
|
|
||||||
|
var vk = Vk.GetApi();
|
||||||
|
|
||||||
|
var appName = (byte*)SilkMarshal.StringToPtr("SharpEmuGpuConformance");
|
||||||
|
var appInfo = new ApplicationInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.ApplicationInfo,
|
||||||
|
PApplicationName = appName,
|
||||||
|
ApiVersion = Vk.Version13,
|
||||||
|
};
|
||||||
|
var instanceInfo = new InstanceCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.InstanceCreateInfo,
|
||||||
|
PApplicationInfo = &appInfo,
|
||||||
|
};
|
||||||
|
Check(vk.CreateInstance(in instanceInfo, null, out var instance), "vkCreateInstance");
|
||||||
|
|
||||||
|
uint deviceCount = 0;
|
||||||
|
vk.EnumeratePhysicalDevices(instance, &deviceCount, null);
|
||||||
|
if (deviceCount == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine("no Vulkan devices found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var physicalDevices = new PhysicalDevice[deviceCount];
|
||||||
|
fixed (PhysicalDevice* pDevices = physicalDevices)
|
||||||
|
{
|
||||||
|
vk.EnumeratePhysicalDevices(instance, &deviceCount, pDevices);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer the first discrete GPU; fall back to the first device.
|
||||||
|
var physical = physicalDevices[0];
|
||||||
|
foreach (var candidate in physicalDevices)
|
||||||
|
{
|
||||||
|
vk.GetPhysicalDeviceProperties(candidate, out var props);
|
||||||
|
if (props.DeviceType == PhysicalDeviceType.DiscreteGpu)
|
||||||
|
{
|
||||||
|
physical = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vk.GetPhysicalDeviceProperties(physical, out var chosenProps);
|
||||||
|
Console.WriteLine(
|
||||||
|
$"executing on: {SilkMarshal.PtrToString((nint)chosenProps.DeviceName)}");
|
||||||
|
|
||||||
|
uint familyCount = 0;
|
||||||
|
vk.GetPhysicalDeviceQueueFamilyProperties(physical, &familyCount, null);
|
||||||
|
var families = new QueueFamilyProperties[familyCount];
|
||||||
|
fixed (QueueFamilyProperties* pFamilies = families)
|
||||||
|
{
|
||||||
|
vk.GetPhysicalDeviceQueueFamilyProperties(physical, &familyCount, pFamilies);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint? computeFamilyFound = null;
|
||||||
|
for (uint index = 0; index < familyCount; index++)
|
||||||
|
{
|
||||||
|
if (families[index].QueueFlags.HasFlag(QueueFlags.ComputeBit))
|
||||||
|
{
|
||||||
|
computeFamilyFound = index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var computeFamily = computeFamilyFound
|
||||||
|
?? throw new InvalidOperationException("device has no compute-capable queue family");
|
||||||
|
|
||||||
|
// The emitted SPIR-V declares the Int64 capability.
|
||||||
|
vk.GetPhysicalDeviceFeatures(physical, out var supportedFeatures);
|
||||||
|
if (!supportedFeatures.ShaderInt64)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"device does not support shaderInt64, which the emitted SPIR-V requires");
|
||||||
|
}
|
||||||
|
|
||||||
|
var priority = 1f;
|
||||||
|
var queueInfo = new DeviceQueueCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.DeviceQueueCreateInfo,
|
||||||
|
QueueFamilyIndex = computeFamily,
|
||||||
|
QueueCount = 1,
|
||||||
|
PQueuePriorities = &priority,
|
||||||
|
};
|
||||||
|
var features = new PhysicalDeviceFeatures { ShaderInt64 = true };
|
||||||
|
var deviceInfo = new DeviceCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.DeviceCreateInfo,
|
||||||
|
QueueCreateInfoCount = 1,
|
||||||
|
PQueueCreateInfos = &queueInfo,
|
||||||
|
PEnabledFeatures = &features,
|
||||||
|
};
|
||||||
|
Check(vk.CreateDevice(physical, in deviceInfo, null, out var device), "vkCreateDevice");
|
||||||
|
vk.GetDeviceQueue(device, computeFamily, 0, out var queue);
|
||||||
|
|
||||||
|
// Storage buffer, host-visible so the CPU can prefill and read back.
|
||||||
|
var bufferInfo = new BufferCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.BufferCreateInfo,
|
||||||
|
Size = BufferSize,
|
||||||
|
Usage = BufferUsageFlags.StorageBufferBit,
|
||||||
|
SharingMode = SharingMode.Exclusive,
|
||||||
|
};
|
||||||
|
Check(vk.CreateBuffer(device, in bufferInfo, null, out var buffer), "vkCreateBuffer");
|
||||||
|
vk.GetBufferMemoryRequirements(device, buffer, out var requirements);
|
||||||
|
vk.GetPhysicalDeviceMemoryProperties(physical, out var memoryProperties);
|
||||||
|
|
||||||
|
uint memoryType = uint.MaxValue;
|
||||||
|
for (var index = 0; index < memoryProperties.MemoryTypeCount; index++)
|
||||||
|
{
|
||||||
|
var flags = memoryProperties.MemoryTypes[index].PropertyFlags;
|
||||||
|
if ((requirements.MemoryTypeBits & (1u << index)) != 0 &&
|
||||||
|
flags.HasFlag(MemoryPropertyFlags.HostVisibleBit) &&
|
||||||
|
flags.HasFlag(MemoryPropertyFlags.HostCoherentBit))
|
||||||
|
{
|
||||||
|
memoryType = (uint)index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memoryType == uint.MaxValue)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"no host-visible, host-coherent memory type available for the readback buffer");
|
||||||
|
}
|
||||||
|
|
||||||
|
var allocateInfo = new MemoryAllocateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.MemoryAllocateInfo,
|
||||||
|
AllocationSize = requirements.Size,
|
||||||
|
MemoryTypeIndex = memoryType,
|
||||||
|
};
|
||||||
|
Check(vk.AllocateMemory(device, in allocateInfo, null, out var memory), "vkAllocateMemory");
|
||||||
|
Check(vk.BindBufferMemory(device, buffer, memory, 0), "vkBindBufferMemory");
|
||||||
|
|
||||||
|
void* mapped;
|
||||||
|
Check(vk.MapMemory(device, memory, 0, BufferSize, 0, &mapped), "vkMapMemory");
|
||||||
|
var words = (uint*)mapped;
|
||||||
|
for (var index = 0; index < (int)(BufferSize / sizeof(uint)); index++)
|
||||||
|
{
|
||||||
|
words[index] = Sentinel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SharpEmu emits all guest buffers as one descriptor array at set 0,
|
||||||
|
// binding 0; this conformance shader uses a single buffer.
|
||||||
|
ShaderModule module;
|
||||||
|
fixed (byte* pCode = code)
|
||||||
|
{
|
||||||
|
var moduleInfo = new ShaderModuleCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.ShaderModuleCreateInfo,
|
||||||
|
CodeSize = (nuint)code.Length,
|
||||||
|
PCode = (uint*)pCode,
|
||||||
|
};
|
||||||
|
Check(vk.CreateShaderModule(device, in moduleInfo, null, out module), "vkCreateShaderModule");
|
||||||
|
}
|
||||||
|
|
||||||
|
var layoutBinding = new DescriptorSetLayoutBinding
|
||||||
|
{
|
||||||
|
Binding = 0,
|
||||||
|
DescriptorType = DescriptorType.StorageBuffer,
|
||||||
|
DescriptorCount = 1,
|
||||||
|
StageFlags = ShaderStageFlags.ComputeBit,
|
||||||
|
};
|
||||||
|
var setLayoutInfo = new DescriptorSetLayoutCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.DescriptorSetLayoutCreateInfo,
|
||||||
|
BindingCount = 1,
|
||||||
|
PBindings = &layoutBinding,
|
||||||
|
};
|
||||||
|
Check(
|
||||||
|
vk.CreateDescriptorSetLayout(device, in setLayoutInfo, null, out var setLayout),
|
||||||
|
"vkCreateDescriptorSetLayout");
|
||||||
|
|
||||||
|
var pipelineLayoutInfo = new PipelineLayoutCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.PipelineLayoutCreateInfo,
|
||||||
|
SetLayoutCount = 1,
|
||||||
|
PSetLayouts = &setLayout,
|
||||||
|
};
|
||||||
|
Check(
|
||||||
|
vk.CreatePipelineLayout(device, in pipelineLayoutInfo, null, out var pipelineLayout),
|
||||||
|
"vkCreatePipelineLayout");
|
||||||
|
|
||||||
|
var entryName = (byte*)SilkMarshal.StringToPtr("main");
|
||||||
|
var pipelineInfo = new ComputePipelineCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.ComputePipelineCreateInfo,
|
||||||
|
Stage = new PipelineShaderStageCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.PipelineShaderStageCreateInfo,
|
||||||
|
Stage = ShaderStageFlags.ComputeBit,
|
||||||
|
Module = module,
|
||||||
|
PName = entryName,
|
||||||
|
},
|
||||||
|
Layout = pipelineLayout,
|
||||||
|
};
|
||||||
|
Check(
|
||||||
|
vk.CreateComputePipelines(device, default, 1, in pipelineInfo, null, out var pipeline),
|
||||||
|
"vkCreateComputePipelines");
|
||||||
|
Console.WriteLine("driver accepted the SPIR-V (pipeline created)");
|
||||||
|
|
||||||
|
var poolSize = new DescriptorPoolSize
|
||||||
|
{
|
||||||
|
Type = DescriptorType.StorageBuffer,
|
||||||
|
DescriptorCount = 1,
|
||||||
|
};
|
||||||
|
var poolInfo = new DescriptorPoolCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.DescriptorPoolCreateInfo,
|
||||||
|
MaxSets = 1,
|
||||||
|
PoolSizeCount = 1,
|
||||||
|
PPoolSizes = &poolSize,
|
||||||
|
};
|
||||||
|
Check(vk.CreateDescriptorPool(device, in poolInfo, null, out var pool), "vkCreateDescriptorPool");
|
||||||
|
|
||||||
|
var setAllocateInfo = new DescriptorSetAllocateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.DescriptorSetAllocateInfo,
|
||||||
|
DescriptorPool = pool,
|
||||||
|
DescriptorSetCount = 1,
|
||||||
|
PSetLayouts = &setLayout,
|
||||||
|
};
|
||||||
|
Check(vk.AllocateDescriptorSets(device, in setAllocateInfo, out var descriptorSet), "vkAllocateDescriptorSets");
|
||||||
|
|
||||||
|
var descriptorBuffer = new DescriptorBufferInfo
|
||||||
|
{
|
||||||
|
Buffer = buffer,
|
||||||
|
Offset = 0,
|
||||||
|
Range = BufferSize,
|
||||||
|
};
|
||||||
|
var write = new WriteDescriptorSet
|
||||||
|
{
|
||||||
|
SType = StructureType.WriteDescriptorSet,
|
||||||
|
DstSet = descriptorSet,
|
||||||
|
DstBinding = 0,
|
||||||
|
DstArrayElement = 0,
|
||||||
|
DescriptorCount = 1,
|
||||||
|
DescriptorType = DescriptorType.StorageBuffer,
|
||||||
|
PBufferInfo = &descriptorBuffer,
|
||||||
|
};
|
||||||
|
vk.UpdateDescriptorSets(device, 1, in write, 0, null);
|
||||||
|
|
||||||
|
var commandPoolInfo = new CommandPoolCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.CommandPoolCreateInfo,
|
||||||
|
QueueFamilyIndex = computeFamily,
|
||||||
|
};
|
||||||
|
Check(vk.CreateCommandPool(device, in commandPoolInfo, null, out var commandPool), "vkCreateCommandPool");
|
||||||
|
|
||||||
|
var commandBufferInfo = new CommandBufferAllocateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.CommandBufferAllocateInfo,
|
||||||
|
CommandPool = commandPool,
|
||||||
|
Level = CommandBufferLevel.Primary,
|
||||||
|
CommandBufferCount = 1,
|
||||||
|
};
|
||||||
|
Check(vk.AllocateCommandBuffers(device, in commandBufferInfo, out var commandBuffer), "vkAllocateCommandBuffers");
|
||||||
|
|
||||||
|
var beginInfo = new CommandBufferBeginInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.CommandBufferBeginInfo,
|
||||||
|
};
|
||||||
|
Check(vk.BeginCommandBuffer(commandBuffer, in beginInfo), "vkBeginCommandBuffer");
|
||||||
|
vk.CmdBindPipeline(commandBuffer, PipelineBindPoint.Compute, pipeline);
|
||||||
|
vk.CmdBindDescriptorSets(
|
||||||
|
commandBuffer,
|
||||||
|
PipelineBindPoint.Compute,
|
||||||
|
pipelineLayout,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
in descriptorSet,
|
||||||
|
0,
|
||||||
|
null);
|
||||||
|
vk.CmdDispatch(commandBuffer, 1, 1, 1);
|
||||||
|
var barrier = new MemoryBarrier
|
||||||
|
{
|
||||||
|
SType = StructureType.MemoryBarrier,
|
||||||
|
SrcAccessMask = AccessFlags.ShaderWriteBit,
|
||||||
|
DstAccessMask = AccessFlags.HostReadBit,
|
||||||
|
};
|
||||||
|
vk.CmdPipelineBarrier(
|
||||||
|
commandBuffer,
|
||||||
|
PipelineStageFlags.ComputeShaderBit,
|
||||||
|
PipelineStageFlags.HostBit,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
in barrier,
|
||||||
|
0,
|
||||||
|
null,
|
||||||
|
0,
|
||||||
|
null);
|
||||||
|
Check(vk.EndCommandBuffer(commandBuffer), "vkEndCommandBuffer");
|
||||||
|
|
||||||
|
var submitInfo = new SubmitInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.SubmitInfo,
|
||||||
|
CommandBufferCount = 1,
|
||||||
|
PCommandBuffers = &commandBuffer,
|
||||||
|
};
|
||||||
|
Check(vk.QueueSubmit(queue, 1, in submitInfo, default), "vkQueueSubmit");
|
||||||
|
Check(vk.QueueWaitIdle(queue), "vkQueueWaitIdle");
|
||||||
|
|
||||||
|
var results = new (string Name, uint Actual, uint Expected)[]
|
||||||
|
{
|
||||||
|
("v_fmac_f32 fma(1.5, 2.25, 10.0)", words[0], expectedFma),
|
||||||
|
("v_mul_hi_i32 hi(0x7FFFFFFF*0x10003)", words[1], expectedHi),
|
||||||
|
("v_mul_lo_i32 lo(0x7FFFFFFF*0x10003)", words[2], expectedLo),
|
||||||
|
("exec=0 store suppressed (offset 12 sentinel)", words[3], Sentinel),
|
||||||
|
("store after exec restore (offset 16)", words[4], expectedRestored),
|
||||||
|
};
|
||||||
|
var failures = 0;
|
||||||
|
foreach (var (name, actual, expected) in results)
|
||||||
|
{
|
||||||
|
var status = actual == expected ? "PASS" : "FAIL";
|
||||||
|
if (actual != expected)
|
||||||
|
{
|
||||||
|
failures++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"{status} {name}: gpu=0x{actual:X8} expected=0x{expected:X8}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalWords = (int)(BufferSize / sizeof(uint));
|
||||||
|
var trailingClobbered = 0;
|
||||||
|
for (var index = results.Length; index < totalWords; index++)
|
||||||
|
{
|
||||||
|
if (words[index] != Sentinel)
|
||||||
|
{
|
||||||
|
trailingClobbered++;
|
||||||
|
Console.WriteLine(
|
||||||
|
$"FAIL trailing word [{index}] clobbered: gpu=0x{words[index]:X8} expected=0x{Sentinel:X8}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
failures += trailingClobbered;
|
||||||
|
if (trailingClobbered == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine(
|
||||||
|
$"PASS trailing words [{results.Length}..{totalWords - 1}] intact (sentinel)");
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine(failures == 0
|
||||||
|
? "RESULT: all values match"
|
||||||
|
: $"RESULT: {failures} mismatch(es)");
|
||||||
|
|
||||||
|
vk.DestroyCommandPool(device, commandPool, null);
|
||||||
|
vk.DestroyDescriptorPool(device, pool, null);
|
||||||
|
vk.DestroyPipeline(device, pipeline, null);
|
||||||
|
vk.DestroyPipelineLayout(device, pipelineLayout, null);
|
||||||
|
vk.DestroyDescriptorSetLayout(device, setLayout, null);
|
||||||
|
vk.DestroyShaderModule(device, module, null);
|
||||||
|
vk.UnmapMemory(device, memory);
|
||||||
|
vk.FreeMemory(device, memory, null);
|
||||||
|
vk.DestroyBuffer(device, buffer, null);
|
||||||
|
vk.DestroyDevice(device, null);
|
||||||
|
vk.DestroyInstance(instance, null);
|
||||||
|
|
||||||
|
Environment.ExitCode = failures == 0 ? 0 : 1;
|
||||||
|
|
||||||
|
static void Check(Result result, string what)
|
||||||
|
{
|
||||||
|
if (result != Result.Success)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"{what} failed: {result}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
-->
|
||||||
|
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
<!-- Standalone dev tool: opt out of the repo-wide lock-file requirement
|
||||||
|
so no packages.lock.json is generated or committed for it. -->
|
||||||
|
<RestorePackagesWithLockFile>false</RestorePackagesWithLockFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Silk.NET.Vulkan" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
// Feeds hand-assembled Gen5 (gfx10) instruction words through the real
|
// Feeds hand-assembled Gen5 (gfx10) instruction words through the real
|
||||||
// decode -> SPIR-V pipeline (Gen5ShaderTranslator / Gen5SpirvTranslator, via
|
// decode -> SPIR-V pipeline (Gen5ShaderTranslator / Gen5SpirvTranslator, via
|
||||||
// reflection so no emulator source changes are required) and writes the
|
// reflection so no emulator source changes are required) and writes the
|
||||||
// resulting vertex and compute SPIR-V blobs to disk. The blobs can then be
|
// resulting vertex, pixel, and compute SPIR-V blobs to disk. The blobs can then be
|
||||||
// checked with spirv-val / spirv-dis.
|
// checked with spirv-val / spirv-dis.
|
||||||
//
|
//
|
||||||
// Programs that contain buffer_store_dword automatically get a single
|
// Programs that contain buffer_store_dword automatically get a single
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
// guestBuffers[0] (descriptor set 0, binding 0).
|
// guestBuffers[0] (descriptor set 0, binding 0).
|
||||||
//
|
//
|
||||||
// Each program carries an expectation: ExpectTranslate=true programs must
|
// Each program carries an expectation: ExpectTranslate=true programs must
|
||||||
// decode and emit both stages; ExpectTranslate=false programs pin a decode
|
// decode and emit the requested stages; ExpectTranslate=false programs pin a decode
|
||||||
// failure that must stay loud. Any unexpected outcome makes the tool exit
|
// failure that must stay loud. Any unexpected outcome makes the tool exit
|
||||||
// non-zero, so it can gate scripts/CI.
|
// non-zero, so it can gate scripts/CI.
|
||||||
//
|
//
|
||||||
@@ -43,6 +43,63 @@ const ulong ProgramAddress = 0x100000;
|
|||||||
0xD56C0005, 0x00020501, // v_mul_hi_i32 v5, v1, v2
|
0xD56C0005, 0x00020501, // v_mul_hi_i32 v5, v1, v2
|
||||||
0xBF810000, // s_endpgm
|
0xBF810000, // s_endpgm
|
||||||
]),
|
]),
|
||||||
|
("mrt", true, [
|
||||||
|
0x7E0002FF, 0x3F800000, // v_mov_b32 v0, 1.0f
|
||||||
|
0x7E0202FF, 0x00000000, // v_mov_b32 v1, 0.0f
|
||||||
|
0x7E0402FF, 0x00000000, // v_mov_b32 v2, 0.0f
|
||||||
|
0x7E0602FF, 0x3F800000, // v_mov_b32 v3, 1.0f
|
||||||
|
0x7E0802FF, 0x00000001, // v_mov_b32 v4, 1u
|
||||||
|
0x7E0A02FF, 0x00000002, // v_mov_b32 v5, 2u
|
||||||
|
0x7E0C02FF, 0x00000003, // v_mov_b32 v6, 3u
|
||||||
|
0x7E0E02FF, 0x00000004, // v_mov_b32 v7, 4u
|
||||||
|
0x7E1002FF, 0xFFFFFFFF, // v_mov_b32 v8, -1
|
||||||
|
0x7E1202FF, 0x00000002, // v_mov_b32 v9, 2
|
||||||
|
0x7E1402FF, 0xFFFFFFFD, // v_mov_b32 v10, -3
|
||||||
|
0x7E1602FF, 0x00000004, // v_mov_b32 v11, 4
|
||||||
|
0xF800000F, 0x03020100, // exp mrt0 v0, v1, v2, v3
|
||||||
|
0xF800003F, 0x07060504, // exp mrt3 v4, v5, v6, v7
|
||||||
|
0xF800086F, 0x0B0A0908, // exp mrt6 v8, v9, v10, v11 done
|
||||||
|
0xBF810000, // s_endpgm
|
||||||
|
]),
|
||||||
|
("mrt-float2", true, [
|
||||||
|
0x7E0002FF, 0x3F800000, // v_mov_b32 v0, 1.0f
|
||||||
|
0x7E0202FF, 0x3E800000, // v_mov_b32 v1, 0.25f
|
||||||
|
0x7E0402FF, 0x3E800000, // v_mov_b32 v2, 0.25f
|
||||||
|
0x7E0602FF, 0x3F000000, // v_mov_b32 v3, 0.5f
|
||||||
|
0xF800000F, 0x03020100, // exp mrt0 v0, v1, v2, v3
|
||||||
|
0xF800081F, 0x03020100, // exp mrt1 v0, v1, v2, v3 done
|
||||||
|
0xBF810000, // s_endpgm
|
||||||
|
]),
|
||||||
|
("mrt8", true, [
|
||||||
|
0x7E0002FF, 0x3F800000, // v_mov_b32 v0, 1.0f
|
||||||
|
0x7E0202FF, 0x00000000, // v_mov_b32 v1, 0.0f
|
||||||
|
0x7E0402FF, 0x00000000, // v_mov_b32 v2, 0.0f
|
||||||
|
0x7E0602FF, 0x3F800000, // v_mov_b32 v3, 1.0f
|
||||||
|
0xF800000F, 0x03020100, // exp mrt0 v0, v1, v2, v3
|
||||||
|
0xF800001F, 0x03020100, // exp mrt1 v0, v1, v2, v3
|
||||||
|
0xF800002F, 0x03020100, // exp mrt2 v0, v1, v2, v3
|
||||||
|
0xF800003F, 0x03020100, // exp mrt3 v0, v1, v2, v3
|
||||||
|
0xF800004F, 0x03020100, // exp mrt4 v0, v1, v2, v3
|
||||||
|
0xF800005F, 0x03020100, // exp mrt5 v0, v1, v2, v3
|
||||||
|
0xF800006F, 0x03020100, // exp mrt6 v0, v1, v2, v3
|
||||||
|
0xF800087F, 0x03020100, // exp mrt7 v0, v1, v2, v3 done
|
||||||
|
0xBF810000, // s_endpgm
|
||||||
|
]),
|
||||||
|
("mrt-partial", true, [
|
||||||
|
0x7E0002FF, 0x3F4CCCCD, // v_mov_b32 v0, 0.8f
|
||||||
|
0x7E0202FF, 0x3F333333, // v_mov_b32 v1, 0.7f
|
||||||
|
0xF8000803, 0x03020100, // exp mrt0 v0, v1, off, off done
|
||||||
|
0xBF810000, // s_endpgm
|
||||||
|
]),
|
||||||
|
("mrt-partial-merge", true, [
|
||||||
|
0x7E0002FF, 0x3DCCCCCD, // v_mov_b32 v0, 0.1f
|
||||||
|
0x7E0202FF, 0x3E4CCCCD, // v_mov_b32 v1, 0.2f
|
||||||
|
0x7E0C02FF, 0x3E99999A, // v_mov_b32 v6, 0.3f
|
||||||
|
0x7E0E02FF, 0x3ECCCCCD, // v_mov_b32 v7, 0.4f
|
||||||
|
0xF8000003, 0x03020100, // exp mrt0 v0, v1, off, off
|
||||||
|
0xF800080C, 0x07060504, // exp mrt0 off, off, v6, v7 done
|
||||||
|
0xBF810000, // s_endpgm
|
||||||
|
]),
|
||||||
("sopp-hints", true, [
|
("sopp-hints", true, [
|
||||||
0xBFA10001, // s_clause 0x1
|
0xBFA10001, // s_clause 0x1
|
||||||
0xBFA30000, // s_waitcnt_depctr 0x0
|
0xBFA30000, // s_waitcnt_depctr 0x0
|
||||||
@@ -102,10 +159,18 @@ var imageBindingType = assembly.GetType("SharpEmu.Libs.Agc.Gen5ImageBinding")
|
|||||||
?? throw new InvalidOperationException("Gen5ImageBinding not found");
|
?? throw new InvalidOperationException("Gen5ImageBinding not found");
|
||||||
var globalBindingType = assembly.GetType("SharpEmu.Libs.Agc.Gen5GlobalMemoryBinding")
|
var globalBindingType = assembly.GetType("SharpEmu.Libs.Agc.Gen5GlobalMemoryBinding")
|
||||||
?? throw new InvalidOperationException("Gen5GlobalMemoryBinding not found");
|
?? throw new InvalidOperationException("Gen5GlobalMemoryBinding not found");
|
||||||
|
var pixelOutputBindingType = assembly.GetType("SharpEmu.Libs.Agc.Gen5PixelOutputBinding")
|
||||||
|
?? throw new InvalidOperationException("Gen5PixelOutputBinding not found");
|
||||||
|
var pixelOutputKindType = assembly.GetType("SharpEmu.Libs.Agc.Gen5PixelOutputKind")
|
||||||
|
?? throw new InvalidOperationException("Gen5PixelOutputKind not found");
|
||||||
var tryCompile = spirvTranslator.GetMethod(
|
var tryCompile = spirvTranslator.GetMethod(
|
||||||
"TryCompileVertexShader",
|
"TryCompileVertexShader",
|
||||||
BindingFlags.Public | BindingFlags.Static)
|
BindingFlags.Public | BindingFlags.Static)
|
||||||
?? throw new InvalidOperationException("Gen5SpirvTranslator.TryCompileVertexShader not found");
|
?? throw new InvalidOperationException("Gen5SpirvTranslator.TryCompileVertexShader not found");
|
||||||
|
var tryCompilePixel = spirvTranslator.GetMethods(BindingFlags.Public | BindingFlags.Static)
|
||||||
|
.Single(method =>
|
||||||
|
method.Name == "TryCompilePixelShader" &&
|
||||||
|
method.GetParameters()[2].ParameterType.IsGenericType);
|
||||||
var tryCompileCompute = spirvTranslator.GetMethod(
|
var tryCompileCompute = spirvTranslator.GetMethod(
|
||||||
"TryCompileComputeShader",
|
"TryCompileComputeShader",
|
||||||
BindingFlags.Public | BindingFlags.Static)
|
BindingFlags.Public | BindingFlags.Static)
|
||||||
@@ -201,8 +266,8 @@ foreach (var (name, expectTranslate, words) in testPrograms)
|
|||||||
null,
|
null,
|
||||||
null)!;
|
null)!;
|
||||||
|
|
||||||
object?[] compileArgs = [state, evaluation, null, null, 0, -1, 0];
|
var compileArgs = PadWithDefaults(tryCompile, [state, evaluation, null, null]);
|
||||||
if ((bool)tryCompile.Invoke(null, compileArgs)!)
|
if ((bool)tryCompile.Invoke(null, BindingFlags.OptionalParamBinding, null, compileArgs, null)!)
|
||||||
{
|
{
|
||||||
var shader = compileArgs[2]!;
|
var shader = compileArgs[2]!;
|
||||||
var spirv = (byte[])shader.GetType().GetProperty("Spirv")!.GetValue(shader)!;
|
var spirv = (byte[])shader.GetType().GetProperty("Spirv")!.GetValue(shader)!;
|
||||||
@@ -216,8 +281,8 @@ foreach (var (name, expectTranslate, words) in testPrograms)
|
|||||||
Console.WriteLine($"[{name}] emit: FAILED ({compileArgs[3]})");
|
Console.WriteLine($"[{name}] emit: FAILED ({compileArgs[3]})");
|
||||||
}
|
}
|
||||||
|
|
||||||
object?[] computeArgs = [state, evaluation, 1u, 1u, 1u, null, null];
|
var computeArgs = PadWithDefaults(tryCompileCompute, [state, evaluation, 1u, 1u, 1u, null, null]);
|
||||||
if ((bool)tryCompileCompute.Invoke(null, computeArgs)!)
|
if ((bool)tryCompileCompute.Invoke(null, BindingFlags.OptionalParamBinding, null, computeArgs, null)!)
|
||||||
{
|
{
|
||||||
var shader = computeArgs[5]!;
|
var shader = computeArgs[5]!;
|
||||||
var spirv = (byte[])shader.GetType().GetProperty("Spirv")!.GetValue(shader)!;
|
var spirv = (byte[])shader.GetType().GetProperty("Spirv")!.GetValue(shader)!;
|
||||||
@@ -230,6 +295,94 @@ foreach (var (name, expectTranslate, words) in testPrograms)
|
|||||||
failures++;
|
failures++;
|
||||||
Console.WriteLine($"[{name}] compute emit: FAILED ({computeArgs[6]})");
|
Console.WriteLine($"[{name}] compute emit: FAILED ({computeArgs[6]})");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name.StartsWith("mrt", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
(uint GuestSlot, uint HostLocation, string Kind)[] outputSpecs = name switch
|
||||||
|
{
|
||||||
|
"mrt" => new (uint GuestSlot, uint HostLocation, string Kind)[]
|
||||||
|
{
|
||||||
|
(0, 0, "Float"),
|
||||||
|
(3, 1, "Uint"),
|
||||||
|
(6, 2, "Sint"),
|
||||||
|
},
|
||||||
|
"mrt-float2" => [(0, 0, "Float"), (1, 1, "Float")],
|
||||||
|
"mrt8" => Enumerable.Range(0, 8)
|
||||||
|
.Select(index => ((uint)index, (uint)index, "Float"))
|
||||||
|
.ToArray(),
|
||||||
|
_ => [(0, 0, "Float")],
|
||||||
|
};
|
||||||
|
var pixelOutputs = Array.CreateInstance(pixelOutputBindingType, outputSpecs.Length);
|
||||||
|
for (var index = 0; index < outputSpecs.Length; index++)
|
||||||
|
{
|
||||||
|
var spec = outputSpecs[index];
|
||||||
|
pixelOutputs.SetValue(
|
||||||
|
Activator.CreateInstance(
|
||||||
|
pixelOutputBindingType,
|
||||||
|
spec.GuestSlot,
|
||||||
|
spec.HostLocation,
|
||||||
|
Enum.Parse(pixelOutputKindType, spec.Kind)),
|
||||||
|
index);
|
||||||
|
}
|
||||||
|
|
||||||
|
var pixelArgs = PadWithDefaults(
|
||||||
|
tryCompilePixel,
|
||||||
|
[state, evaluation, pixelOutputs, null, null]);
|
||||||
|
if ((bool)tryCompilePixel.Invoke(
|
||||||
|
null,
|
||||||
|
BindingFlags.OptionalParamBinding,
|
||||||
|
null,
|
||||||
|
pixelArgs,
|
||||||
|
null)!)
|
||||||
|
{
|
||||||
|
var shader = pixelArgs[3]!;
|
||||||
|
var spirv = (byte[])shader.GetType().GetProperty("Spirv")!.GetValue(shader)!;
|
||||||
|
var path = Path.Combine(outputDirectory, $"{name}-ps.spv");
|
||||||
|
File.WriteAllBytes(path, spirv);
|
||||||
|
Console.WriteLine($"[{name}] pixel emit: success, {spirv.Length} bytes -> {path}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
failures++;
|
||||||
|
Console.WriteLine($"[{name}] pixel emit: FAILED ({pixelArgs[4]})");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name == "mrt")
|
||||||
|
{
|
||||||
|
var invalidOutputs = Array.CreateInstance(pixelOutputBindingType, 2);
|
||||||
|
invalidOutputs.SetValue(
|
||||||
|
Activator.CreateInstance(
|
||||||
|
pixelOutputBindingType,
|
||||||
|
0u,
|
||||||
|
0u,
|
||||||
|
Enum.Parse(pixelOutputKindType, "Float")),
|
||||||
|
0);
|
||||||
|
invalidOutputs.SetValue(
|
||||||
|
Activator.CreateInstance(
|
||||||
|
pixelOutputBindingType,
|
||||||
|
3u,
|
||||||
|
7u,
|
||||||
|
Enum.Parse(pixelOutputKindType, "Float")),
|
||||||
|
1);
|
||||||
|
var invalidPixelArgs = PadWithDefaults(
|
||||||
|
tryCompilePixel,
|
||||||
|
[state, evaluation, invalidOutputs, null, null]);
|
||||||
|
if ((bool)tryCompilePixel.Invoke(
|
||||||
|
null,
|
||||||
|
BindingFlags.OptionalParamBinding,
|
||||||
|
null,
|
||||||
|
invalidPixelArgs,
|
||||||
|
null)!)
|
||||||
|
{
|
||||||
|
failures++;
|
||||||
|
Console.WriteLine("[mrt] FAILED: sparse host locations were accepted");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[mrt] sparse host locations rejected as expected ({invalidPixelArgs[4]})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine(failures == 0
|
Console.WriteLine(failures == 0
|
||||||
@@ -237,6 +390,37 @@ Console.WriteLine(failures == 0
|
|||||||
: $"RESULT: {failures} unexpected outcome(s)");
|
: $"RESULT: {failures} unexpected outcome(s)");
|
||||||
Environment.ExitCode = failures == 0 ? 0 : 1;
|
Environment.ExitCode = failures == 0 ? 0 : 1;
|
||||||
|
|
||||||
|
// Reflection Invoke does not apply C# default parameter values, so a newly
|
||||||
|
// added optional parameter on a translator entry point would otherwise throw
|
||||||
|
// TargetParameterCountException. Type.Missing + OptionalParamBinding lets the
|
||||||
|
// runtime substitute the declared defaults; only a new *required* parameter
|
||||||
|
// should force a tool update.
|
||||||
|
static object?[] PadWithDefaults(MethodInfo method, object?[] arguments)
|
||||||
|
{
|
||||||
|
var parameters = method.GetParameters();
|
||||||
|
if (arguments.Length > parameters.Length)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"{method.DeclaringType?.Name}.{method.Name} takes fewer parameters than the tool supplies");
|
||||||
|
}
|
||||||
|
|
||||||
|
var padded = new object?[parameters.Length];
|
||||||
|
arguments.CopyTo(padded, 0);
|
||||||
|
for (var i = arguments.Length; i < padded.Length; i++)
|
||||||
|
{
|
||||||
|
if (!parameters[i].IsOptional)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"{method.DeclaringType?.Name}.{method.Name} gained a required parameter " +
|
||||||
|
$"'{parameters[i].Name}' — the tool needs updating");
|
||||||
|
}
|
||||||
|
|
||||||
|
padded[i] = Type.Missing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return padded;
|
||||||
|
}
|
||||||
|
|
||||||
internal sealed class FakeMemory : ICpuMemory
|
internal sealed class FakeMemory : ICpuMemory
|
||||||
{
|
{
|
||||||
private readonly List<(ulong Base, byte[] Data)> _regions = [];
|
private readonly List<(ulong Base, byte[] Data)> _regions = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user