Compare commits

..

1 Commits

Author SHA1 Message Date
ParantezTech f69f81f682 [HLE] Add RandomExports HLE 2026-07-18 23:33:45 +03:00
87 changed files with 1204 additions and 7029 deletions
+26 -29
View File
@@ -89,6 +89,7 @@ jobs:
DOTNET_NOLOGO: true
NUGET_PACKAGES: ${{ github.workspace }}\.nuget\packages
PUBLISH_DIR: ${{ github.workspace }}\artifacts\publish\win-x64
RELEASE_DIR: ${{ github.workspace }}\artifacts\release
steps:
- name: Checkout repository
uses: actions/checkout@v6
@@ -120,13 +121,24 @@ jobs:
- 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}"
- name: Create release archive
run: |
New-Item -ItemType Directory -Path $env:RELEASE_DIR -Force | Out-Null
$archiveName = "sharpemu-${{ needs.init.outputs.version }}-win-x64.zip"
$archivePath = Join-Path $env:RELEASE_DIR $archiveName
if (Test-Path $archivePath) {
Remove-Item $archivePath -Force
}
Compress-Archive -Path (Join-Path $env:PUBLISH_DIR '*') -DestinationPath $archivePath -CompressionLevel Optimal
- name: Upload build artifact
uses: actions/upload-artifact@v7
with:
name: sharpemu-win-x64-${{ needs.init.outputs.short-sha }}
path: ${{ env.PUBLISH_DIR }}
path: ${{ env.RELEASE_DIR }}\sharpemu-${{ needs.init.outputs.version }}-win-x64.zip
if-no-files-found: error
include-hidden-files: true
build-posix:
name: Build ${{ matrix.rid }}
@@ -146,6 +158,7 @@ jobs:
DOTNET_NOLOGO: true
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
PUBLISH_DIR: ${{ github.workspace }}/artifacts/publish/${{ matrix.rid }}
RELEASE_DIR: ${{ github.workspace }}/artifacts/release
SPIRV_HEADERS_COMMIT: ad9184e76a66b1001c29db9b0a3e87f646c64de0
# SpirvModuleBuilder emits SPIR-V 1.5 and VulkanVideoPresenter requests Vulkan 1.2.
SPIRV_TARGET_ENV: vulkan1.2
@@ -210,13 +223,19 @@ jobs:
if: matrix.rid == 'osx-x64'
run: scripts/fetch-macos-moltenvk.sh "$PUBLISH_DIR"
- name: Create release archive
run: |
mkdir -p "$RELEASE_DIR"
# tar keeps the executable bit, which zip would drop.
tar -czf "$RELEASE_DIR/sharpemu-${{ needs.init.outputs.version }}-${{ matrix.rid }}.tar.gz" \
-C "$PUBLISH_DIR" .
- name: Upload build artifact
uses: actions/upload-artifact@v7
with:
name: sharpemu-${{ matrix.rid }}-${{ needs.init.outputs.short-sha }}
path: ${{ env.PUBLISH_DIR }}
path: ${{ env.RELEASE_DIR }}/sharpemu-${{ needs.init.outputs.version }}-${{ matrix.rid }}.tar.gz
if-no-files-found: error
include-hidden-files: true
release:
name: Publish GitHub Release
@@ -236,28 +255,6 @@ jobs:
with:
path: release
- name: Package release assets
shell: bash
env:
SHORT_SHA: ${{ needs.init.outputs.short-sha }}
VERSION: ${{ needs.init.outputs.version }}
run: |
set -euo pipefail
win_dir="release/sharpemu-win-x64-${SHORT_SHA}"
linux_dir="release/sharpemu-linux-x64-${SHORT_SHA}"
macos_dir="release/sharpemu-osx-x64-${SHORT_SHA}"
for package_dir in "${win_dir}" "${linux_dir}" "${macos_dir}"; do
test -d "${package_dir}"
done
mkdir -p release-assets
(cd "${win_dir}" && zip -q -r "../../release-assets/sharpemu-${VERSION}-win-x64.zip" .)
chmod +x "${linux_dir}/SharpEmu" "${macos_dir}/SharpEmu"
tar -czf "release-assets/sharpemu-${VERSION}-linux-x64.tar.gz" -C "${linux_dir}" .
tar -czf "release-assets/sharpemu-${VERSION}-osx-x64.tar.gz" -C "${macos_dir}" .
- name: Create release
shell: bash
env:
@@ -267,9 +264,9 @@ jobs:
RELEASE_TAG: ${{ needs.init.outputs.release-tag }}
VERSION: ${{ needs.init.outputs.version }}
run: |
mapfile -t assets < <(find release-assets -maxdepth 1 -type f \( -name '*.zip' -o -name '*.tar.gz' \) | sort)
if [ "${#assets[@]}" -ne 3 ]; then
echo "Expected 3 release assets, found ${#assets[@]}." >&2
mapfile -t assets < <(find release -type f \( -name '*.zip' -o -name '*.tar.gz' \) | sort)
if [ "${#assets[@]}" -eq 0 ]; then
echo "No release assets found." >&2
exit 1
fi
-1
View File
@@ -42,4 +42,3 @@ ehthumbs.db
.vs/
.idea/
.vscode/
+1 -1
View File
@@ -9,7 +9,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<SharpEmuVersion>0.0.2-beta.4</SharpEmuVersion>
<SharpEmuVersion>0.0.2-beta.3</SharpEmuVersion>
<Version>$(SharpEmuVersion)</Version>
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
-20
View File
@@ -1,20 +0,0 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
# Aerolib Catalog
```bash
# NID to export name
python scripts/aerolib_catalog.py lookup Zxa0VhQVTsk
# Export name to NID
python scripts/aerolib_catalog.py lookup sceKernelWaitSema
# Search export names
python scripts/aerolib_catalog.py search VideoOut --limit 20
# Export all NID/name pairs to artifacts/aerolib.txt
python scripts/aerolib_catalog.py export
```
+3 -3
View File
@@ -14,9 +14,9 @@ available, presents its decoded BGRA frames at the normal guest-flip boundary.
This preserves the game's own timing and lets the host Vulkan presenter display
the movie without trying to execute the PS5-specific Bink GPU decode path.
Without an adapter, Bink files remain visible to the guest and the game's
statically linked decoder runs normally. Set SHARPEMU_BINK_MODE=skip only when
explicitly testing a title whose cinematics are optional.
Without an adapter, Bink movies are skipped by default: their open call returns
not-found so games that mark cinematics as optional progress to their next
state instead of waiting on an empty Bink GPU texture.
Set SHARPEMU_BINK_MODE=dummy to retain the open and show a built-in,
non-decoded placeholder frame. This requires no SDK, but is a visual diagnostic
-57
View File
@@ -1,57 +0,0 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
# Guest write watch
`GuestWriteWatch` is an optional diagnostic tool. It helps you find managed
code and HLE code that damage guest memory. The tool starts only if you set one
or more `SHARPEMU_WATCH_*` environment variables.
The tool monitors writes through the SharpEmu managed virtual-memory APIs. It
does not monitor stores that native guest code makes directly. Use a platform
debugger or a hardware watchpoint to monitor these stores.
## Watch modes
- `SHARPEMU_WATCH_WRITE=0x<address>` logs a write that overlaps the eight-byte
block at the specified guest address.
- `SHARPEMU_WATCH_POOL_HEADER=1` monitors the pointer at offset `0x40`. It
monitors the first 64 direct mappings that have a size of 64 KiB and
protection value `0xF2`.
- `SHARPEMU_WATCH_VALUE_PATTERN=1` logs an eight-byte write if its lower 32 bits
are `1`. The upper 32 bits must look like a small guest-pointer prefix.
- `SHARPEMU_WATCH_VALUE1=1` logs short writes of value `1` in the high guest
memory range. The tool logs a maximum of 128 entries for each process.
- `SHARPEMU_WATCH_BULK_TORN=1` scans aligned 64-bit words in bulk writes. It
finds damaged pointer patterns and byte-shifted pointer patterns. The tool
logs a maximum of 64 entries for each process.
- `SHARPEMU_WATCH_BULK_DEST_HI=0x<high-dword>` scans only writes that have the
specified upper 32 bits in the destination address.
For each match, the tool logs the destination address, the data pattern, and the
managed call stack. The log uses the `watch_write` or `watch_bulk_torn` warning
tag.
Use these variables together to scan bulk writes in the
`0x00000080xxxxxxxx` region.
macOS and Linux:
```sh
SHARPEMU_WATCH_BULK_TORN=1 \
SHARPEMU_WATCH_BULK_DEST_HI=0x80 \
SharpEmu /path/to/eboot.bin
```
Windows PowerShell:
```powershell
$env:SHARPEMU_WATCH_BULK_TORN = "1"
$env:SHARPEMU_WATCH_BULK_DEST_HI = "0x80"
& .\SharpEmu.exe C:\path\to\game\eboot.bin
```
To reduce unnecessary log entries, use an exact `SHARPEMU_WATCH_WRITE`
address from a crash dump.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "10.0.103",
"rollForward": "latestFeature"
"rollForward": "disable"
}
}
-181
View File
@@ -1,181 +0,0 @@
#!/usr/bin/env python3
# Copyright (C) 2026 SharpEmu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
from __future__ import annotations
import argparse
import base64
import hashlib
import re
import sys
from pathlib import Path
NID_SUFFIX = bytes.fromhex("518d64a635ded8c1e6b039b1c3e55230")
NID_PATTERN = re.compile(r"^[A-Za-z0-9+-]{11}$")
DEFAULT_NAMES_FILE = Path(__file__).resolve().with_name("ps5_names.txt")
DEFAULT_EXPORT_FILE = Path(__file__).resolve().parents[1] / "artifacts" / "aerolib.txt"
def compute_nid(export_name: str) -> str:
digest = hashlib.sha1(export_name.encode("utf-8") + NID_SUFFIX).digest()
encoded = base64.b64encode(digest[:8][::-1]).decode("ascii")
return encoded.rstrip("=").replace("/", "-")
def read_names(path: Path) -> list[str]:
try:
return [
line.strip()
for line in path.read_text(encoding="utf-8").splitlines()
if line.strip()
]
except OSError as error:
raise SystemExit(f"Unable to read catalog '{path}': {error}") from error
def write_pair(nid: str, export_name: str) -> None:
print(f"{nid}\t{export_name}")
def lookup(args: argparse.Namespace) -> int:
value = args.value.strip()
if NID_PATTERN.fullmatch(value):
for export_name in read_names(args.names):
if compute_nid(export_name) == value:
write_pair(value, export_name)
return 0
print(f"NID not found in catalog: {value}", file=sys.stderr)
return 1
names = set(read_names(args.names))
write_pair(compute_nid(value), value)
if value not in names:
print("Warning: export name is not present in the catalog.", file=sys.stderr)
return 0
def search(args: argparse.Namespace) -> int:
names = read_names(args.names)
if args.regex:
try:
pattern = re.compile(args.query, 0 if args.case_sensitive else re.IGNORECASE)
except re.error as error:
print(f"Invalid regular expression: {error}", file=sys.stderr)
return 2
matches = (name for name in names if pattern.search(name))
elif args.case_sensitive:
matches = (name for name in names if args.query in name)
else:
query = args.query.casefold()
matches = (name for name in names if query in name.casefold())
count = 0
for export_name in matches:
write_pair(compute_nid(export_name), export_name)
count += 1
if args.limit and count >= args.limit:
break
if count == 0:
print(f"No catalog names matched: {args.query}", file=sys.stderr)
return 1
return 0
def export_catalog(args: argparse.Namespace) -> int:
pairs = [(compute_nid(name), name) for name in read_names(args.names)]
if args.sort == "nid":
pairs.sort(key=lambda pair: (pair[0], pair[1]))
elif args.sort == "name":
pairs.sort(key=lambda pair: pair[1])
args.output.parent.mkdir(parents=True, exist_ok=True)
try:
with args.output.open("w", encoding="utf-8", newline="\n") as output:
output.write("# NID\tExportName\n")
for nid, export_name in pairs:
output.write(f"{nid}\t{export_name}\n")
except OSError as error:
print(f"Unable to write catalog '{args.output}': {error}", file=sys.stderr)
return 1
print(f"Wrote {len(pairs)} entries to {args.output}")
return 0
def create_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Inspect the SharpEmu PS5 export-name/NID catalog.",
epilog=(
"Examples:\n"
" python scripts/aerolib_catalog.py lookup Zxa0VhQVTsk\n"
" python scripts/aerolib_catalog.py lookup sceKernelWaitSema\n"
" python scripts/aerolib_catalog.py search VideoOut --limit 20\n"
" python scripts/aerolib_catalog.py export"
),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--names",
type=Path,
default=DEFAULT_NAMES_FILE,
help=f"source name list (default: {DEFAULT_NAMES_FILE})",
)
subparsers = parser.add_subparsers(dest="command", required=True)
lookup_parser = subparsers.add_parser(
"lookup", help="resolve a NID or calculate the NID for an export name"
)
lookup_parser.add_argument("value", help="11-character NID or exact export name")
lookup_parser.set_defaults(handler=lookup)
search_parser = subparsers.add_parser(
"search", help="find export names and print matching NID/name pairs"
)
search_parser.add_argument("query", help="name substring or regular expression")
search_parser.add_argument(
"--limit", type=int, default=50, help="maximum matches; 0 means unlimited"
)
search_parser.add_argument(
"--case-sensitive", action="store_true", help="match case exactly"
)
search_parser.add_argument(
"--regex", action="store_true", help="treat the query as a regular expression"
)
search_parser.set_defaults(handler=search)
export_parser = subparsers.add_parser(
"export", help="write every NID/name pair to a tab-separated text file"
)
export_parser.add_argument(
"output",
type=Path,
nargs="?",
default=DEFAULT_EXPORT_FILE,
help=f"output file (default: {DEFAULT_EXPORT_FILE})",
)
export_parser.add_argument(
"--sort",
choices=("source", "nid", "name"),
default="nid",
help="output ordering (default: nid)",
)
export_parser.set_defaults(handler=export_catalog)
return parser
def main() -> int:
parser = create_parser()
args = parser.parse_args()
return args.handler(args)
if __name__ == "__main__":
raise SystemExit(main())
+7 -2
View File
@@ -45,6 +45,11 @@ internal static partial class Program
[STAThread]
private static int Main(string[] args)
{
// Avoid blocking full collections while guest and render threads are
// running, and establish the GC mode before the runtime reserves the
// fixed guest address-space window.
System.Runtime.GCSettings.LatencyMode = System.Runtime.GCLatencyMode.SustainedLowLatency;
try
{
return Run(args);
@@ -607,7 +612,7 @@ internal static partial class Program
nint jobHandle = 0;
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, "1");
var created = CreateProcessW(
null,
processPath,
cmdLineBuilder,
0,
0,
@@ -1433,7 +1438,7 @@ internal static partial class Program
[DllImport("kernel32.dll", EntryPoint = "CreateProcessW", SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CreateProcessW(
string? applicationName,
string applicationName,
StringBuilder commandLine,
nint processAttributes,
nint threadAttributes,
+1
View File
@@ -49,6 +49,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'win-x64' Or '$(RuntimeIdentifier)' == ''">
-5
View File
@@ -13,9 +13,4 @@ SPDX-License-Identifier: GPL-2.0-or-later
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<asmv3:application xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<asmv3:windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</asmv3:windowsSettings>
</asmv3:application>
</assembly>
+603
View File
@@ -0,0 +1,603 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[10.0.8, )",
"resolved": "10.0.8",
"contentHash": "dVbSXGIFNR5nZcv2tOLoWI+a9T4jtFd77IYjuND+QVe360qWgAF7H0WtoopYhRw/+SgpGUTyrkrh+65+ClNnfw=="
},
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
"resolved": "2.1.25547.20250602",
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
},
"Avalonia.BuildServices": {
"type": "Transitive",
"resolved": "11.3.2",
"contentHash": "qHDToxto1e3hci5YqbG9n0Ty8mlp3zBUN5wT66wKqaDVzXyQ0do3EnRILd4Ke9jpvsktaPpgE0YjEk7hornryQ=="
},
"Avalonia.FreeDesktop": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "aUwv8BNruRUOaUfMu4U3uibIUS60/rSHgGOhd8zBkLkpxY3JFJvgRbeq5ZzHIyKXCuKi18PO00YHAgCarp3wdw==",
"dependencies": {
"Avalonia": "11.3.18",
"Tmds.DBus.Protocol": "0.21.3"
}
},
"Avalonia.Native": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"Avalonia.Remote.Protocol": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "vw+6ZfgTuu72dA9aVWn6u56t2nrBd5MoMU0wo/qI9XJAl/c0oYYphIvwLvJP1JorubQY4UE3d0ac8ULBhrGBiA=="
},
"Avalonia.Skia": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "/B4aXmNRNjG8I5U/a1xJI+bIi0XO6DDzS3mBrIKlVnJRY2CyZiUeESRQXLnIU77Z9TvqkUROs+D47s085YjFtA==",
"dependencies": {
"Avalonia": "11.3.18",
"HarfBuzzSharp": "8.3.1.1",
"HarfBuzzSharp.NativeAssets.Linux": "8.3.1.1",
"HarfBuzzSharp.NativeAssets.WebAssembly": "8.3.1.1",
"SkiaSharp": "2.88.9",
"SkiaSharp.NativeAssets.Linux": "2.88.9",
"SkiaSharp.NativeAssets.WebAssembly": "2.88.9"
}
},
"Avalonia.Win32": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "eioUHkM2PeLPETd1aEks3rvb9plbba6buIrNdrqCpwE/qgHKUjvRNBd5mUQfAbGgTLiAes524gB8uUMDhrsJVQ==",
"dependencies": {
"Avalonia": "11.3.18",
"Avalonia.Angle.Windows.Natives": "2.1.25547.20250602"
}
},
"Avalonia.X11": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "m4Ki/G5Dovnq+6QzfS0iGbK8V77Q6oTjToMLOB0CxPCCrl3Oxywh6kIjuGJDPaN6kopMmjxlNShyQf+vPYL+JA==",
"dependencies": {
"Avalonia": "11.3.18",
"Avalonia.FreeDesktop": "11.3.18",
"Avalonia.Skia": "11.3.18"
}
},
"HarfBuzzSharp": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "tLZN66oe/uiRPTZfrCU4i8ScVGwqHNh5MHrXj0yVf4l7Mz0FhTGnQ71RGySROTmdognAs0JtluHkL41pIabWuQ==",
"dependencies": {
"HarfBuzzSharp.NativeAssets.Win32": "8.3.1.1",
"HarfBuzzSharp.NativeAssets.macOS": "8.3.1.1"
}
},
"HarfBuzzSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
},
"HarfBuzzSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
},
"HarfBuzzSharp.NativeAssets.WebAssembly": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "loJweK2u/mH/3C2zBa0ggJlITIszOkK64HLAZB7FUT670dTg965whLFYHDQo69NmC4+d9UN0icLC9VHidXaVCA=="
},
"HarfBuzzSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
},
"MicroCom.Runtime": {
"type": "Transitive",
"resolved": "0.11.0",
"contentHash": "MEnrZ3UIiH40hjzMDsxrTyi8dtqB5ziv3iBeeU4bXsL/7NLSal9F1lZKpK+tfBRnUoDSdtcW3KufE4yhATOMCA=="
},
"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=="
},
"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.Input.Common": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "QbJVV7kFBHEByayXCYdJtXXI9Sp4a+QAf0IdGV6uCWkFYcEmqBYW3aaNGvFOdSwTBDbHL5T/OtOCrGh4qYhk7A==",
"dependencies": {
"Silk.NET.Windowing.Common": "2.23.0"
}
},
"Silk.NET.Input.Glfw": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "KGHYqsv/IQRJtD6dloYh2tN4CkaM40vxM2kj0cGKBoCQiBDYHHhJiyDTyMPx0W7Fz5IgnhnG42ELmIAa0DH69A==",
"dependencies": {
"Silk.NET.Input.Common": "2.23.0",
"Silk.NET.Windowing.Glfw": "2.23.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"
}
},
"SkiaSharp": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "3MD5VHjXXieSHCleRLuaTXmL2pD0mB7CcOB1x2kA1I4bhptf4e3R27iM93264ZYuAq6mkUyX5XbcxnZvMJYc1Q==",
"dependencies": {
"SkiaSharp.NativeAssets.Win32": "2.88.9",
"SkiaSharp.NativeAssets.macOS": "2.88.9"
}
},
"SkiaSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
"dependencies": {
"SkiaSharp": "2.88.9"
}
},
"SkiaSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
},
"SkiaSharp.NativeAssets.WebAssembly": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "kt06RccBHSnAs2wDYdBSfsjIDbY3EpsOVqnlDgKdgvyuRA8ZFDaHRdWNx1VHjGgYzmnFCGiTJBnXFl5BqGwGnA=="
},
"SkiaSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
},
"Ultz.Native.GLFW": {
"type": "Transitive",
"resolved": "3.4.0",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
},
"sharpemu.core": {
"type": "Project",
"dependencies": {
"Iced": "[1.21.0, )",
"SharpEmu.HLE": "[0.0.2-beta.3, )",
"SharpEmu.Libs": "[0.0.2-beta.3, )",
"SharpEmu.Logging": "[0.0.2-beta.3, )"
}
},
"sharpemu.debugger": {
"type": "Project",
"dependencies": {
"SharpEmu.Core": "[0.0.2-beta.3, )",
"SharpEmu.HLE": "[0.0.2-beta.3, )",
"SharpEmu.Logging": "[0.0.2-beta.3, )"
}
},
"sharpemu.gui": {
"type": "Project",
"dependencies": {
"Avalonia": "[11.3.18, )",
"Avalonia.Desktop": "[11.3.18, )",
"Avalonia.Fonts.Inter": "[11.3.18, )",
"Avalonia.Themes.Fluent": "[11.3.18, )",
"SharpEmu.Core": "[0.0.2-beta.3, )",
"SharpEmu.Libs": "[0.0.2-beta.3, )",
"SharpEmu.Logging": "[0.0.2-beta.3, )",
"Tmds.DBus.Protocol": "[0.21.3, )"
}
},
"sharpemu.hle": {
"type": "Project",
"dependencies": {
"SharpEmu.Logging": "[0.0.2-beta.3, )"
}
},
"sharpemu.libs": {
"type": "Project",
"dependencies": {
"SharpEmu.HLE": "[0.0.2-beta.3, )",
"SharpEmu.ShaderCompiler": "[0.0.2-beta.3, )",
"SharpEmu.ShaderCompiler.Metal": "[0.0.2-beta.3, )",
"SharpEmu.ShaderCompiler.Vulkan": "[0.0.2-beta.3, )",
"Silk.NET.Input": "[2.23.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"
},
"sharpemu.shadercompiler": {
"type": "Project",
"dependencies": {
"SharpEmu.HLE": "[0.0.2-beta.3, )"
}
},
"sharpemu.shadercompiler.metal": {
"type": "Project",
"dependencies": {
"SharpEmu.ShaderCompiler": "[0.0.2-beta.3, )"
}
},
"sharpemu.shadercompiler.vulkan": {
"type": "Project",
"dependencies": {
"SharpEmu.ShaderCompiler": "[0.0.2-beta.3, )"
}
},
"Avalonia": {
"type": "CentralTransitive",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "2C4UxhWUObWGgYKWic1x5BMMWGJP6SElb91WeOxs+X/iR26rtkqpxFFwwo50FXS9AyYnHfk8QKXDEfe7oT/kZA==",
"dependencies": {
"Avalonia.BuildServices": "11.3.2",
"Avalonia.Remote.Protocol": "11.3.18",
"MicroCom.Runtime": "0.11.0"
}
},
"Avalonia.Desktop": {
"type": "CentralTransitive",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "bilMPa5vYiis6fbNovb6esKytBnOCEGojBa1XFegLCRHCP6g6PvZwS0XF/YOAGkENRlHG8dI7lohOpQ9bIkq1g==",
"dependencies": {
"Avalonia": "11.3.18",
"Avalonia.Native": "11.3.18",
"Avalonia.Skia": "11.3.18",
"Avalonia.Win32": "11.3.18",
"Avalonia.X11": "11.3.18"
}
},
"Avalonia.Fonts.Inter": {
"type": "CentralTransitive",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "27u6hB3Y2Ue586yjfeVakberY73VNQXtuKwe/P927XG1QPlhsfmOyifLHDDpSHG85Zl1x/Xv9IZ3+tk9FnjcZQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"Avalonia.Themes.Fluent": {
"type": "CentralTransitive",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "+Q/TJoynD0zNuu5w2gD+xcTl7GNKJFxlPYAndRLs/mTDrNbbsvv/271WyIysbMPsXSjCyBDp7RCZzQkpD6x5Bg==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"Iced": {
"type": "CentralTransitive",
"requested": "[1.21.0, )",
"resolved": "1.21.0",
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
},
"Silk.NET.Input": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "Xzl+tVwAp2eEd8blmGQjmJrsZPnp3PWG0KJjiAQHaY2Zr/ELVWeAROKXmZdCAvexzmte2JVGEy/dxnMycbxlpg==",
"dependencies": {
"Silk.NET.Input.Common": "2.23.0",
"Silk.NET.Input.Glfw": "2.23.0"
}
},
"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"
}
},
"Tmds.DBus.Protocol": {
"type": "CentralTransitive",
"requested": "[0.21.3, )",
"resolved": "0.21.3",
"contentHash": "hDwB8WsQoyALQKqIbwzS68UKdlnafDm4T/DkO/JrA/YIneP/rKv96SxYPVXeh3FP4i/SXfShrYftKLtciJAIlw=="
}
},
"net10.0/linux-x64": {
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
"resolved": "2.1.25547.20250602",
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
},
"Avalonia.Native": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"HarfBuzzSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
},
"HarfBuzzSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
},
"HarfBuzzSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
},
"SkiaSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
"dependencies": {
"SkiaSharp": "2.88.9"
}
},
"SkiaSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
},
"SkiaSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
},
"Ultz.Native.GLFW": {
"type": "Transitive",
"resolved": "3.4.0",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
}
},
"net10.0/osx-arm64": {
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
"resolved": "2.1.25547.20250602",
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
},
"Avalonia.Native": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"HarfBuzzSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
},
"HarfBuzzSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
},
"HarfBuzzSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
},
"SkiaSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
"dependencies": {
"SkiaSharp": "2.88.9"
}
},
"SkiaSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
},
"SkiaSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
},
"Ultz.Native.GLFW": {
"type": "Transitive",
"resolved": "3.4.0",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
}
},
"net10.0/osx-x64": {
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
"resolved": "2.1.25547.20250602",
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
},
"Avalonia.Native": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"HarfBuzzSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
},
"HarfBuzzSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
},
"HarfBuzzSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
},
"SkiaSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
"dependencies": {
"SkiaSharp": "2.88.9"
}
},
"SkiaSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
},
"SkiaSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
},
"Ultz.Native.GLFW": {
"type": "Transitive",
"resolved": "3.4.0",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
}
},
"net10.0/win-x64": {
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
"resolved": "2.1.25547.20250602",
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
},
"Avalonia.Native": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"HarfBuzzSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
},
"HarfBuzzSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
},
"HarfBuzzSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
},
"SkiaSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
"dependencies": {
"SkiaSharp": "2.88.9"
}
},
"SkiaSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
},
"SkiaSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
},
"Ultz.Native.GLFW": {
"type": "Transitive",
"resolved": "3.4.0",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
}
}
}
}
@@ -1,71 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Core.Cpu.Emulation;
/// <summary>
/// Pure software implementation of the bit-field math behind AMD's SSE4a EXTRQ/INSERTQ
/// (immediate-form) instructions.
///
/// The direct-execution backend runs guest PS5 code natively on the host CPU. The PS5's Zen 2
/// cores implement AMD-only SSE4a (EXTRQ/INSERTQ), but Intel hosts - and Rosetta 2 on Apple
/// Silicon - do not, so they raise #UD (STATUS_ILLEGAL_INSTRUCTION) instead of executing the
/// opcode. SharpEmu already rewrites one specific compiled EXTRQ+VPBLENDD idiom at load time
/// (see <see cref="Native.Sse4aExtrqBlendPatch"/>), but any other occurrence of EXTRQ/INSERTQ -
/// a different register allocation, a title built with a different compiler version, and so on
/// - still aborts the title. This class ported from Kyty's
/// <c>Loader::X64InstructionEmulator::TryEmulateSse4a</c> provides the general bit-field
/// extract/insert so the illegal-instruction handler can finish *any* immediate-form
/// EXTRQ/INSERTQ in software and resume, instead of relying on a single hard-coded byte pattern.
///
/// The methods operate on plain 64-bit integers rather than the OS CONTEXT record so the bit
/// math can be unit-tested in isolation; the unsafe CONTEXT/XMM plumbing lives in the backend
/// adapter (<see cref="Native.DirectExecutionBackend"/>).
/// </summary>
public static class Sse4aBitFieldEmulator
{
public static bool IsValidBitField(int length, int index)
{
var len = length & 0x3F;
var idx = index & 0x3F;
return (len != 0 || idx == 0) && (len == 0 ? idx == 0 : idx + len <= 64);
}
public static ulong ExtractBitField(ulong value, int length, int index)
{
var len = length & 0x3F;
var idx = index & 0x3F;
if (!IsValidBitField(length, index))
{
return 0;
}
if (len == 0)
{
return value;
}
var mask = len == 64 ? ulong.MaxValue : (1UL << len) - 1;
return (value >> idx) & mask;
}
public static ulong InsertBitField(ulong destination, ulong source, int length, int index)
{
var len = length & 0x3F;
var idx = index & 0x3F;
if (!IsValidBitField(length, index))
{
return destination;
}
if (len == 0)
{
return source;
}
var fieldMask = len == 64 ? ulong.MaxValue : (1UL << len) - 1;
var destinationClearMask = fieldMask << idx;
var sourceField = (source & fieldMask) << idx;
return (destination & ~destinationClearMask) | sourceField;
}
}
@@ -1,197 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Threading;
using Iced.Intel;
using SharpEmu.Core.Cpu.Emulation;
namespace SharpEmu.Core.Cpu.Native;
// General software fallback for the AMD-only instructions PS5 titles occasionally emit that a
// Zen 2-only host implements but Intel hosts (and Rosetta 2 on Apple Silicon) do not:
// - SSE4a EXTRQ/INSERTQ, immediate form
// - MONITORX/MWAITX
//
// This is a direct port of Kyty's Loader::X64InstructionEmulator (TryEmulateSse4a /
// TryEmulateMonitorxMwaitx). SharpEmu already special-cases exactly one compiled EXTRQ+VPBLENDD
// byte sequence at load time (Sse4aExtrqBlendPatch), which only helps the one idiom it was
// reverse-engineered from. This file is a general, fault-time fallback that engages for any
// immediate-form EXTRQ/INSERTQ or MONITORX/MWAITX the narrower patch (or a title using a
// different compiler/register allocation) does not cover, complementing rather than replacing
// it: the load-time patch still avoids paying the fault-and-recover cost on the hot path it was
// built for, while this method is the safety net for everything else.
//
// This is deliberately additive: DirectExecutionBackend.IllegalInstruction.cs (the BMI1/BMI2/ABM
// fallback) is untouched, and this method is only reached from VectoredHandler after that one
// has already declined to handle the fault.
public sealed partial class DirectExecutionBackend
{
// Byte offset of Xmm0 within the Win64 CONTEXT record: FltSave (the XMM_SAVE_AREA32/FXSAVE
// image) starts right after Rip at offset 256, and XmmRegisters[0] sits 160 bytes into that
// area (32-byte header + 8 legacy x87/MMX slots x 16 bytes). 256 + 160 = 416 (0x1A0). Cross-
// checked against this file's own Win64ContextSize (0x4D0): rebuilding the whole CONTEXT
// layout field-by-field from offset 0 lands on the same 0x4D0 total, which would not happen
// if this offset (or anything before it) were wrong.
private const int Win64ContextXmm0Offset = 0x1A0;
private static int _sse4aSoftwareFallbackAnnounced;
private static long _sse4aInstructionsEmulated;
private static int _monitorxSoftwareFallbackAnnounced;
private static long _monitorxInstructionsEmulated;
private unsafe bool TryRecoverAmdCompatInstruction(void* contextRecord, ulong rip)
{
if (TryRecoverMonitorxMwaitx(contextRecord, rip))
{
return true;
}
// MONITORX/MWAITX above only ever reads guest code memory and rewrites RIP, both of
// which the POSIX signal bridge (DirectExecutionBackend.PosixSignals.cs) faithfully
// round-trips through the real ucontext, so it works on every supported OS. EXTRQ/
// INSERTQ additionally read and write an XMM register: on Windows contextRecord is the
// live CONTEXT the OS resumes the thread from, so touching the Xmm0.. slots is visible
// to the guest, but on POSIX contextRecord is a CONTEXT-shaped scratch buffer that the
// bridge only populates with the 17 general-purpose registers - the XMM region is never
// read from or written back to the real mcontext/ucontext. Running this on POSIX would
// silently compute a result from stale/zeroed XMM bytes and then discard whatever it
// "wrote", so keep it Windows-only, matching Kyty's own scope for the identical fix.
return OperatingSystem.IsWindows() && TryRecoverSse4aExtractInsert(contextRecord, rip);
}
private unsafe bool TryRecoverMonitorxMwaitx(void* contextRecord, ulong rip)
{
// MONITORX (0F 01 FA) and MWAITX (0F 01 FB) are fixed 3-byte encodings with no
// ModRM/SIB/displacement/immediate, so a raw byte compare is sufficient and unambiguous.
var opcode = new byte[3];
if (!TryReadHostBytes(rip, opcode) ||
opcode[0] != 0x0F || opcode[1] != 0x01 || (opcode[2] != 0xFA && opcode[2] != 0xFB))
{
return false;
}
// PS5 titles use this pair in idle/wait loops: MONITORX arms a monitor on a cache line
// and MWAITX blocks until that line is written (or a timeout elapses). Hosts without
// the extension raise #UD on either one. We do not model the monitor itself, only its
// observable effect on guest forward progress: MONITORX becomes a no-op (arming a
// watch we never honour has no side effect of its own) and MWAITX becomes a plain
// thread yield, i.e. treat the awaited condition as already satisfied so the guest
// loop keeps making progress instead of executing an illegal opcode forever.
if (opcode[2] == 0xFB)
{
Thread.Yield();
}
WriteCtxU64(contextRecord, CTX_RIP, rip + 3);
Interlocked.Increment(ref _monitorxInstructionsEmulated);
if (Interlocked.Exchange(ref _monitorxSoftwareFallbackAnnounced, 1) == 0)
{
Console.Error.WriteLine(
"[LOADER][INFO] Host lacks AMD MONITORX/MWAITX used by the guest; " +
"emulating those instructions in software.");
}
return true;
}
private unsafe bool TryRecoverSse4aExtractInsert(void* contextRecord, ulong rip)
{
if (!OperatingSystem.IsWindows() || !TryReadFaultingInstruction(rip, out var instruction))
{
return false;
}
var isExtrq = instruction.Mnemonic == Mnemonic.Extrq;
var isInsertq = instruction.Mnemonic == Mnemonic.Insertq;
if (!isExtrq && !isInsertq)
{
return false;
}
if (isExtrq && instruction.OpCount != 3 || isInsertq && instruction.OpCount != 4)
{
return false;
}
if (instruction.GetOpKind(0) != OpKind.Register ||
!TryGetXmmOffset(instruction.GetOpRegister(0), out var destOffset))
{
return false;
}
var destLow = ReadCtxU64(contextRecord, destOffset);
if (isExtrq)
{
var length = (int)instruction.GetImmediate(1);
var index = (int)instruction.GetImmediate(2);
if (!Sse4aBitFieldEmulator.IsValidBitField(length, index))
{
return false;
}
WriteCtxU64(contextRecord, destOffset, Sse4aBitFieldEmulator.ExtractBitField(destLow, length, index));
WriteCtxU64(contextRecord, destOffset + 8, 0);
}
else
{
if (instruction.GetOpKind(1) != OpKind.Register ||
!TryGetXmmOffset(instruction.GetOpRegister(1), out var srcOffset))
{
return false;
}
var length = (int)instruction.GetImmediate(2);
var index = (int)instruction.GetImmediate(3);
if (!Sse4aBitFieldEmulator.IsValidBitField(length, index))
{
return false;
}
WriteCtxU64(contextRecord, destOffset, Sse4aBitFieldEmulator.InsertBitField(
destLow, ReadCtxU64(contextRecord, srcOffset), length, index));
WriteCtxU64(contextRecord, destOffset + 8, 0);
}
WriteCtxU64(contextRecord, CTX_RIP, rip + (ulong)instruction.Length);
Interlocked.Increment(ref _sse4aInstructionsEmulated);
if (Interlocked.Exchange(ref _sse4aSoftwareFallbackAnnounced, 1) == 0)
{
Console.Error.WriteLine(
"[LOADER][INFO] Host lacks SSE4a EXTRQ/INSERTQ used by the guest; " +
"emulating those instructions in software.");
}
return true;
}
// Maps an Iced XMM register to its byte offset in the Win64 CONTEXT record. Written as an
// explicit switch (rather than arithmetic on the Register enum) to match the style already
// used by TryGetGprSlot/TryGetGpr64Offset in DirectExecutionBackend.IllegalInstruction.cs.
private static bool TryGetXmmOffset(Register register, out int offset)
{
switch (register)
{
case Register.XMM0: offset = Win64ContextXmm0Offset + 16 * 0; return true;
case Register.XMM1: offset = Win64ContextXmm0Offset + 16 * 1; return true;
case Register.XMM2: offset = Win64ContextXmm0Offset + 16 * 2; return true;
case Register.XMM3: offset = Win64ContextXmm0Offset + 16 * 3; return true;
case Register.XMM4: offset = Win64ContextXmm0Offset + 16 * 4; return true;
case Register.XMM5: offset = Win64ContextXmm0Offset + 16 * 5; return true;
case Register.XMM6: offset = Win64ContextXmm0Offset + 16 * 6; return true;
case Register.XMM7: offset = Win64ContextXmm0Offset + 16 * 7; return true;
case Register.XMM8: offset = Win64ContextXmm0Offset + 16 * 8; return true;
case Register.XMM9: offset = Win64ContextXmm0Offset + 16 * 9; return true;
case Register.XMM10: offset = Win64ContextXmm0Offset + 16 * 10; return true;
case Register.XMM11: offset = Win64ContextXmm0Offset + 16 * 11; return true;
case Register.XMM12: offset = Win64ContextXmm0Offset + 16 * 12; return true;
case Register.XMM13: offset = Win64ContextXmm0Offset + 16 * 13; return true;
case Register.XMM14: offset = Win64ContextXmm0Offset + 16 * 14; return true;
case Register.XMM15: offset = Win64ContextXmm0Offset + 16 * 15; return true;
default:
offset = 0;
return false;
}
}
}
@@ -133,11 +133,6 @@ public sealed partial class DirectExecutionBackend
{
return -1;
}
if (exceptionCode == StatusIllegalInstruction &&
TryRecoverAmdCompatInstruction(contextRecord, rip))
{
return -1;
}
if (IsBenignHostDebugException(exceptionCode))
{
return -1;
@@ -530,12 +530,9 @@ public sealed partial class DirectExecutionBackend
{
GuestThreadExecution.RestoreImportCallFrame(previousImportCallFrame);
}
if (Volatile.Read(ref _pendingGuestExceptionCount) != 0)
{
DeliverPendingGuestExceptionAtSafePoint(
cpuContext,
CaptureImportBoundaryContinuation(cpuContext, argPackPtr, num7));
}
DeliverPendingGuestExceptionAtSafePoint(
cpuContext,
CaptureImportBoundaryContinuation(cpuContext, argPackPtr, num7));
StoreImportVectorReturn(cpuContext, argPackPtr);
if (dispatchResolved &&
orbisGen2Result == OrbisGen2Result.ORBIS_GEN2_OK &&
@@ -1329,12 +1326,9 @@ public sealed partial class DirectExecutionBackend
GuestThreadExecution.RestoreImportCallFrame(previousImportCallFrame);
}
}
if (Volatile.Read(ref _pendingGuestExceptionCount) != 0)
{
DeliverPendingGuestExceptionAtSafePoint(
cpuContext,
CaptureImportBoundaryContinuation(cpuContext, argPackPtr, returnRip));
}
DeliverPendingGuestExceptionAtSafePoint(
cpuContext,
CaptureImportBoundaryContinuation(cpuContext, argPackPtr, returnRip));
StoreImportVectorReturn(cpuContext, argPackPtr);
if (returnValue != (int)OrbisGen2Result.ORBIS_GEN2_OK)
@@ -1416,8 +1410,6 @@ public sealed partial class DirectExecutionBackend
"eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
"qvMUCyyaCSI" or // sceKernelAprSubmitCommandBufferAndGetId
"Q2V+iqvjgC0" or // vsnprintf
"AV6ipCNa4Rw" or // strcasecmp
"viiwFMaNamA" or // strstr
"q1cHNfGycLI" or // scePadRead
"xk0AcarP3V4" or // scePadOpen
"yH17Q6NWtVg" or // sceUserServiceGetEvent
@@ -1444,9 +1436,6 @@ public sealed partial class DirectExecutionBackend
var expectedMutexTrylockBusy =
string.Equals(nid, "K-jXhbt2gn4", StringComparison.Ordinal) &&
result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
var expectedSemaphoreTrywaitAgain =
string.Equals(nid, "H2a+IN9TP0E", StringComparison.Ordinal) &&
result == OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
var expectedNetAcceptWouldBlock =
string.Equals(nid, "PIWqhn9oSxc", StringComparison.Ordinal) &&
resultValue == unchecked((int)0x80410123);
@@ -1460,7 +1449,6 @@ public sealed partial class DirectExecutionBackend
!expectedTimedWaitTimeout &&
!expectedEqueueTimeout &&
!expectedMutexTrylockBusy &&
!expectedSemaphoreTrywaitAgain &&
!expectedNetAcceptWouldBlock &&
!expectedUserServiceNoEvent &&
!expectedPrivacyInvalidParameter)
@@ -1583,8 +1571,6 @@ public sealed partial class DirectExecutionBackend
"WkkeywLJcgU" or // wcslen
"Ovb2dSJOAuE" or // strcmp
"aesyjrHVWy4" or // strncmp
"AV6ipCNa4Rw" or // strcasecmp
"viiwFMaNamA" or // strstr
"pNtJdE3x49E" or // wcscmp
"fV2xHER+bKE" or // wcscoll
"E8wCoUEbfzk" or // wcsncmp
@@ -712,11 +712,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private readonly Dictionary<ulong, PendingGuestException> _pendingGuestExceptions = new Dictionary<ulong, PendingGuestException>();
// Import dispatch is the hottest managed path in UE titles. Most imports do
// not have an exception queued, so publish the dictionary population and let
// safe points skip _guestThreadGate entirely in the common case.
private int _pendingGuestExceptionCount;
private readonly HashSet<ulong> _activeGuestExceptionDeliveries = new HashSet<ulong>();
private int _guestThreadPumpDepth;
@@ -1413,54 +1408,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
0x75, 0xE7,
0xC3,
],
"AV6ipCNa4Rw" =>
[
0x0F, 0xB6, 0x07,
0x0F, 0xB6, 0x16,
0x8D, 0x48, 0xBF,
0x83, 0xF9, 0x19,
0x77, 0x03,
0x83, 0xC0, 0x20,
0x8D, 0x4A, 0xBF,
0x83, 0xF9, 0x19,
0x77, 0x03,
0x83, 0xC2, 0x20,
0x29, 0xD0,
0x75, 0x0C,
0x85, 0xD2,
0x74, 0x08,
0x48, 0xFF, 0xC7,
0x48, 0xFF, 0xC6,
0xEB, 0xD4,
0xC3,
],
"viiwFMaNamA" =>
[
0x0F, 0xB6, 0x16,
0x84, 0xD2,
0x74, 0x2D,
0x0F, 0xB6, 0x07,
0x84, 0xC0,
0x74, 0x2A,
0x38, 0xD0,
0x75, 0x1D,
0x4C, 0x8D, 0x47, 0x01,
0x4C, 0x8D, 0x4E, 0x01,
0x41, 0x0F, 0xB6, 0x09,
0x84, 0xC9,
0x74, 0x12,
0x41, 0x38, 0x08,
0x75, 0x08,
0x49, 0xFF, 0xC0,
0x49, 0xFF, 0xC1,
0xEB, 0xEB,
0x48, 0xFF, 0xC7,
0xEB, 0xD3,
0x48, 0x89, 0xF8,
0xC3,
0x31, 0xC0,
0xC3,
],
"pNtJdE3x49E" or "fV2xHER+bKE" =>
[
0x0F, 0xB7, 0x07,
@@ -3953,10 +3900,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
// unwinding. Unity can begin its next stop-the-world cycle in
// that window; treating the new raise as part of the old delivery
// strands the collector waiting for an acknowledgement.
QueuePendingGuestExceptionLocked(threadHandle, new PendingGuestException(
_pendingGuestExceptions[threadHandle] = new PendingGuestException(
handler,
exceptionType,
external.ExceptionStackBase));
external.ExceptionStackBase);
return true;
}
@@ -3965,10 +3912,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
// managed thread corrupts the worker's control state. Queue the
// request and let that exact executor consume it at its next HLE
// boundary, where the original guest thread is safely paused.
QueuePendingGuestExceptionLocked(threadHandle, new PendingGuestException(
_pendingGuestExceptions[threadHandle] = new PendingGuestException(
handler,
exceptionType,
external.ExceptionStackBase));
external.ExceptionStackBase);
if (logGuestExceptions)
{
Console.Error.WriteLine(
@@ -4013,17 +3960,17 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
}
if (target.ExceptionDeliveryActive)
{
QueuePendingGuestExceptionLocked(threadHandle, new PendingGuestException(
_pendingGuestExceptions[threadHandle] = new PendingGuestException(
handler,
exceptionType,
exceptionStackBase));
exceptionStackBase);
return true;
}
QueuePendingGuestExceptionLocked(threadHandle, new PendingGuestException(
_pendingGuestExceptions[threadHandle] = new PendingGuestException(
handler,
exceptionType,
exceptionStackBase));
exceptionStackBase);
if (logGuestExceptions)
{
Console.Error.WriteLine(
@@ -4184,7 +4131,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
RestoreInterruptedGuestThread();
if (target.State == GuestThreadRunState.Blocked &&
!target.ExecutorActive &&
TryRemovePendingGuestExceptionLocked(threadHandle, out var queued))
_pendingGuestExceptions.Remove(threadHandle, out var queued))
{
followUp = queued;
}
@@ -4270,11 +4217,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
CpuContext currentContext,
GuestCpuContinuation interruptedContinuation)
{
if (Volatile.Read(ref _pendingGuestExceptionCount) == 0)
{
return;
}
var threadHandle = GuestThreadExecution.CurrentGuestThreadHandle;
if (threadHandle == 0)
{
@@ -4288,7 +4230,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
return;
}
if (!TryRemovePendingGuestExceptionLocked(threadHandle, out pending))
if (!_pendingGuestExceptions.Remove(threadHandle, out pending))
{
return;
}
@@ -4350,27 +4292,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
}
}
private void QueuePendingGuestExceptionLocked(
ulong threadHandle,
PendingGuestException pending)
{
_pendingGuestExceptions[threadHandle] = pending;
Volatile.Write(ref _pendingGuestExceptionCount, _pendingGuestExceptions.Count);
}
private bool TryRemovePendingGuestExceptionLocked(
ulong threadHandle,
out PendingGuestException pending)
{
if (!_pendingGuestExceptions.Remove(threadHandle, out pending))
{
return false;
}
Volatile.Write(ref _pendingGuestExceptionCount, _pendingGuestExceptions.Count);
return true;
}
private static bool TryWriteGuestExceptionContext(
CpuContext context,
ulong address,
@@ -4465,7 +4386,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
_guestThreads.Clear();
_externalGuestThreads.Clear();
_pendingGuestExceptions.Clear();
Volatile.Write(ref _pendingGuestExceptionCount, 0);
_activeGuestExceptionDeliveries.Clear();
}
+1 -1
View File
@@ -252,7 +252,7 @@ public static unsafe class JitStubs
var pattern = TlsAccessPattern;
var end = start + length - pattern.Length;
for (var ptr = start; ptr <= end; ptr++)
for (var ptr = start; ptr < end; ptr++)
{
if (MatchesPattern(ptr, pattern))
{
@@ -40,9 +40,6 @@ public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemo
return result;
}
public bool TryCopy(ulong destinationAddress, ulong sourceAddress, ulong length) =>
_inner.TryCopy(destinationAddress, sourceAddress, length);
public bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address)
{
if (_inner is IGuestMemoryAllocator allocator)
@@ -20,11 +20,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
private readonly Dictionary<(ulong DesiredAddress, ulong Alignment, bool Executable), ulong> _allocationSearchHints = new();
private readonly Dictionary<ulong, ProgramHeaderFlags> _pageProtections = new();
private bool _disposed;
[ThreadStatic]
private static CommittedRangeCache? _committedRangeCache;
private long _mappingGeneration;
private const ulong PageSize = 0x1000;
private const ulong GuestAllocationArenaAddress = 0x00006000_0000_0000;
private const ulong GuestAllocationArenaSize = 0x0100_0000;
@@ -33,77 +28,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
private const ulong FullCommitRegionLimit = 4UL << 30;
private const ulong DefaultLazyReservePrimeBytes = 0x0400_0000UL; // 64 MiB
private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB
private const int CommittedRangeCacheCapacity = 4;
private sealed class CommittedRangeCache
{
private readonly CommittedRange[] _ranges = new CommittedRange[CommittedRangeCacheCapacity];
private PhysicalVirtualMemory? _owner;
private long _generation;
private int _count;
private int _nextReplacement;
public bool Contains(
PhysicalVirtualMemory owner,
long generation,
ulong start,
ulong end)
{
if (!ReferenceEquals(_owner, owner) || _generation != generation)
{
return false;
}
for (var index = 0; index < _count; index++)
{
var range = _ranges[index];
if (start >= range.Start && end <= range.End)
{
return true;
}
}
return false;
}
public void Add(
PhysicalVirtualMemory owner,
long generation,
ulong start,
ulong end)
{
if (!ReferenceEquals(_owner, owner) || _generation != generation)
{
_owner = owner;
_generation = generation;
_count = 0;
_nextReplacement = 0;
}
for (var index = 0; index < _count; index++)
{
var range = _ranges[index];
if (start <= range.End && end >= range.Start)
{
_ranges[index] = new CommittedRange(
Math.Min(start, range.Start),
Math.Max(end, range.End));
return;
}
}
if (_count < _ranges.Length)
{
_ranges[_count++] = new CommittedRange(start, end);
return;
}
_ranges[_nextReplacement] = new CommittedRange(start, end);
_nextReplacement = (_nextReplacement + 1) % _ranges.Length;
}
}
private readonly record struct CommittedRange(ulong Start, ulong End);
// Raw Windows PAGE_* values retained for the internal region/protection
// bookkeeping: regions and saved old-protection values always carry the raw
@@ -425,87 +349,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
return actualAddress;
}
public bool TryBackFixedRange(ulong address, ulong size, bool executable)
{
if (size == 0)
{
return false;
}
var start = AlignDown(address, PageSize);
var end = AlignUp(address + size, PageSize);
if (end <= start)
{
return false;
}
var hostProtection = executable ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite;
// Walk the range page-run by page-run. VirtualQuery reports the largest run
// of same-state pages from the queried address, so a single query advances
// us over whole free or occupied stretches. Only free stretches get backed;
// stretches already reserved or committed by another allocation are left as
// they are, which is exactly what a fixed mapping does on hardware.
var cursor = start;
var backedAny = false;
while (cursor < end)
{
if (!_hostMemory.Query(cursor, out var info))
{
return false;
}
var queriedEnd = info.RegionSize > ulong.MaxValue - info.BaseAddress
? ulong.MaxValue
: info.BaseAddress + info.RegionSize;
var runEnd = Math.Min(end, queriedEnd);
if (runEnd <= cursor)
{
return false;
}
if (info.State == HostRegionState.Free)
{
var runSize = runEnd - cursor;
var allocated = _hostMemory.Allocate(cursor, runSize, hostProtection);
if (allocated != cursor)
{
if (allocated != 0)
{
_hostMemory.Free(allocated);
}
return false;
}
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
_gate.EnterWriteLock();
try
{
InsertRegionSorted(new MemoryRegion
{
VirtualAddress = cursor,
Size = runSize,
IsExecutable = executable,
IsReservedOnly = false,
Protection = protection
});
}
finally
{
_gate.ExitWriteLock();
}
TraceVmem($"Backed fixed range gap: 0x{cursor:X16} - 0x{runEnd:X16} ({runSize} bytes)");
backedAny = true;
}
cursor = runEnd;
}
return backedAny;
}
public bool TryAllocateAtOrAbove(
ulong desiredAddress,
ulong size,
@@ -597,7 +440,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
_gate.ExitWriteLock();
}
Interlocked.Increment(ref _mappingGeneration);
_hostMemory.Free(address);
}
@@ -769,7 +611,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
{
_allocationSearchHints.Clear();
}
Interlocked.Increment(ref _mappingGeneration);
}
finally
{
@@ -1078,7 +919,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
}
NotifyGuestWriteWatch(virtualAddress, source);
return true;
}
}
@@ -1104,68 +944,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
}
}
private static void NotifyGuestWriteWatch(ulong virtualAddress, ReadOnlySpan<byte> source)
{
if (GuestWriteWatch.Armed)
{
GuestWriteWatch.Check(virtualAddress, source);
}
}
public bool TryCopy(ulong destinationAddress, ulong sourceAddress, ulong length)
{
if (length == 0)
{
return true;
}
if (length > int.MaxValue)
{
return false;
}
// Match TryWrite's managed-write notification before touching an
// identity-mapped guest page protected by the image tracker.
GuestImageWriteTracker.NotifyManagedWrite(destinationAddress, length);
_gate.EnterReadLock();
try
{
var sourceRegion = FindRegion(sourceAddress, length);
var destinationRegion = FindRegion(destinationAddress, length);
if (sourceRegion is null || destinationRegion is null ||
!TryResolveRegionOffset(sourceAddress, length, sourceRegion, out var sourceOffset) ||
!TryResolveRegionOffset(destinationAddress, length, destinationRegion, out var destinationOffset))
{
return false;
}
var sourcePointer = sourceRegion.VirtualAddress + sourceOffset;
var destinationPointer = destinationRegion.VirtualAddress + destinationOffset;
if ((sourceRegion.IsReservedOnly &&
!EnsureRangeCommitted(sourcePointer, length, sourceRegion)) ||
(destinationRegion.IsReservedOnly &&
!EnsureRangeCommitted(destinationPointer, length, destinationRegion)) ||
!CanReadWithoutProtectionChange(sourcePointer, length, sourceRegion) ||
!CanWriteWithoutProtectionChange(destinationPointer, length, destinationRegion))
{
return false;
}
// Span.CopyTo has memmove overlap semantics, so this allocation-free
// path safely serves both libc memcpy and libc memmove.
new ReadOnlySpan<byte>((void*)sourcePointer, checked((int)length)).CopyTo(
new Span<byte>((void*)destinationPointer, checked((int)length)));
NotifyGuestWriteWatch(
destinationAddress,
new ReadOnlySpan<byte>((void*)destinationPointer, checked((int)length)));
return true;
}
finally
{
_gate.ExitReadLock();
}
}
private bool TryReadExclusive(ulong virtualAddress, Span<byte> destination)
{
var region = FindRegion(virtualAddress, (ulong)destination.Length);
@@ -1238,7 +1016,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
}
NotifyGuestWriteWatch(virtualAddress, source);
return true;
}
@@ -1263,7 +1040,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
}
}
NotifyGuestWriteWatch(virtualAddress, source);
return true;
}
@@ -1505,12 +1281,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
var startPage = AlignDown(address, PageSize);
var endPage = AlignUp(address + size, PageSize);
var mappingGeneration = Volatile.Read(ref _mappingGeneration);
var committedRangeCache = _committedRangeCache ??= new CommittedRangeCache();
if (committedRangeCache.Contains(this, mappingGeneration, startPage, endPage))
{
return true;
}
var commitProtection = GetCommitProtection(region);
var pageAddress = startPage;
@@ -1532,9 +1302,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
if (info.State == HostRegionState.Committed)
{
// The host query proved this whole range is committed. Retain
// that result instead of caching only the caller's small span.
CacheCommittedRange(info.BaseAddress, queriedEnd, mappingGeneration);
pageAddress = rangeEnd;
continue;
}
@@ -1550,23 +1317,12 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
return false;
}
CacheCommittedRange(pageAddress, rangeEnd, mappingGeneration);
pageAddress = rangeEnd;
}
CacheCommittedRange(startPage, endPage, mappingGeneration);
return true;
}
private void CacheCommittedRange(ulong startPage, ulong endPage, long mappingGeneration)
{
(_committedRangeCache ??= new CommittedRangeCache()).Add(
this,
mappingGeneration,
startPage,
endPage);
}
private bool TryTemporarilyProtectForRead(
ulong address,
ulong size,
+1 -8
View File
@@ -2,7 +2,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Loader;
using SharpEmu.HLE;
namespace SharpEmu.Core.Memory;
@@ -94,14 +93,8 @@ public sealed class VirtualMemory : IVirtualMemory
}
CopyToRegions(virtualAddress, source, regionIndex);
return true;
}
if (GuestWriteWatch.Armed)
{
GuestWriteWatch.Check(virtualAddress, source);
}
return true;
}
private bool TryValidateRange(
+2 -2
View File
@@ -248,7 +248,7 @@ internal sealed class EmulatorProcess : IDisposable
{
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, "1");
if (!CreateProcessW(
null,
exePath,
commandLine,
0,
0,
@@ -629,7 +629,7 @@ internal sealed class EmulatorProcess : IDisposable
[DllImport("kernel32.dll", EntryPoint = "CreateProcessW", SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CreateProcessW(string? applicationName, StringBuilder commandLine, nint processAttributes, nint threadAttributes, [MarshalAs(UnmanagedType.Bool)] bool inheritHandles, uint flags, nint environment, string currentDirectory, ref StartupInfoEx startupInfo, out ProcessInformation processInformation);
private static extern bool CreateProcessW(string applicationName, StringBuilder commandLine, nint processAttributes, nint threadAttributes, [MarshalAs(UnmanagedType.Bool)] bool inheritHandles, uint flags, nint environment, string currentDirectory, ref StartupInfoEx startupInfo, out ProcessInformation processInformation);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint WaitForSingleObject(nint handle, uint milliseconds);
-7
View File
@@ -351,13 +351,6 @@ public sealed class GameSurfaceHost : NativeControlHost
var width = Math.Max(1, (int)Math.Round(Bounds.Width * renderScale));
var height = Math.Max(1, (int)Math.Round(Bounds.Height * renderScale));
var sizeChanged = _surface.PixelWidth != width || _surface.PixelHeight != height;
if (Environment.GetEnvironmentVariable("SHARPEMU_TRACE_SURFACE_SIZE") == "1")
{
Console.Error.WriteLine(
$"[GUI][TRACE] GameSurfaceHost.UpdateSurfaceSize bounds={Bounds.Width}x{Bounds.Height} " +
$"scale={renderScale} computed={width}x{height} changed={sizeChanged} " +
$"prevSurface={_surface.PixelWidth}x{_surface.PixelHeight}");
}
_surface.UpdatePixelSize(width, height);
if (!sizeChanged)
+1 -37
View File
@@ -53,9 +53,6 @@ public sealed class GuiSettings
/// <summary>Names of SHARPEMU_* switches set to "1" in the emulator's environment at launch.</summary>
public List<string> EnvironmentToggles { get; set; } = new();
/// <summary>Internal render resolution scale (1.0 = native, 0.5 = half).</summary>
public double RenderResolutionScale { get; set; } = 1.0;
/// <summary>
/// Discord application ID used for Rich Presence; the default is the
/// SharpEmu application. Override to rebrand what Discord shows as
@@ -74,7 +71,7 @@ public sealed class GuiSettings
if (File.Exists(SettingsPath))
{
var json = File.ReadAllText(SettingsPath);
return NormalizeFromJson(json);
return JsonSerializer.Deserialize<GuiSettings>(json, SerializerOptions) ?? new GuiSettings();
}
}
catch (Exception)
@@ -85,39 +82,6 @@ public sealed class GuiSettings
return new GuiSettings();
}
/// <summary>
/// Deserializes settings and normalizes null references and null or empty list
/// entries introduced by JSON. Empty scalar strings remain unchanged.
/// </summary>
internal static GuiSettings NormalizeFromJson(string json)
{
var settings = JsonSerializer.Deserialize<GuiSettings>(json, SerializerOptions) ?? new GuiSettings();
settings.GameFolders = FilterNullOrEmpty(settings.GameFolders);
settings.ExcludedGames = FilterNullOrEmpty(settings.ExcludedGames);
settings.EnvironmentToggles = FilterNullOrEmpty(settings.EnvironmentToggles);
settings.LogLevel ??= "Info";
settings.Language ??= "en";
settings.DiscordClientId ??= "1525606762248540221";
if (settings.RenderResolutionScale <= 0 || settings.RenderResolutionScale > 2.0)
{
settings.RenderResolutionScale = 1.0;
}
return settings;
}
// JSON can populate non-nullable lists with null references and entries.
private static List<string> FilterNullOrEmpty(List<string>? source)
{
if (source is null)
{
return [];
}
return source.Where(entry => !string.IsNullOrEmpty(entry)).ToList();
}
public void Save()
{
try
-23
View File
@@ -400,29 +400,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
</StackPanel>
</ScrollViewer>
</TabItem>
<TabItem x:Name="GraphicsTabItem" Header="Graphics" FontSize="15">
<ScrollViewer>
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
<Border Classes="card">
<StackPanel Spacing="14">
<TextBlock x:Name="RenderingSectionTitle" Classes="sectionTitle" Text="RENDERING" />
<local:SettingRow x:Name="RenderResolutionRow" Label="Internal resolution"
Description="Render offscreen targets below native resolution and upscale on present. Lower values trade image quality for GPU headroom; takes effect on next launch.">
<ComboBox x:Name="RenderResolutionBox" Width="160" SelectedIndex="0"
VerticalAlignment="Center" CornerRadius="8">
<ComboBoxItem x:Name="RenderResolution100Item" Content="100% (native)" Tag="1.0" />
<ComboBoxItem x:Name="RenderResolution75Item" Content="75%" Tag="0.75" />
<ComboBoxItem x:Name="RenderResolution50Item" Content="50%" Tag="0.5" />
<ComboBoxItem x:Name="RenderResolution25Item" Content="25%" Tag="0.25" />
</ComboBox>
</local:SettingRow>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</TabItem>
<TabItem x:Name="EnvTabItem" Header="Environment" FontSize="15">
<ScrollViewer>
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
+12 -84
View File
@@ -92,11 +92,6 @@ public partial class MainWindow : Window
// plain window color remains the fallback when the asset fails to load.
private Bitmap? _defaultBackdrop;
// Whether the native loading/closing popup should be showing; it is a
// desktop-topmost popup, so it closes while the launcher is in the
// background or minimized and reopens from this flag on activation.
private bool _sessionLoadingActive;
// Controller navigation state.
private readonly DispatcherTimer _gamepadTimer;
private HostGamepadButtons _previousPadButtons;
@@ -155,18 +150,8 @@ public partial class MainWindow : Window
};
_libraryBlurTimer.Tick += (_, _) => AdvanceLibraryBlur();
// Native popups float above every window on the desktop; they must
// follow the launcher into the background or a minimized state.
Activated += (_, _) =>
{
UpdateSessionBarVisibility();
SessionLoadingPopup.IsOpen = _sessionLoadingActive;
};
Deactivated += (_, _) =>
{
SessionBarPopup.IsOpen = false;
SessionLoadingPopup.IsOpen = false;
};
Activated += (_, _) => UpdateSessionBarVisibility();
Deactivated += (_, _) => SessionBarPopup.IsOpen = false;
TitleBar.PointerPressed += OnTitleBarPointerPressed;
GameList.SelectionChanged += (_, _) => UpdateSelectedGame();
@@ -192,18 +177,6 @@ public partial class MainWindow : Window
// it is open already uses the new values.
LogLevelBox.SelectionChanged += (_, _) => _settings.LogLevel = SelectedLogLevel();
TraceImportsBox.ValueChanged += (_, _) => _settings.ImportTraceLimit = (int)(TraceImportsBox.Value ?? 0);
RenderResolutionBox.SelectionChanged += (_, _) =>
{
if (RenderResolutionBox.SelectedItem is ComboBoxItem { Tag: string tag } &&
double.TryParse(
tag,
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture,
out var scale))
{
_settings.RenderResolutionScale = scale;
}
};
StrictToggle.IsCheckedChanged += (_, _) => _settings.StrictDynlibResolution = StrictToggle.IsChecked == true;
LogToFileToggle.IsCheckedChanged += (_, _) => _settings.LogToFile = LogToFileToggle.IsChecked == true;
OverrideLogFileToggle.IsCheckedChanged += (_, _) =>
@@ -441,15 +414,6 @@ public partial class MainWindow : Window
return;
}
if (_isRunning || _isStopping)
{
// The game renders inside the launcher window, so the launcher
// stays active while playing. The controller belongs to the game
// then: no navigation, and Circle/B must never stop the session.
_previousPadButtons = pad.Buttons;
return;
}
var shoulderPressed = pad.Buttons & ~_previousPadButtons;
if ((shoulderPressed & HostGamepadButtons.L1) != 0)
{
@@ -499,6 +463,11 @@ public partial class MainWindow : Window
LaunchSelected();
}
if ((pressed & HostGamepadButtons.Circle) != 0)
{
StopEmulator();
}
_previousPadButtons = pad.Buttons;
}
@@ -881,13 +850,6 @@ public partial class MainWindow : Window
_ => 2,
};
TraceImportsBox.Value = Math.Clamp(_settings.ImportTraceLimit, 0, 4096);
RenderResolutionBox.SelectedIndex = _settings.RenderResolutionScale switch
{
>= 0.875 => 0,
>= 0.625 => 1,
>= 0.375 => 2,
_ => 3,
};
StrictToggle.IsChecked = _settings.StrictDynlibResolution;
LogToFileToggle.IsChecked = _settings.LogToFile;
OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile;
@@ -1664,23 +1626,13 @@ public partial class MainWindow : Window
base.OnPropertyChanged(change);
if (change.Property == WindowStateProperty)
{
// The XAML WindowState="Maximized" assignment raises this change
// during InitializeComponent, before named controls are wired up.
if (WindowState == WindowState.Minimized)
{
_sndPreview.Pause();
if (SessionLoadingPopup is { } popup)
{
popup.IsOpen = false;
}
}
else
{
_sndPreview.Resume();
if (SessionLoadingPopup is { } popup)
{
popup.IsOpen = _sessionLoadingActive;
}
}
}
}
@@ -1807,12 +1759,6 @@ public partial class MainWindow : Window
_appliedEnvironmentVariables.Add(name);
}
Environment.SetEnvironmentVariable(
"SHARPEMU_RENDER_SCALE",
_settings.RenderResolutionScale.ToString(
"0.###",
System.Globalization.CultureInfo.InvariantCulture));
if (SharpEmuLog.TryParseLevel(effective.LogLevel, out var logLevel))
{
SharpEmuLog.MinimumLevel = logLevel;
@@ -2055,27 +2001,16 @@ public partial class MainWindow : Window
RestoreGameViewToFull();
GameView.Background = Brushes.Black;
GameView.IsHitTestVisible = true;
_gameSurfaceHost?.SetPresentationVisible(true);
_gameSurfaceHost?.SetCursorAutoHide(true);
LibraryPage.IsVisible = false;
OptionsPage.IsVisible = false;
LibraryToolbar.IsVisible = false;
ContentToolbar.IsVisible = false;
ConsolePanel.IsVisible = false;
LaunchBar.IsVisible = false;
HideSessionLoading();
SessionLoadingPopup.IsOpen = false;
UpdateSessionBarVisibility();
// Defer so the layout pass from the margin change above settles first.
Dispatcher.UIThread.Post(() =>
{
if (!_isRunning || _isStopping)
{
return;
}
_gameSurfaceHost?.RefreshSurfaceSize();
_gameSurfaceHost?.SetPresentationVisible(true);
_gameSurfaceHost?.SetCursorAutoHide(true);
});
}
});
}
@@ -2174,7 +2109,7 @@ public partial class MainWindow : Window
GameView.IsVisible = false;
GameView.IsHitTestVisible = true;
SessionBarPopup.IsOpen = false;
HideSessionLoading();
SessionLoadingPopup.IsOpen = false;
AnimateLibraryBlur(0, clearWhenComplete: true);
MainContent.Margin = new Thickness(32, 24, 32, 20);
ContentToolbar.IsVisible = true;
@@ -2258,14 +2193,7 @@ public partial class MainWindow : Window
{
SessionLoadingTitle.Text = title;
SessionLoadingDetail.Text = detail;
_sessionLoadingActive = true;
SessionLoadingPopup.IsOpen = IsActive && WindowState != WindowState.Minimized;
}
private void HideSessionLoading()
{
_sessionLoadingActive = false;
SessionLoadingPopup.IsOpen = false;
SessionLoadingPopup.IsOpen = true;
}
private void ReturnToLibraryWhileStopping()
+1 -13
View File
@@ -49,7 +49,7 @@ public sealed class PerGameSettings
var path = PathFor(titleId);
if (File.Exists(path))
{
return NormalizeFromJson(File.ReadAllText(path));
return JsonSerializer.Deserialize<PerGameSettings>(File.ReadAllText(path), SerializerOptions);
}
}
catch (Exception)
@@ -59,18 +59,6 @@ public sealed class PerGameSettings
return null;
}
// A null list inherits global settings; only entries in a present list are sanitized.
internal static PerGameSettings? NormalizeFromJson(string json)
{
var settings = JsonSerializer.Deserialize<PerGameSettings>(json, SerializerOptions);
if (settings?.EnvironmentToggles is { } toggles)
{
settings.EnvironmentToggles = toggles.Where(entry => !string.IsNullOrEmpty(entry)).ToList();
}
return settings;
}
public void Save(string titleId)
{
if (string.IsNullOrWhiteSpace(titleId))
-4
View File
@@ -24,10 +24,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="SharpEmu.Libs.Tests" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" />
<PackageReference Include="Avalonia.Desktop" />
+2 -43
View File
@@ -32,7 +32,6 @@ public static unsafe class GuestImageWriteTracker
public int Armed;
public int FirstCpuWriteSeen;
public int PendingFirstCpuWrite;
public long WriteGeneration;
public bool TraceLifetime;
public long SourceSequence;
public long FirstCpuWriteTraceSequence;
@@ -156,21 +155,10 @@ public static unsafe class GuestImageWriteTracker
{
// Never resize an object that is still reachable from the
// signal handler's lock-free snapshot. Retire it and publish
// a fresh immutable range, carrying the write generation so
// resizes do not hide guest CPU rewrites from cache owners.
var writeGeneration = Volatile.Read(ref range.WriteGeneration);
// a fresh immutable range.
DisarmLocked(range, "replace-range");
_rangesByAddress.Remove(address);
range = new TrackedRange
{
Address = address,
ByteCount = byteCount,
Start = start,
End = start + length,
WriteGeneration = writeGeneration,
};
_rangesByAddress[address] = range;
RebuildSnapshotLocked();
range = null;
}
if (range is null)
@@ -284,31 +272,6 @@ public static unsafe class GuestImageWriteTracker
}
}
/// <summary>
/// Returns the monotonic first-write generation for a tracked allocation.
/// Unlike the consuming dirty flag, this remains changed after another
/// cache owner consumes and re-arms the range.
/// </summary>
public static bool TryGetWriteGeneration(ulong address, out long generation)
{
generation = 0;
if (!_enabled)
{
return false;
}
lock (_gate)
{
if (!_rangesByAddress.TryGetValue(address, out var range))
{
return false;
}
generation = Volatile.Read(ref range.WriteGeneration);
return true;
}
}
/// <summary>
/// Prepares pages touched by a managed HLE memory write. Native guest
/// stores fault and enter <see cref="TryHandleWriteFault"/> through the
@@ -462,10 +425,6 @@ public static unsafe class GuestImageWriteTracker
}
var wasArmed = Interlocked.Exchange(ref range.Armed, 0) != 0;
if (wasArmed)
{
Interlocked.Increment(ref range.WriteGeneration);
}
if (wasArmed &&
range.TraceLifetime &&
Interlocked.CompareExchange(ref range.FirstCpuWriteSeen, 1, 0) == 0)
+1 -1
View File
@@ -17,7 +17,7 @@ public static class GuestTlsTemplate
// Must match CpuDispatcher/DirectExecutionBackend's mapped prefix. PS5
// modules can require more than one host page of Variant II static TLS;
// Dreaming Sarah's startup image, for example, reaches 0x1870 bytes.
public const ulong StartupStaticTlsReservation = 0x20000UL; // Was 0x10000UL, but thats too small for GTA V
public const ulong StartupStaticTlsReservation = 0x10000UL;
private static readonly object _gate = new();
private static readonly SortedDictionary<ulong, ModuleTemplate> _modules = new();
private static readonly Dictionary<ulong, ThreadDtv> _threadDtvs = new();
-203
View File
@@ -1,203 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Globalization;
using System.Threading;
namespace SharpEmu.HLE;
// This tool monitors guest-memory writes only when a watch mode is active.
public static class GuestWriteWatch
{
private const ulong WatchBytes = 8;
private const int MaxBulkReports = 64;
private static readonly ulong WatchBase = Parse(
Environment.GetEnvironmentVariable("SHARPEMU_WATCH_WRITE"));
private static readonly bool WatchPoolHeaders = IsEnabled("SHARPEMU_WATCH_POOL_HEADER");
private static readonly ulong[] PoolSlots = new ulong[64];
private static int _poolSlotCount;
private static readonly bool WatchValuePattern = IsEnabled("SHARPEMU_WATCH_VALUE_PATTERN");
private static readonly bool WatchValue1 = IsEnabled("SHARPEMU_WATCH_VALUE1");
private const ulong DirectBandLow = 0x100_0000_0000;
private const ulong DirectBandHigh = 0x1000_0000_0000;
private static int _value1Reports;
private static readonly bool WatchBulkTorn = IsEnabled("SHARPEMU_WATCH_BULK_TORN");
private static readonly ulong BulkDestHigh = Parse(
Environment.GetEnvironmentVariable("SHARPEMU_WATCH_BULK_DEST_HI"));
private static int _bulkTornReports;
private static int _bulkShiftReports;
public static bool Armed =>
WatchBase != 0 || WatchPoolHeaders || WatchValuePattern || WatchValue1 || WatchBulkTorn;
public static void OnDirectMapping(ulong mappedAddress, ulong length, int protection)
{
if (!WatchPoolHeaders || !IsPoolMapping(length, protection))
{
return;
}
var index = Interlocked.Increment(ref _poolSlotCount) - 1;
if (index < PoolSlots.Length)
{
Volatile.Write(ref PoolSlots[index], mappedAddress + 0x40);
Console.Error.WriteLine(
$"[LOADER][WARN] watch_write armed on pool header slot 0x{mappedAddress + 0x40:X16}");
}
}
public static void Check(ulong address, ReadOnlySpan<byte> data)
{
if (WatchBulkTorn &&
data.Length >= 8 &&
(BulkDestHigh != 0
? (address >> 32) == BulkDestHigh
: address >= DirectBandLow && address < DirectBandHigh))
{
for (var offset = FirstAlignedOffset(address); offset + 8 <= data.Length; offset += 8)
{
var qword = BinaryPrimitives.ReadUInt64LittleEndian(data.Slice(offset, 8));
var kind = ClassifyBulkValue(qword);
if (kind is not null && ReserveBulkReport(kind))
{
Console.Error.WriteLine(
$"[LOADER][WARN] watch_bulk_torn HIT ({kind}) " +
$"dest=0x{address + (ulong)offset:X16} (base=0x{address:X16}+0x{offset:X}) " +
$"len={data.Length} qword=0x{qword:X16}{Environment.NewLine}{Environment.StackTrace}");
Console.Error.Flush();
return;
}
}
}
if (WatchValue1 &&
address >= DirectBandLow && address < DirectBandHigh &&
data.Length is >= 1 and <= 8 &&
LittleEndianValue(data) == 1 &&
Interlocked.Increment(ref _value1Reports) <= 128)
{
Report(address, data);
return;
}
if (WatchValuePattern && data.Length == 8)
{
var value = BinaryPrimitives.ReadUInt64LittleEndian(data);
if ((value & 0xFFFFFFFF) == 1 && value >> 32 is > 0 and <= 0xFFFF)
{
Report(address, data);
return;
}
}
if (WatchBase != 0 && Overlaps(address, data.Length, WatchBase))
{
Report(address, data);
return;
}
var slots = Math.Min(Volatile.Read(ref _poolSlotCount), PoolSlots.Length);
for (var i = 0; i < slots; i++)
{
var slot = Volatile.Read(ref PoolSlots[i]);
if (slot != 0 && Overlaps(address, data.Length, slot))
{
Report(address, data);
return;
}
}
}
internal static string? ClassifyBulkValue(ulong qword)
{
var low32 = qword & 0xFFFFFFFF;
var high32 = qword >> 32;
if (low32 == 1 && high32 is > 0 and <= 0xFFFF)
{
return "torn";
}
var prefix = low32 & 0xFF00_0000;
var hasShiftedPointerPrefix = prefix is 0x0800_0000 or 0x8000_0000;
return high32 == 0 && hasShiftedPointerPrefix && (low32 & 0xFF) == 0
? "shift"
: null;
}
internal static int FirstAlignedOffset(ulong address) =>
(int)((8 - (address & 7)) & 7);
internal static bool IsPoolMapping(ulong length, int protection) =>
length == 0x10000 && protection == 0xF2;
internal static bool Overlaps(ulong address, int length, ulong slot)
{
if (length <= 0)
{
return false;
}
var writeLength = (ulong)length - 1;
var writeEnd = address > ulong.MaxValue - writeLength
? ulong.MaxValue
: address + writeLength;
var slotEnd = slot > ulong.MaxValue - (WatchBytes - 1)
? ulong.MaxValue
: slot + WatchBytes - 1;
return address <= slotEnd && slot <= writeEnd;
}
private static ulong LittleEndianValue(ReadOnlySpan<byte> data)
{
ulong value = 0;
for (var i = 0; i < data.Length; i++)
{
value |= (ulong)data[i] << (i * 8);
}
return value;
}
private static void Report(ulong address, ReadOnlySpan<byte> data)
{
Console.Error.WriteLine(
$"[LOADER][WARN] watch_write HIT addr=0x{address:X16} len={data.Length} " +
$"first_qword=0x{LittleEndianValue(data):X16}{Environment.NewLine}{Environment.StackTrace}");
Console.Error.Flush();
}
private static bool IsEnabled(string name) =>
string.Equals(Environment.GetEnvironmentVariable(name), "1", StringComparison.Ordinal);
private static bool ReserveBulkReport(string kind) =>
kind == "torn"
? Interlocked.Increment(ref _bulkTornReports) <= MaxBulkReports
: Interlocked.Increment(ref _bulkShiftReports) <= MaxBulkReports;
internal static ulong Parse(string? text)
{
if (string.IsNullOrWhiteSpace(text))
{
return 0;
}
text = text.Trim();
if (text.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
{
text = text[2..];
}
return ulong.TryParse(text, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var value)
? value
: 0;
}
}
-2
View File
@@ -10,6 +10,4 @@ public interface ICpuMemory
bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source);
bool TryCompare(ulong virtualAddress, ReadOnlySpan<byte> expected) => false;
bool TryCopy(ulong destinationAddress, ulong sourceAddress, ulong length) => false;
}
-11
View File
@@ -15,17 +15,6 @@ public interface IGuestAddressSpace : IGuestMemoryAllocator
{
ulong AllocateAt(ulong desiredAddress, ulong size, bool executable = true, bool allowAlternative = true);
/// <summary>
/// Backs an entire fixed-address range, matching the guest's
/// <c>SCE_KERNEL_MAP_FIXED</c> contract. Unlike <see cref="AllocateAt"/>, which
/// reserves the range in one all-or-nothing host call, this walks the range and
/// fills only the sub-ranges that are not already backed. That keeps a fixed
/// mapping whole when part of the requested window is already occupied — the
/// partial-overlap case where the single-call reservation fails outright and
/// leaves the remainder unmapped for the guest to fault into.
/// </summary>
bool TryBackFixedRange(ulong address, ulong size, bool executable);
bool TryAllocateAtOrAbove(ulong desiredAddress, ulong size, bool executable, ulong alignment, out ulong actualAddress);
bool TryProtect(ulong address, ulong size, GuestPageProtection protection);
+84 -329
View File
@@ -147,10 +147,6 @@ public static partial class AgcExports
private const uint Gen5TextureFormatR16G16B16A16Float = 12;
private const uint Gen5TextureType1D = 8;
private const uint Gen5TextureType2D = 9;
private const uint Gen5TextureType3D = 10;
private const uint Gen5TextureTypeCube = 11;
private const uint Gen5TextureType1DArray = 12;
private const uint Gen5TextureType2DArray = 13;
private const ulong MaxPresentedTextureBytes = 128UL * 1024UL * 1024UL;
private const ulong VideoOutPixelFormatA8R8G8B8Srgb = 0x80000000;
private const ulong VideoOutPixelFormatA8B8G8R8Srgb = 0x80002200;
@@ -442,8 +438,7 @@ public static partial class AgcExports
TextureDescriptor Descriptor,
bool IsStorage,
uint MipLevel,
IReadOnlyList<uint> SamplerDescriptor,
bool IsArrayed = false);
IReadOnlyList<uint> SamplerDescriptor);
private readonly record struct RenderTargetWriter(
ulong Sequence,
@@ -573,8 +568,6 @@ public static partial class AgcExports
public ulong WorkSequence { get; set; }
public ulong SubmissionSequence { get; set; }
public bool WaitMonitorRunning { get; set; }
public object WaitMonitorSignalGate { get; } = new();
public long WaitMonitorSignalVersion { get; set; }
}
private readonly record struct RegisteredAgcResource(
@@ -645,21 +638,6 @@ public static partial class AgcExports
public static int GetRegisterDefaults2Internal(CpuContext ctx) =>
ReturnRegisterDefaults(ctx, internalDefaults: true);
/// <summary>
/// Reports that the GPU is not running in Trinity mode, matching the base
/// console this backend emulates.
/// </summary>
[SysAbiExport(
Nid = "BfBDZGbti7A",
ExportName = "sceAgcGetIsTrinityMode",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int GetIsTrinityMode(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "f3dg2CSgRKY",
ExportName = "sceAgcCreateShader",
@@ -3335,11 +3313,24 @@ public static partial class AgcExports
length,
op,
out var dispatch,
out _))
out var indirectDimsRetryAddress))
{
state.FrameDispatchCount++;
ObserveComputeDispatch(ctx, gpuState, state, dispatch);
}
else if (indirectDimsRetryAddress != 0 &&
HandleSubmittedIndirectDimsWait(
ctx,
state,
commandAddress,
currentAddress,
offset,
dwordCount,
indirectDimsRetryAddress,
tracePackets))
{
return true; // suspend until the producer computes the dims
}
}
if (op == ItNop &&
@@ -3677,11 +3668,27 @@ public static partial class AgcExports
void CompleteAndWake()
{
CompleteLabelProducer(producer);
lock (gpuState.WaitMonitorSignalGate)
if (GpuWaitRegistry.Count == 0)
{
gpuState.WaitMonitorSignalVersion++;
Monitor.Pulse(gpuState.WaitMonitorSignalGate);
return;
}
// Resuming a DCB can enqueue another compute dispatch and wait for
// it. Never do that reentrantly on the Vulkan render thread.
ThreadPool.UnsafeQueueUserWorkItem(
static state =>
{
var (resumeContext, resumeGpuState) = state;
lock (resumeGpuState.Gate)
{
DrainResumableDcbs(
resumeContext,
resumeGpuState,
tracePackets: _traceAgc);
}
},
(ctx, gpuState),
preferLocal: false);
}
void ApplyAndQueueCompletion()
@@ -4860,45 +4867,38 @@ public static partial class AgcExports
SubmittedGpuState gpuState)
{
var delayMilliseconds = 1;
long observedSignal;
lock (gpuState.WaitMonitorSignalGate)
{
observedSignal = gpuState.WaitMonitorSignalVersion;
}
while (true)
{
int resumed;
int remaining;
var madeProgress = false;
lock (gpuState.Gate)
{
resumed = DrainResumableDcbs(ctx, gpuState, tracePackets: _traceAgc);
remaining = GpuWaitRegistry.CountForMemory(ctx.Memory);
if (_traceAgc && resumed != 0)
var before = GpuWaitRegistry.CountForMemory(ctx.Memory);
if (before == 0)
{
gpuState.WaitMonitorRunning = false;
return;
}
DrainResumableDcbs(ctx, gpuState, tracePackets: _traceAgc);
var after = GpuWaitRegistry.CountForMemory(ctx.Memory);
madeProgress = after < before;
if (madeProgress)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] agc.wait_monitor_resumed count={resumed} " +
$"remaining={remaining}");
$"[LOADER][TRACE] agc.wait_monitor_resumed count={before - after} " +
$"remaining={after}");
}
if (remaining == 0)
if (after == 0)
{
gpuState.WaitMonitorRunning = false;
return;
}
}
delayMilliseconds = resumed != 0
delayMilliseconds = madeProgress
? 1
: Math.Min(delayMilliseconds * 2, 16);
lock (gpuState.WaitMonitorSignalGate)
{
if (gpuState.WaitMonitorSignalVersion == observedSignal)
{
Monitor.Wait(gpuState.WaitMonitorSignalGate, delayMilliseconds);
}
observedSignal = gpuState.WaitMonitorSignalVersion;
}
Thread.Sleep(delayMilliseconds);
}
}
@@ -4952,17 +4952,16 @@ public static partial class AgcExports
// guest memory (labels are advanced by ReleaseMem/WriteData/DmaData packets
// or direct CPU writes) and resumes the ones now satisfied. A resumed DCB
// can itself write labels that unblock others, so loop to a fixed point.
private static int DrainResumableDcbs(
private static void DrainResumableDcbs(
CpuContext ctx,
SubmittedGpuState gpuState,
bool tracePackets)
{
if (!_gpuWaitSuspendEnabled)
{
return 0;
return;
}
var resumedCount = 0;
for (var pass = 0; pass < 256; pass++)
{
var woken = GpuWaitRegistry.CollectSatisfied(ctx.Memory, (address, is64Bit) =>
@@ -5040,7 +5039,7 @@ public static partial class AgcExports
}
}
return resumedCount;
return;
}
if (woken is not null)
@@ -5048,12 +5047,9 @@ public static partial class AgcExports
foreach (var waiter in woken)
{
ResumeSuspendedDcb(ctx, gpuState, waiter, tracePackets);
resumedCount++;
}
}
}
return resumedCount;
}
private static void ResumeSuspendedDcb(
@@ -5964,8 +5960,7 @@ public static partial class AgcExports
binding,
exportEvaluation.ImageBindings),
binding.MipLevel ?? 0,
binding.SamplerDescriptor,
Gen5ShaderTranslator.IsArrayedImageBinding(binding)));
binding.SamplerDescriptor));
}
IReadOnlyList<Gen5VertexInputBinding> vertexInputs =
@@ -6422,8 +6417,7 @@ public static partial class AgcExports
texture,
isStorage,
binding.MipLevel ?? 0,
binding.SamplerDescriptor,
Gen5ShaderTranslator.IsArrayedImageBinding(binding)));
binding.SamplerDescriptor));
}
error = string.Empty;
@@ -7394,7 +7388,6 @@ public static partial class AgcExports
binding.IsStorage,
binding.MipLevel,
binding.SamplerDescriptor,
binding.IsArrayed,
out var texture))
{
textures.Add(texture);
@@ -7790,10 +7783,7 @@ public static partial class AgcExports
TextureDescriptor descriptor,
uint sourceWidth,
int logicalByteCount,
byte[] source,
bool baseMipInTail = false,
int tailElementX = 0,
int tailElementY = 0)
byte[] source)
{
if (!GnmTiling.NeedsDetile(descriptor.TileMode) ||
!TryGetTextureElementLayout(
@@ -7806,48 +7796,6 @@ public static partial class AgcExports
return null;
}
if (baseMipInTail)
{
if (!GnmTiling.TryGetBlockElementDimensions(
descriptor.TileMode,
bytesPerElement,
out var blockWidth,
out var blockHeight))
{
return null;
}
var blockByteCount = (long)blockWidth * blockHeight * bytesPerElement;
if (source.Length < blockByteCount ||
(long)elementsWide * elementsHigh * bytesPerElement > logicalByteCount)
{
return null;
}
var blockLinear = new byte[blockByteCount];
if (!GnmTiling.TryDetile(
source,
blockLinear,
descriptor.TileMode,
blockWidth,
blockHeight,
bytesPerElement))
{
return null;
}
var tailLinear = new byte[logicalByteCount];
var rowBytes = elementsWide * bytesPerElement;
for (var y = 0; y < elementsHigh; y++)
{
var sourceOffset = (((long)tailElementY + y) * blockWidth + tailElementX) * bytesPerElement;
blockLinear.AsSpan((int)sourceOffset, rowBytes)
.CopyTo(tailLinear.AsSpan(y * rowBytes, rowBytes));
}
return tailLinear;
}
var linear = new byte[logicalByteCount];
return GnmTiling.TryDetile(
source,
@@ -7885,23 +7833,18 @@ public static partial class AgcExports
bool isStorage,
uint mipLevel,
IReadOnlyList<uint> samplerDescriptor,
bool isArrayed,
out GuestDrawTexture texture)
{
texture = default!;
if ((descriptor.Type != Gen5TextureType1D &&
descriptor.Type != Gen5TextureType2D &&
descriptor.Type != Gen5TextureType3D &&
descriptor.Type != Gen5TextureTypeCube &&
descriptor.Type != Gen5TextureType1DArray &&
descriptor.Type != Gen5TextureType2DArray) ||
descriptor.Type != Gen5TextureType2D) ||
descriptor.Width == 0 ||
descriptor.Height == 0 ||
descriptor.Width > 8192 ||
descriptor.Height > 8192)
{
TraceTextureFallback(descriptor, "invalid-descriptor");
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType, isArrayed);
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
return true;
}
@@ -7922,22 +7865,18 @@ public static partial class AgcExports
TraceTextureFallback(
descriptor,
$"invalid-byte-count:{sourceByteCount}");
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType, isArrayed);
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
return true;
}
var physicalSourceByteCount = sourceByteCount;
var elementsWide = 0;
var elementsHigh = 0;
var bytesPerElement = 0;
var hasElementLayout = GnmTiling.NeedsDetile(descriptor.TileMode) &&
if (GnmTiling.NeedsDetile(descriptor.TileMode) &&
TryGetTextureElementLayout(
descriptor,
sourceWidth,
out elementsWide,
out elementsHigh,
out bytesPerElement);
if (hasElementLayout &&
out var elementsWide,
out var elementsHigh,
out var bytesPerElement) &&
GnmTiling.TryGetTiledByteCount(
descriptor.TileMode,
elementsWide,
@@ -7951,50 +7890,13 @@ public static partial class AgcExports
if (physicalSourceByteCount > MaxPresentedTextureBytes ||
physicalSourceByteCount > int.MaxValue)
{
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType, isArrayed);
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
return true;
}
var resourceMipLevels = descriptor.HasExtendedDescriptor
? descriptor.ResourceMipLevels
: 1u;
var baseMipByteOffset = 0UL;
var baseMipInTail = false;
var mipTailElementX = 0;
var mipTailElementY = 0;
var chainSliceBytes = physicalSourceByteCount;
if (hasElementLayout && resourceMipLevels > 1 &&
GnmTiling.TryGetBaseMipPlacement(
descriptor.TileMode,
elementsWide,
elementsHigh,
bytesPerElement,
resourceMipLevels,
out baseMipByteOffset,
out baseMipInTail,
out mipTailElementX,
out mipTailElementY,
out var placedChainSliceBytes))
{
chainSliceBytes = placedChainSliceBytes;
}
var wantsArrayUpload = isArrayed &&
!isStorage &&
descriptor.Address != 0 &&
(descriptor.Type == Gen5TextureType2DArray ||
descriptor.Type == Gen5TextureType1DArray) &&
descriptor.Depth > 1;
var arrayUploadLayers = wantsArrayUpload ? descriptor.Depth : 1u;
// Upload-known (not plain availability): the presenter's answer goes
// generation-stale when the guest CPU rewrites a CPU-backed image
// (video planes, streamed font atlases), which routes this draw back
// through the texel copy below so the refresh path re-uploads.
if (!isStorage &&
!wantsArrayUpload &&
descriptor.Address != 0 &&
GuestGpu.Current.IsGuestImageUploadKnown(
GuestGpu.Current.IsGpuGuestImageAvailable(
descriptor.Address,
descriptor.Format,
descriptor.NumberType))
@@ -8015,8 +7917,7 @@ public static partial class AgcExports
Pitch: sourceWidth,
TileMode: descriptor.TileMode,
DstSelect: descriptor.DstSelect,
Sampler: ToGuestSampler(samplerDescriptor),
ArrayedView: isArrayed);
Sampler: ToGuestSampler(samplerDescriptor));
return true;
}
@@ -8039,17 +7940,14 @@ public static partial class AgcExports
// and run the same AddrLib-derived detile path used below for
// sampled textures before seeding the Vulkan image.
var storageSource = new byte[(int)physicalSourceByteCount];
if (ctx.Memory.TryRead(descriptor.Address + baseMipByteOffset, storageSource))
if (ctx.Memory.TryRead(descriptor.Address, storageSource))
{
readSucceeded = true;
var linearStorage = TryDetileTextureSource(
descriptor,
sourceWidth,
checked((int)sourceByteCount),
storageSource,
baseMipInTail,
mipTailElementX,
mipTailElementY) ?? storageSource
storageSource) ?? storageSource
.AsSpan(0, checked((int)sourceByteCount))
.ToArray();
if (linearStorage.AsSpan().IndexOfAnyExcept((byte)0) >= 0)
@@ -8105,19 +8003,6 @@ public static partial class AgcExports
// (skipping would leave the draw with no pixels and a fallback
// texture for the frame — visible flicker on animated textures).
var sampler = ToGuestSampler(samplerDescriptor);
// Track the guest allocation before reading its texels so a CPU
// rewrite landing after the copy still bumps the write generation.
// The generation rides on the texture and is recorded by the
// presenter after upload, where the upload-known skip compares it
// against the tracker to force fresh texels for rewritten memory.
SharpEmu.HLE.GuestImageWriteTracker.Track(
descriptor.Address,
physicalSourceByteCount,
source: "agc.decoded-texture");
var hasWriteGeneration =
SharpEmu.HLE.GuestImageWriteTracker.TryGetWriteGeneration(
descriptor.Address,
out var writeGeneration);
if (!_textureCopySkipDisabled &&
descriptor.Address != 0 &&
!SharpEmu.HLE.GuestImageWriteTracker.PeekDirty(descriptor.Address) &&
@@ -8131,9 +8016,7 @@ public static partial class AgcExports
descriptor.DstSelect,
descriptor.TileMode,
sourceWidth,
sampler,
isArrayed,
arrayUploadLayers)))
sampler)))
{
texture = new GuestDrawTexture(
descriptor.Address,
@@ -8151,77 +8034,17 @@ public static partial class AgcExports
Pitch: sourceWidth,
TileMode: descriptor.TileMode,
DstSelect: descriptor.DstSelect,
Sampler: sampler,
ArrayedView: isArrayed,
ArrayLayers: arrayUploadLayers);
Sampler: sampler);
return true;
}
if (wantsArrayUpload)
{
var arrayLayers = arrayUploadLayers;
var layerBytes = checked((int)sourceByteCount);
var totalBytes = (long)layerBytes * arrayLayers;
if (totalBytes <= int.MaxValue)
{
var layered = new byte[totalBytes];
var uploadedLayers = 0u;
for (var layer = 0u; layer < arrayLayers; layer++)
{
var sliceSource = new byte[(int)physicalSourceByteCount];
if (!ctx.Memory.TryRead(
descriptor.Address + layer * chainSliceBytes + baseMipByteOffset,
sliceSource))
{
break;
}
var sliceLinear = TryDetileTextureSource(
descriptor,
sourceWidth,
layerBytes,
sliceSource,
baseMipInTail,
mipTailElementX,
mipTailElementY) ?? sliceSource.AsSpan(0, layerBytes).ToArray();
sliceLinear.AsSpan(0, layerBytes)
.CopyTo(layered.AsSpan(checked((int)(layer * layerBytes))));
uploadedLayers++;
}
if (uploadedLayers == arrayLayers)
{
texture = new GuestDrawTexture(
descriptor.Address,
descriptor.Width,
descriptor.Height,
descriptor.Format,
descriptor.NumberType,
layered,
IsFallback: false,
IsStorage: false,
MipLevels: descriptor.MipLevels,
MipLevel: mipLevel,
BaseMipLevel: descriptor.ViewBaseLevel,
ResourceMipLevels: descriptor.ResourceMipLevels,
Pitch: sourceWidth,
TileMode: descriptor.TileMode,
DstSelect: descriptor.DstSelect,
Sampler: sampler,
ArrayedView: true,
ArrayLayers: arrayLayers);
return true;
}
}
}
var source = new byte[(int)physicalSourceByteCount];
if (!ctx.Memory.TryRead(descriptor.Address + baseMipByteOffset, source))
if (!ctx.Memory.TryRead(descriptor.Address, source))
{
TraceTextureFallback(
descriptor,
$"guest-read-failed:{sourceByteCount}");
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType, isArrayed);
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
return true;
}
@@ -8253,10 +8076,7 @@ public static partial class AgcExports
descriptor,
sourceWidth,
checked((int)sourceByteCount),
source,
baseMipInTail,
mipTailElementX,
mipTailElementY) ?? source.AsSpan(0, checked((int)sourceByteCount)).ToArray();
source) ?? source.AsSpan(0, checked((int)sourceByteCount)).ToArray();
DumpLinearTextureIfRequested(descriptor, sourceWidth, rgba);
texture = new GuestDrawTexture(
descriptor.Address,
@@ -8274,9 +8094,7 @@ public static partial class AgcExports
Pitch: sourceWidth,
TileMode: descriptor.TileMode,
DstSelect: descriptor.DstSelect,
Sampler: ToGuestSampler(samplerDescriptor),
WriteGeneration: hasWriteGeneration ? writeGeneration : -1,
ArrayedView: isArrayed);
Sampler: ToGuestSampler(samplerDescriptor));
return true;
}
@@ -8555,8 +8373,7 @@ public static partial class AgcExports
private static GuestDrawTexture CreateFallbackGuestDrawTexture(
bool isStorage,
uint format,
uint numberType,
bool isArrayed = false)
uint numberType)
{
var fallbackFormat = format == 0 ? 10u : format;
var fallbackNumberType = numberType;
@@ -8570,8 +8387,7 @@ public static partial class AgcExports
IsFallback: true,
IsStorage: isStorage,
MipLevels: 1,
MipLevel: 0,
ArrayedView: isArrayed);
MipLevel: 0);
}
private static GuestSampler ToGuestSampler(IReadOnlyList<uint> descriptor) =>
@@ -8947,8 +8763,7 @@ public static partial class AgcExports
texture,
isStorage,
binding.MipLevel ?? 0,
binding.SamplerDescriptor,
Gen5ShaderTranslator.IsArrayedImageBinding(binding)));
binding.SamplerDescriptor));
hasStorageBinding |= isStorage;
var descriptorState = descriptorValid ? string.Empty : "/invalid-desc";
@@ -9079,7 +8894,7 @@ public static partial class AgcExports
out _);
var globalMemoryBuffers =
CreateTranslatedComputeGlobalBuffers(evaluation);
GuestGpu.Current.SubmitComputeDispatch(
var workSequence = GuestGpu.Current.SubmitComputeDispatch(
shaderAddress,
computeShader,
textures,
@@ -9098,9 +8913,12 @@ public static partial class AgcExports
dispatch.ThreadCountX,
dispatch.ThreadCountY,
dispatch.ThreadCountZ);
// Vulkan queue order keeps dependent dispatches coherent. CPU visibility is
// published by explicit PM4 release/write actions instead of per dispatch.
gpuDispatch = true;
if (writesGlobalMemory &&
!GuestGpu.Current.WaitForGuestWork(workSequence))
{
computeError = $"global-write-sync-timeout sequence={workSequence}";
}
}
}
@@ -11598,69 +11416,6 @@ public static partial class AgcExports
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
private static int RemoveResourcesForOwner(SubmittedGpuState state, uint owner)
{
var stale = new List<uint>();
foreach (var (handle, resource) in state.RegisteredResources)
{
if (resource.Owner == owner)
{
stale.Add(handle);
}
}
foreach (var handle in stale)
{
state.RegisteredResources.Remove(handle);
}
return stale.Count;
}
[SysAbiExport(
Nid = "ZLJk9r2+2Aw",
ExportName = "sceAgcDriverUnregisterOwnerAndResources",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int DriverUnregisterOwnerAndResources(CpuContext ctx)
{
var owner = (uint)ctx[CpuRegister.Rdi];
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
int resources;
lock (state.Gate)
{
if (!state.ResourceOwners.Remove(owner))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
resources = RemoveResourcesForOwner(state, owner);
state.ComputeQueues.Remove(owner);
}
TraceAgc($"agc.driver_unregister_owner owner={owner} resources={resources}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "SCoAN5fYlUM",
ExportName = "sceAgcDriverUnregisterAllResourcesForOwner",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int DriverUnregisterAllResourcesForOwner(CpuContext ctx)
{
var owner = (uint)ctx[CpuRegister.Rdi];
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
int resources;
lock (state.Gate)
{
resources = RemoveResourcesForOwner(state, owner);
}
TraceAgc($"agc.driver_unregister_owner_resources owner={owner} resources={resources}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "pWLG7WOpVcw",
ExportName = "sceAgcDriverUnregisterResource",
@@ -27,7 +27,7 @@ internal static class AgcShaderCompilerHooks
internal static void Install()
{
Gen5ShaderScalarEvaluator.FallbackMemoryReader =
KernelMemoryCompatExports.TryReadShaderGuestMemory;
KernelMemoryCompatExports.TryReadTrackedLibcHeap;
Gen5ShaderScalarEvaluator.GlobalMemoryPool =
GuestDataPool.Shared;
}
-158
View File
@@ -194,164 +194,6 @@ internal static class GnmTiling
}
}
public static bool TryGetBlockElementDimensions(
uint swizzleMode,
int bytesPerElement,
out int blockWidth,
out int blockHeight)
{
blockWidth = 0;
blockHeight = 0;
if (bytesPerElement <= 0 ||
!TryGetSwizzleKind(swizzleMode, out _, out var blockBytes))
{
return false;
}
var bppLog2 = BitLog2((uint)bytesPerElement);
if (bppLog2 < 0)
{
return false;
}
(blockWidth, blockHeight) = SquareBlockDimensions(blockBytes >> bppLog2);
return blockWidth != 0 && blockHeight != 0;
}
/// <summary>
/// Locates mip 0 in a GFX10 mip chain, which AddrLib stores smallest-first
/// (Gfx10Lib::ComputeSurfaceInfoMacroTiled/MicroTiled).
/// </summary>
public static bool TryGetBaseMipPlacement(
uint swizzleMode,
int elementsWide,
int elementsHigh,
int bytesPerElement,
uint resourceMipLevels,
out ulong byteOffset,
out bool inMipTail,
out int tailElementX,
out int tailElementY,
out ulong chainSliceBytes)
{
byteOffset = 0;
inMipTail = false;
tailElementX = 0;
tailElementY = 0;
chainSliceBytes = 0;
if (resourceMipLevels <= 1 ||
!ShouldDetile(swizzleMode) ||
elementsWide <= 0 ||
elementsHigh <= 0 ||
bytesPerElement <= 0 ||
!TryGetSwizzleKind(swizzleMode, out _, out var blockBytes))
{
return false;
}
var bppLog2 = BitLog2((uint)bytesPerElement);
if (bppLog2 < 0)
{
return false;
}
var (blockWidth, blockHeight) = SquareBlockDimensions(blockBytes >> bppLog2);
var blockSizeLog2 = BitLog2((uint)blockBytes);
if (blockWidth == 0 || blockHeight == 0 || blockSizeLog2 < 8)
{
return false;
}
var mipLevels = (int)Math.Min(resourceMipLevels, 16u);
var maxMipsInTail = blockSizeLog2 <= 8 ? 0
: blockSizeLog2 <= 11
? 1 + (1 << (blockSizeLog2 - 9))
: blockSizeLog2 - 4;
var tailWidth = (blockSizeLog2 & 1) != 0 ? blockWidth >> 1 : blockWidth;
var tailHeight = (blockSizeLog2 & 1) != 0 ? blockHeight : blockHeight >> 1;
var firstMipInTail = mipLevels;
var mipSizes = new ulong[mipLevels];
for (var i = 0; i < mipLevels; i++)
{
var mipWidth = Math.Max(elementsWide >> i, 1);
var mipHeight = Math.Max(elementsHigh >> i, 1);
if (maxMipsInTail > 0 &&
mipWidth <= tailWidth &&
mipHeight <= tailHeight &&
mipLevels - i <= maxMipsInTail)
{
firstMipInTail = i;
break;
}
var alignedWidth = (ulong)(mipWidth + blockWidth - 1) / (ulong)blockWidth * (ulong)blockWidth;
var alignedHeight = (ulong)(mipHeight + blockHeight - 1) / (ulong)blockHeight * (ulong)blockHeight;
mipSizes[i] = alignedWidth * alignedHeight * (ulong)bytesPerElement;
}
if (firstMipInTail == 0)
{
var m = maxMipsInTail - 1;
var mipOffset = m > 6 ? 16 << m : m << 8;
var mipX = ((mipOffset >> 9) & 1) |
((mipOffset >> 10) & 2) |
((mipOffset >> 11) & 4) |
((mipOffset >> 12) & 8) |
((mipOffset >> 13) & 16) |
((mipOffset >> 14) & 32);
var mipY = ((mipOffset >> 8) & 1) |
((mipOffset >> 9) & 2) |
((mipOffset >> 10) & 4) |
((mipOffset >> 11) & 8) |
((mipOffset >> 12) & 16) |
((mipOffset >> 13) & 32);
if ((blockSizeLog2 & 1) != 0)
{
(mipX, mipY) = (mipY, mipX);
if ((bppLog2 & 1) != 0)
{
mipY = (mipY << 1) | (mipX & 1);
mipX >>= 1;
}
}
var (microWidth, microHeight) = SquareBlockDimensions(256 >> bppLog2);
if (microWidth == 0 || microHeight == 0)
{
return false;
}
tailElementX = mipX * microWidth;
tailElementY = mipY * microHeight;
if (tailElementX + elementsWide > blockWidth ||
tailElementY + elementsHigh > blockHeight)
{
tailElementX = 0;
tailElementY = 0;
return false;
}
inMipTail = true;
chainSliceBytes = (ulong)blockBytes;
return true;
}
byteOffset = firstMipInTail < mipLevels ? (ulong)blockBytes : 0;
chainSliceBytes = byteOffset;
for (var i = firstMipInTail - 1; i >= 1; i--)
{
byteOffset += mipSizes[i];
}
for (var i = 0; i < firstMipInTail; i++)
{
chainSliceBytes += mipSizes[i];
}
return true;
}
/// <summary>
/// Deswizzles <paramref name="tiled"/> into linear row-major order.
/// Elements are pixels for uncompressed formats and 4x4 blocks for
+17 -10
View File
@@ -6,7 +6,6 @@ using SharpEmu.Libs.Kernel;
using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using Microsoft.Win32.SafeHandles;
namespace SharpEmu.Libs.Ampr;
@@ -44,17 +43,17 @@ public static class AmprExports
{
public CachedHostFile(string path)
{
Handle = File.OpenHandle(
Stream = new FileStream(
path,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite | FileShare.Delete,
bufferSize: 1024 * 1024,
FileOptions.RandomAccess);
Length = RandomAccess.GetLength(Handle);
}
public SafeFileHandle Handle { get; }
public long Length { get; }
public object Gate { get; } = new();
public FileStream Stream { get; }
}
[SysAbiExport(
@@ -736,7 +735,13 @@ public static class AmprExports
return openResult;
}
if (fileOffset >= (ulong)cachedFile.Length)
long fileLength;
lock (cachedFile.Gate)
{
fileLength = cachedFile.Stream.Length;
}
if (fileOffset >= (ulong)fileLength)
{
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -755,10 +760,12 @@ public static class AmprExports
}
var request = (int)Math.Min((ulong)buffer.Length, size - bytesRead);
var read = RandomAccess.Read(
cachedFile.Handle,
buffer.AsSpan(0, request),
unchecked((long)absoluteOffset));
int read;
lock (cachedFile.Gate)
{
cachedFile.Stream.Position = unchecked((long)absoluteOffset);
read = cachedFile.Stream.Read(buffer, 0, request);
}
if (read <= 0)
{
+11 -90
View File
@@ -17,9 +17,7 @@ public static class AvPlayerExports
private const int FrameBufferCount = 3;
private const int FrameInfoSize = 40;
private const int FrameInfoExSize = 104;
// This structure is 32 bytes. A larger write can damage the guest stack.
private const int StreamInfoSize = 32;
private const int StreamInfoExSize = 32;
private const int StreamInfoSize = 40;
private const int MaxGuestPathLength = 4096;
private static readonly object StateGate = new();
private static readonly Dictionary<ulong, PlayerState> Players = new();
@@ -406,8 +404,7 @@ public static class AvPlayerExports
ExportName = "sceAvPlayerGetStreamInfoEx",
Target = Generation.Gen5,
LibraryName = "libSceAvPlayer")]
public static int AvPlayerGetStreamInfoEx(CpuContext ctx) =>
GetStreamInfoCore(ctx, StreamInfoExSize);
public static int AvPlayerSetDecoderMode(CpuContext ctx) => ValidatePlayer(ctx);
[SysAbiExport(
Nid = "XC9wM+xULz8",
@@ -564,48 +561,12 @@ public static class AvPlayerExports
}
}
internal static void RegisterPlayerForTest(
ulong handle,
int width,
int height,
ulong durationMilliseconds)
{
PlayerState? previous;
lock (StateGate)
{
Players.Remove(handle, out previous);
Players[handle] = new PlayerState
{
Handle = handle,
Width = width,
Height = height,
DurationMilliseconds = durationMilliseconds,
};
}
previous?.Dispose();
}
internal static void RemovePlayerForTest(ulong handle)
{
PlayerState? player;
lock (StateGate)
{
Players.Remove(handle, out player);
}
player?.Dispose();
}
[SysAbiExport(
Nid = "d8FcbzfAdQw",
ExportName = "sceAvPlayerGetStreamInfo",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAvPlayer")]
public static int AvPlayerGetStreamInfo(CpuContext ctx) =>
GetStreamInfoCore(ctx, StreamInfoSize);
private static int GetStreamInfoCore(CpuContext ctx, int infoSize)
public static int AvPlayerGetStreamInfo(CpuContext ctx)
{
var streamIndex = unchecked((uint)ctx[CpuRegister.Rsi]);
var infoAddress = ctx[CpuRegister.Rdx];
@@ -617,7 +578,7 @@ public static class AvPlayerExports
return SetReturn(ctx, InvalidParameters);
}
Span<byte> info = stackalloc byte[infoSize];
Span<byte> info = stackalloc byte[StreamInfoSize];
info.Clear();
BinaryPrimitives.WriteUInt32LittleEndian(info[0..], streamIndex); // 0=video, 1=audio
if (streamIndex == 0)
@@ -1048,7 +1009,7 @@ public static class AvPlayerExports
{
return false;
}
var ffprobe = GetFfprobePath(ffmpeg, OperatingSystem.IsWindows());
var ffprobe = Path.Combine(Path.GetDirectoryName(ffmpeg) ?? string.Empty, "ffprobe");
if (!File.Exists(ffprobe))
{
return false;
@@ -1131,33 +1092,13 @@ public static class AvPlayerExports
}
}
private static string? FindFfmpeg() =>
FindFfmpeg(
Environment.GetEnvironmentVariable("SHARPEMU_FFMPEG_PATH"),
Environment.GetEnvironmentVariable("PATH"),
OperatingSystem.IsWindows());
internal static string? FindFfmpeg(
string? configured,
string? searchPath,
bool isWindows)
private static string? FindFfmpeg()
{
var configured = Environment.GetEnvironmentVariable("SHARPEMU_FFMPEG_PATH");
if (!string.IsNullOrWhiteSpace(configured) && File.Exists(configured))
{
return configured;
}
var executable = isWindows ? "ffmpeg.exe" : "ffmpeg";
foreach (var directory in (searchPath ?? string.Empty)
.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries))
{
var candidate = Path.Combine(RemovePathQuotes(directory), executable);
if (File.Exists(candidate))
{
return candidate;
}
}
foreach (var candidate in new[] { "/opt/homebrew/bin/ffmpeg", "/usr/local/bin/ffmpeg" })
{
if (File.Exists(candidate))
@@ -1168,16 +1109,6 @@ public static class AvPlayerExports
return null;
}
internal static string GetFfprobePath(string ffmpeg, bool isWindows) =>
Path.Combine(
Path.GetDirectoryName(ffmpeg) ?? string.Empty,
isWindows ? "ffprobe.exe" : "ffprobe");
private static string RemovePathQuotes(string directory) =>
directory.Length >= 2 && directory[0] == '"' && directory[^1] == '"'
? directory[1..^1]
: directory;
internal static string? ResolveGuestPath(string guestPath)
{
if (string.IsNullOrWhiteSpace(guestPath))
@@ -1187,9 +1118,7 @@ public static class AvPlayerExports
var normalized = guestPath.Replace('\\', '/');
var fileReference = normalized.StartsWith("file:", StringComparison.OrdinalIgnoreCase);
var unrealProjectRelative =
normalized.StartsWith("../", StringComparison.Ordinal) ||
normalized.StartsWith("./", StringComparison.Ordinal);
var unrealProjectRelative = false;
if (normalized.StartsWith("file://", StringComparison.OrdinalIgnoreCase) &&
Uri.TryCreate(normalized, UriKind.Absolute, out var uri) &&
uri.IsFile)
@@ -1220,10 +1149,7 @@ public static class AvPlayerExports
if (unrealProjectRelative)
{
if (!TryRemoveUnrealLeadingDotSegments(normalized, out normalized))
{
return null;
}
normalized = RemoveUnrealLeadingDotSegments(normalized);
}
var app0 = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
@@ -1307,20 +1233,15 @@ public static class AvPlayerExports
}
}
private static bool TryRemoveUnrealLeadingDotSegments(
string guestPath,
out string normalized)
private static string RemoveUnrealLeadingDotSegments(string guestPath)
{
var removedParent = false;
while (guestPath.StartsWith("../", StringComparison.Ordinal) ||
guestPath.StartsWith("./", StringComparison.Ordinal))
{
removedParent |= guestPath.StartsWith("../", StringComparison.Ordinal);
guestPath = guestPath[(guestPath.IndexOf('/') + 1)..];
}
normalized = guestPath;
return !removedParent || guestPath.Contains('/');
return guestPath;
}
private static bool TryDecodeFileReference(string encoded, out string decoded)
+9 -14
View File
@@ -29,9 +29,10 @@ internal static class Bink2MovieBridge
private static bool _availabilityReported;
/// <summary>
/// Returns true only when movie skipping was explicitly requested. Without
/// a host adapter the guest must be allowed to run the Bink implementation
/// statically linked into its executable.
/// Returns true when the guest should receive a normal "file not found"
/// result for a Bink movie. This is the safe default without a decoder:
/// games that treat movies as optional fall through to their next state
/// rather than submitting an empty Bink GPU texture forever.
/// </summary>
internal static bool ShouldSkipGuestMovie(string hostPath) =>
hostPath.EndsWith(".bk2", StringComparison.OrdinalIgnoreCase) &&
@@ -52,18 +53,12 @@ internal static class Bink2MovieBridge
return;
}
var mode = ResolveMode();
if (mode == MovieMode.Dummy)
if (ResolveMode() == MovieMode.Dummy)
{
AttachDummyMovieLocked(hostPath);
return;
}
if (mode != MovieMode.Native)
{
return;
}
var adapter = GetAdapterLocked();
if (adapter is null)
{
@@ -170,15 +165,16 @@ internal static class Bink2MovieBridge
return MovieMode.Skip;
}
// Prefer the optional host adapter when one is supplied. Otherwise let
// the game's statically linked Bink implementation consume the file.
// With no SDK adapter present, returning "not found" makes optional
// cinematics advance. Supplying either an explicit path or the normal
// side-by-side adapter enables native playback automatically.
if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("SHARPEMU_BINK2_BRIDGE")) ||
EnumerateAdapterCandidates().Any(File.Exists))
{
return MovieMode.Native;
}
return MovieMode.Guest;
return MovieMode.Skip;
}
private static void AttachDummyMovieLocked(string hostPath)
@@ -339,7 +335,6 @@ internal static class Bink2MovieBridge
private enum MovieMode
{
Guest,
Skip,
Dummy,
Native,
+2 -121
View File
@@ -2,7 +2,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers;
using System.Numerics;
namespace SharpEmu.Libs.Gpu;
@@ -16,125 +15,7 @@ namespace SharpEmu.Libs.Gpu;
/// </summary>
internal static class GuestDataPool
{
public static ArrayPool<byte> Shared { get; } = new BoundedByteArrayPool(
public static ArrayPool<byte> Shared { get; } = ArrayPool<byte>.Create(
maxArrayLength: 16 * 1024 * 1024,
maxCachedBytes: 256UL * 1024 * 1024,
maxArraysPerBucket: 8);
public static void Trim() => ((BoundedByteArrayPool)Shared).Trim();
private sealed class BoundedByteArrayPool : ArrayPool<byte>
{
private readonly object _gate = new();
private readonly int _maxArrayLength;
private readonly ulong _maxCachedBytes;
private readonly int _maxArraysPerBucket;
private readonly Dictionary<int, Stack<byte[]>> _cachedByBucket = [];
private readonly HashSet<byte[]> _leases =
new(System.Collections.Generic.ReferenceEqualityComparer.Instance);
private ulong _cachedBytes;
public BoundedByteArrayPool(
int maxArrayLength,
ulong maxCachedBytes,
int maxArraysPerBucket)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxArrayLength);
ArgumentOutOfRangeException.ThrowIfZero(maxCachedBytes);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxArraysPerBucket);
_maxArrayLength = maxArrayLength;
_maxCachedBytes = maxCachedBytes;
_maxArraysPerBucket = maxArraysPerBucket;
}
public override byte[] Rent(int minimumLength)
{
ArgumentOutOfRangeException.ThrowIfNegative(minimumLength);
var length = GetAllocationLength(minimumLength);
byte[]? array = null;
lock (_gate)
{
if (length <= _maxArrayLength &&
_cachedByBucket.TryGetValue(length, out var bucket) &&
bucket.TryPop(out array))
{
_cachedBytes -= (ulong)array.LongLength;
}
array ??= new byte[length];
_leases.Add(array);
}
return array;
}
public override void Return(byte[] array, bool clearArray = false)
{
ArgumentNullException.ThrowIfNull(array);
lock (_gate)
{
if (!_leases.Remove(array))
{
return;
}
}
if (clearArray)
{
Array.Clear(array);
}
lock (_gate)
{
if (array.Length > _maxArrayLength ||
!IsBucketLength(array.Length) ||
(ulong)array.LongLength > _maxCachedBytes -
Math.Min(_cachedBytes, _maxCachedBytes))
{
return;
}
if (!_cachedByBucket.TryGetValue(array.Length, out var bucket))
{
bucket = new Stack<byte[]>();
_cachedByBucket.Add(array.Length, bucket);
}
if (bucket.Count >= _maxArraysPerBucket)
{
return;
}
bucket.Push(array);
_cachedBytes += (ulong)array.LongLength;
}
}
public void Trim()
{
lock (_gate)
{
_cachedByBucket.Clear();
_cachedBytes = 0;
}
}
private int GetAllocationLength(int minimumLength)
{
if (minimumLength <= 16)
{
return 16;
}
if (minimumLength > _maxArrayLength)
{
return minimumLength;
}
return checked((int)BitOperations.RoundUpToPowerOf2((uint)minimumLength));
}
private static bool IsBucketLength(int length) =>
length >= 16 && (length & (length - 1)) == 0;
}
maxArraysPerBucket: 96);
}
+2 -9
View File
@@ -27,12 +27,7 @@ internal sealed record GuestDrawTexture(
uint Pitch = 0,
uint TileMode = 0,
uint DstSelect = 0xFAC,
GuestSampler Sampler = default,
// Guest CPU write-tracker generation of the memory RgbaPixels was read
// from; -1 when the range is untracked or the pixels were not read here.
long WriteGeneration = -1,
bool ArrayedView = false,
uint ArrayLayers = 1);
GuestSampler Sampler = default);
/// <summary>Raw guest sampler descriptor dwords, copied verbatim from guest memory.</summary>
internal readonly record struct GuestSampler(
@@ -53,9 +48,7 @@ internal readonly record struct TextureContentIdentity(
uint DstSelect,
uint TileMode,
uint Pitch,
GuestSampler Sampler,
bool Arrayed = false,
uint ArrayLayers = 1);
GuestSampler Sampler);
internal sealed record GuestMemoryBuffer(
ulong BaseAddress,
+2 -2
View File
@@ -362,7 +362,7 @@ public static class KernelExports
ExportName = "open",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int Open(CpuContext ctx) => KernelMemoryCompatExports.PosixOpen(ctx);
public static int Open(CpuContext ctx) => KernelMemoryCompatExports.KernelOpenUnderscore(ctx);
[SysAbiExport(
Nid = "1G3lF1Gg1k8",
@@ -376,7 +376,7 @@ public static class KernelExports
ExportName = "fstat",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Fstat(CpuContext ctx) => KernelMemoryCompatExports.PosixFstat(ctx);
public static int Fstat(CpuContext ctx) => KernelMemoryCompatExports.KernelFstat(ctx);
[SysAbiExport(
Nid = "hcuQgD53UxM",
@@ -1,4 +1,4 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
@@ -65,10 +65,7 @@ public static partial class KernelMemoryCompatExports
private const uint HostPageExecuteReadWrite = 0x40;
private const uint HostPageExecuteWriteCopy = 0x80;
private const uint HostPageGuard = 0x100;
private const int Enoent = 2;
private const int Ebadf = 9;
private const int Enomem = 12;
private const int Eacces = 13;
private const int Efault = 14;
private const int Einval = 22;
private const int Erange = 34;
@@ -1108,13 +1105,10 @@ public static partial class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (count > 0 && !ctx.Memory.TryCopy(destination, source, (ulong)count))
var payload = GC.AllocateUninitializedArray<byte>(count);
if (count > 0 && (!TryReadCompat(ctx, source, payload) || !TryWriteCompat(ctx, destination, payload)))
{
var payload = GC.AllocateUninitializedArray<byte>(count);
if (!TryReadCompat(ctx, source, payload) || !TryWriteCompat(ctx, destination, payload))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = destination;
@@ -1548,13 +1542,7 @@ public static partial class KernelMemoryCompatExports
ExportName = "close",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixClose(CpuContext ctx)
{
var result = KernelCloseCore(ctx, unchecked((int)ctx[CpuRegister.Rdi]));
return result == (int)OrbisGen2Result.ORBIS_GEN2_OK
? 0
: PosixFailure(ctx, result, notFoundErrno: Ebadf);
}
public static int PosixClose(CpuContext ctx) => KernelCloseCore(ctx, unchecked((int)ctx[CpuRegister.Rdi]));
[SysAbiExport(
Nid = "UK2Tl2DWUns",
@@ -1611,29 +1599,6 @@ public static partial class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// Translates a failed raw Orbis kernel result into the libc/POSIX ABI:
// return -1 with errno set. The raw sceKernel* implementations report the
// 0x8002xxxx sentinel through the return value, but the POSIX-named exports
// (open/fstat/close/read/write/stat) are called by libc code that expects a
// negative result on failure. Leaking the raw sentinel makes callers store
// it as a "valid" fd or handle and later dereference it - the null-pointer
// access violation seen when Unity's IL2CPP file layer probes an absent
// il2cpp.usym. fd-based calls pass notFoundErrno=Ebadf; path-based calls
// leave the Enoent default.
private static int PosixFailure(CpuContext ctx, int orbisResult, int notFoundErrno = Enoent)
{
var errno = orbisResult switch
{
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT => Einval,
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT => Efault,
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED => Eacces,
_ => notFoundErrno,
};
KernelRuntimeCompatExports.TrySetErrno(ctx, errno);
ctx[CpuRegister.Rax] = ulong.MaxValue;
return -1;
}
[SysAbiExport(
Nid = "E6ao34wPw+U",
ExportName = "stat",
@@ -1642,29 +1607,23 @@ public static partial class KernelMemoryCompatExports
public static int PosixStat(CpuContext ctx)
{
var result = KernelStat(ctx);
return result == (int)OrbisGen2Result.ORBIS_GEN2_OK
? 0
: PosixFailure(ctx, result);
}
if (result == (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
return 0;
}
// POSIX open(2): translates a failed raw open into -1/errno. On success
// KernelOpenUnderscore already writes the fd into RAX (the import bridge
// prefers a written RAX over the return value), so returning 0 is correct.
public static int PosixOpen(CpuContext ctx)
{
var result = KernelOpenUnderscore(ctx);
return result == (int)OrbisGen2Result.ORBIS_GEN2_OK
? 0
: PosixFailure(ctx, result);
}
// POSIX fstat(2): a bad fd maps to EBADF rather than the path-oriented ENOENT.
public static int PosixFstat(CpuContext ctx)
{
var result = KernelFstat(ctx);
return result == (int)OrbisGen2Result.ORBIS_GEN2_OK
? 0
: PosixFailure(ctx, result, notFoundErrno: Ebadf);
// stat(2) follows the libc/POSIX ABI: failures return -1 and expose
// the reason through errno. Returning the raw Orbis kernel code here
// makes callers treat a missing file as a non-negative success value.
var errno = result switch
{
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT => Einval,
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT => Efault,
_ => 2, // ENOENT
};
KernelRuntimeCompatExports.TrySetErrno(ctx, errno);
ctx[CpuRegister.Rax] = ulong.MaxValue;
return -1;
}
[SysAbiExport(
@@ -1678,7 +1637,6 @@ public static partial class KernelMemoryCompatExports
var count = ctx[CpuRegister.Rsi];
var idsAddress = ctx[CpuRegister.Rdx];
var sizesAddress = ctx[CpuRegister.Rcx];
var errorIndexAddress = ctx[CpuRegister.R8];
if (pathListAddress == 0 || count == 0 || sizesAddress == 0 || count > 1024)
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Einval);
@@ -1703,8 +1661,12 @@ public static partial class KernelMemoryCompatExports
var hostPath = ResolveGuestPath(guestPath);
if (!TryGetAprFileSize(hostPath, out var fileSize))
{
// Stop at the first miss and report its index.
// The caller can then use its normal file-open fallback.
// Per-file resolve: a missing entry gets an invalid id
// (0xFFFFFFFF, already written above) and size 0, and the batch
// CONTINUES. Aborting the whole batch on the first miss left the
// remaining paths unresolved and could stall the guest's asset
// streaming when a batch happens to include an absent (e.g.
// patch/DLC) file; the caller checks per-file id/size.
LogIoTrace("apr_resolve", guestPath, $"host='{hostPath}' index={i} count={count} result=not_found");
if (sizesAddress != 0 &&
!TryWriteUInt64Compat(ctx, sizesAddress + (i * sizeof(ulong)), 0))
@@ -1713,16 +1675,7 @@ public static partial class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (errorIndexAddress != 0 &&
!TryWriteUInt32Compat(ctx, errorIndexAddress, (uint)i))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
KernelRuntimeCompatExports.TrySetErrno(ctx, 2); // ENOENT
ctx[CpuRegister.Rax] = ulong.MaxValue;
return -1;
continue;
}
var fileId = AmprFileRegistry.Register(guestPath, hostPath);
@@ -2140,15 +2093,7 @@ public static partial class KernelMemoryCompatExports
ExportName = "read",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixRead(CpuContext ctx)
{
// On success KernelReadUnderscore writes the byte count into RAX, which
// the import bridge prefers over this return value.
var result = KernelReadUnderscore(ctx);
return result == (int)OrbisGen2Result.ORBIS_GEN2_OK
? 0
: PosixFailure(ctx, result, notFoundErrno: Ebadf);
}
public static int PosixRead(CpuContext ctx) => KernelReadUnderscore(ctx);
[SysAbiExport(
Nid = "Cg4srZ6TKbU",
@@ -2347,15 +2292,7 @@ public static partial class KernelMemoryCompatExports
ExportName = "write",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixWrite(CpuContext ctx)
{
// On success KernelWriteUnderscore writes the byte count into RAX, which
// the import bridge prefers over this return value.
var result = KernelWriteUnderscore(ctx);
return result == (int)OrbisGen2Result.ORBIS_GEN2_OK
? 0
: PosixFailure(ctx, result, notFoundErrno: Ebadf);
}
public static int PosixWrite(CpuContext ctx) => KernelWriteUnderscore(ctx);
[SysAbiExport(
Nid = "4wSze92BhLI",
@@ -2390,37 +2327,6 @@ public static partial class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "smIj7eqzZE8",
ExportName = "clock_getres",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int ClockGetres(CpuContext ctx)
{
var timespecAddress = ctx[CpuRegister.Rsi];
// POSIX allows a null resolution pointer: the call then only validates
// the clock id, which every id a title passes here does.
if (timespecAddress == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// clock_gettime above is backed by DateTimeOffset.UtcNow, whose tick is
// 100 ns, so that is the honest resolution to report rather than the 1 ns
// a caller might otherwise assume it can rely on.
const ulong ResolutionNanoseconds = 100;
if (!ctx.TryWriteUInt64(timespecAddress, 0) ||
!ctx.TryWriteUInt64(timespecAddress + sizeof(long), ResolutionNanoseconds))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "vNe1w4diLCs",
ExportName = "__tls_get_addr",
@@ -2921,51 +2827,12 @@ public static partial class KernelMemoryCompatExports
LibraryName = "libKernel")]
public static int KernelMapDirectMemory(CpuContext ctx)
{
return MapDirectMemoryCore(
ctx,
inOutAddressPointer: ctx[CpuRegister.Rdi],
length: ctx[CpuRegister.Rsi],
protection: unchecked((int)ctx[CpuRegister.Rdx]),
flags: ctx[CpuRegister.Rcx],
directMemoryStart: ctx[CpuRegister.R8],
alignment: ctx[CpuRegister.R9]);
}
[SysAbiExport(
Nid = "BQQniolj9tQ",
ExportName = "sceKernelMapDirectMemory2",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelMapDirectMemory2(CpuContext ctx)
{
// The "2" variant inserts a memoryType argument (rdx) ahead of v1's
// protection, shifting protection/flags/directMemoryStart down one
// register each and pushing alignment onto the stack (the 7th argument,
// at [rsp + 8], above the return address). The memoryType only selects
// cache/GPU access attributes, which this HLE does not model per
// mapping, so it is accepted but does not affect placement.
ulong alignment = 0;
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp] + sizeof(ulong), out alignment);
return MapDirectMemoryCore(
ctx,
inOutAddressPointer: ctx[CpuRegister.Rdi],
length: ctx[CpuRegister.Rsi],
protection: unchecked((int)ctx[CpuRegister.Rcx]),
flags: ctx[CpuRegister.R8],
directMemoryStart: ctx[CpuRegister.R9],
alignment: alignment);
}
private static int MapDirectMemoryCore(
CpuContext ctx,
ulong inOutAddressPointer,
ulong length,
int protection,
ulong flags,
ulong directMemoryStart,
ulong alignment)
{
var inOutAddressPointer = ctx[CpuRegister.Rdi];
var length = ctx[CpuRegister.Rsi];
var protection = unchecked((int)ctx[CpuRegister.Rdx]);
var flags = ctx[CpuRegister.Rcx];
var directMemoryStart = ctx[CpuRegister.R8];
var alignment = ctx[CpuRegister.R9];
if (ShouldTraceDirectMemory())
{
Console.Error.WriteLine(
@@ -3078,7 +2945,6 @@ public static partial class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
GuestWriteWatch.OnDirectMapping(mappedAddress, length, protection);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -3461,7 +3327,7 @@ public static partial class KernelMemoryCompatExports
public static int KernelDirectMemoryQuery(CpuContext ctx)
{
var offset = ctx[CpuRegister.Rdi];
var flags = ctx[CpuRegister.Rsi];
_ = ctx[CpuRegister.Rsi]; // flags
var infoAddress = ctx[CpuRegister.Rdx];
var infoSize = ctx[CpuRegister.Rcx];
if (infoAddress == 0 || infoSize < 24)
@@ -3469,90 +3335,27 @@ public static partial class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (offset >= DirectMemorySizeBytes)
{
// Real hardware returns EACCES here (0x8002000D), not ENOENT.
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED;
}
var findNext = (flags & 1) != 0;
var found = false;
var matchStart = 0UL;
var matchEnd = 0UL;
var matchMemoryType = 0;
lock (_memoryGate)
{
var candidates = _directAllocations.Values
.Where(block => findNext
? block.Start + block.Length > offset
: offset >= block.Start && offset < block.Start + block.Length)
.OrderBy(block => block.Start);
foreach (var block in candidates)
foreach (var block in _directAllocations.Values)
{
found = true;
matchStart = block.Start;
matchEnd = block.Start + block.Length;
matchMemoryType = block.MemoryType;
break;
if (offset < block.Start || offset >= block.Start + block.Length)
{
continue;
}
if (!ctx.TryWriteUInt64(infoAddress, block.Start) ||
!ctx.TryWriteUInt64(infoAddress + sizeof(ulong), block.Start + block.Length) ||
!TryWriteInt32(ctx, infoAddress + (sizeof(ulong) * 2), block.MemoryType))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}
if (!found)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED;
}
if (!ctx.TryWriteUInt64(infoAddress, matchStart) ||
!ctx.TryWriteUInt64(infoAddress + sizeof(ulong), matchEnd) ||
!TryWriteInt32(ctx, infoAddress + (sizeof(ulong) * 2), matchMemoryType))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
/// <summary>
/// POSIX alias of <see cref="KernelMprotect"/>; identical (addr, len, prot)
/// argument order. Imported by libcohtml, whose embedded V8 changes page
/// permissions through this name when moving JIT pages between writable and
/// executable.
/// </summary>
[SysAbiExport(
Nid = "YQOfxL4QfeU",
ExportName = "mprotect",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixMprotect(CpuContext ctx) => KernelMprotect(ctx);
/// <summary>
/// POSIX alias of <see cref="KernelMunmap"/>; identical (addr, len) argument
/// order.
/// </summary>
[SysAbiExport(
Nid = "UqDGjXA5yUM",
ExportName = "munmap",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixMunmap(CpuContext ctx) => KernelMunmap(ctx);
/// <summary>
/// Reports the 16 KiB page granularity this backend maps and aligns against
/// (<see cref="OrbisPageSize"/>), not the host's 4 KiB. An allocator that
/// rounded to the host value would hand back sub-page offsets that every
/// mapping call here then rejects for misalignment.
/// </summary>
[SysAbiExport(
Nid = "k+AXqu2-eBc",
ExportName = "getpagesize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixGetPageSize(CpuContext ctx)
{
ctx[CpuRegister.Rax] = OrbisPageSize;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
[SysAbiExport(
@@ -4685,8 +4488,7 @@ public static partial class KernelMemoryCompatExports
allowSearch: false,
allowAllocateAtAlternative: false,
"reserve fixed range",
out _,
backPartialOverlap: true);
out _);
}
internal static bool IsGuestRangeBacked(CpuContext ctx, ulong address, ulong length)
@@ -4887,7 +4689,7 @@ public static partial class KernelMemoryCompatExports
!guestPath.StartsWith("/", StringComparison.Ordinal) &&
!guestPath.StartsWith("\\", StringComparison.Ordinal))
{
var relative = NormalizeMountRelativePath(guestPath);
var relative = guestPath.Replace('/', Path.DirectorySeparatorChar);
return Path.Combine(app0Root, relative);
}
}
@@ -4962,40 +4764,12 @@ public static partial class KernelMemoryCompatExports
return _cachedApp0Root;
}
// Resolves "." and ".." inside a mount-relative guest path and clamps the
// result at the mount root, so a guest path can never escape into the host
// filesystem. Unreal Engine titles depend on this: their base directory is
// <app>/binaries/<platform>, so they address content with "../../../"
// prefixes that land back inside /app0 on real hardware. Combining those
// raw against the app0 root walked out of the game folder entirely, so the
// title enumerated an unrelated host directory and never found its .pak files.
private static string NormalizeMountRelativePath(string relativePath)
{
var segments = relativePath.Split(
new[] { '/', '\\' },
StringSplitOptions.RemoveEmptyEntries);
var resolved = new List<string>(segments.Length);
foreach (var segment in segments)
{
if (segment == ".")
{
continue;
}
if (segment == "..")
{
if (resolved.Count > 0)
{
resolved.RemoveAt(resolved.Count - 1);
}
continue;
}
resolved.Add(segment);
}
return string.Join(Path.DirectorySeparatorChar, resolved);
return relativePath
.TrimStart('/', '\\')
.Replace('/', Path.DirectorySeparatorChar)
.Replace('\\', Path.DirectorySeparatorChar);
}
private static string ResolveDevlogAppRoot()
@@ -5818,74 +5592,6 @@ public static partial class KernelMemoryCompatExports
return true;
}
/// <summary>
/// Inserts <paramref name="replacement"/> into the tracked-region table,
/// carving it out of any regions it overlaps and preserving the parts of
/// those regions that fall outside it.
/// </summary>
/// <remarks>
/// The table is a SortedList keyed by start address, so assigning directly
/// destroys any existing entry that happens to share a start address. That
/// silently discarded enclosing reservations: a title reserves a large range
/// and then commits a small mapping at the same base, and the record of
/// everything past the small mapping disappears — leaving sceKernelVirtualQuery
/// unable to find memory the guest legitimately owns.
///
/// Carving also keeps the table non-overlapping. Previously a new region
/// starting inside an existing one produced two overlapping entries, which
/// the ordered scan in TryFindVirtualQueryRegionLocked is not written to
/// expect.
/// </remarks>
private static void ReplaceMappedRegionRangeLocked(MappedRegion replacement)
{
if (replacement.Length == 0 ||
!TryAddU64(replacement.Address, replacement.Length, out var replacementEnd))
{
_mappedRegions[replacement.Address] = replacement;
return;
}
var start = replacement.Address;
List<MappedRegion>? overlapping = null;
foreach (var region in _mappedRegions.Values)
{
if (region.Length == 0 ||
!TryAddU64(region.Address, region.Length, out var regionEnd))
{
continue;
}
if (region.Address < replacementEnd && regionEnd > start)
{
(overlapping ??= []).Add(region);
}
}
if (overlapping is not null)
{
foreach (var region in overlapping)
{
_mappedRegions.Remove(region.Address);
}
foreach (var region in overlapping)
{
var regionEnd = region.Address + region.Length;
if (region.Address < start)
{
AddMappedRegionSliceLocked(region, region.Address, start, region.Protection);
}
if (regionEnd > replacementEnd)
{
AddMappedRegionSliceLocked(region, replacementEnd, regionEnd, region.Protection);
}
}
}
_mappedRegions[start] = replacement;
}
private static void AddMappedRegionSliceLocked(
MappedRegion source,
ulong start,
@@ -6304,7 +6010,7 @@ public static partial class KernelMemoryCompatExports
return highWaterMark;
}
private static unsafe bool TryReadHostMemory(ulong address, Span<byte> destination)
private static bool TryReadHostMemory(ulong address, Span<byte> destination)
{
if (destination.IsEmpty || !IsHostRangeAccessible(address, (ulong)destination.Length, writeAccess: false))
{
@@ -6313,7 +6019,9 @@ public static partial class KernelMemoryCompatExports
try
{
new ReadOnlySpan<byte>((void*)address, destination.Length).CopyTo(destination);
var temporary = new byte[destination.Length];
Marshal.Copy((nint)address, temporary, 0, temporary.Length);
temporary.AsSpan().CopyTo(destination);
return true;
}
catch
@@ -6353,41 +6061,6 @@ public static partial class KernelMemoryCompatExports
return false;
}
internal static bool TryReadShaderGuestMemory(
ulong address,
Span<byte> destination)
{
if (destination.IsEmpty)
{
return true;
}
if (TryReadTrackedLibcHeap(address, destination))
{
return true;
}
var length = (ulong)destination.Length;
lock (_memoryGate)
{
if (TryFindVirtualQueryRegionLocked(
address,
findNext: false,
out var region) &&
length <= region.Length &&
address >= region.Address &&
length <= region.Address + region.Length - address)
{
return TryReadHostMemory(address, destination);
}
}
// Direct execution uses guest virtual addresses as host virtual addresses.
// Some native mmap paths predate _mappedRegions tracking, so retain the same
// committed/readable-page fallback used by the libc compatibility layer.
return TryReadHostMemory(address, destination);
}
internal static bool TryReadTrackedLibcHeapGpuAlias(
ulong packedAddress,
Span<byte> destination)
@@ -6667,7 +6340,7 @@ public static partial class KernelMemoryCompatExports
return value != 0 && (value & (value - 1)) == 0;
}
private static unsafe bool TryWriteHostMemory(ulong address, ReadOnlySpan<byte> source)
private static bool TryWriteHostMemory(ulong address, ReadOnlySpan<byte> source)
{
if (source.IsEmpty || !IsHostRangeAccessible(address, (ulong)source.Length, writeAccess: true))
{
@@ -6676,7 +6349,8 @@ public static partial class KernelMemoryCompatExports
try
{
source.CopyTo(new Span<byte>((void*)address, source.Length));
var temporary = source.ToArray();
Marshal.Copy(temporary, 0, (nint)address, temporary.Length);
return true;
}
catch
@@ -6703,37 +6377,20 @@ public static partial class KernelMemoryCompatExports
return false;
}
var endAddress = address + length - 1;
var currentAddress = address;
while (currentAddress <= endAddress)
if (!TryQueryHostPage(address, out var startInfo) || !HasRequiredProtection(startInfo.Protect, writeAccess))
{
if (!TryQueryHostPage(currentAddress, out var info) ||
!HasRequiredProtection(info.Protect, writeAccess))
{
return false;
}
return false;
}
var regionBase = unchecked((ulong)info.BaseAddress);
var regionSize = (ulong)info.RegionSize;
if (regionSize == 0 ||
regionBase > currentAddress ||
ulong.MaxValue - regionBase < regionSize)
{
return false;
}
var endAddress = address + length - 1;
if (endAddress == address)
{
return true;
}
var regionEnd = regionBase + regionSize;
if (regionEnd <= currentAddress)
{
return false;
}
if (regionEnd > endAddress)
{
return true;
}
currentAddress = regionEnd;
if (!TryQueryHostPage(endAddress, out var endInfo) || !HasRequiredProtection(endInfo.Protect, writeAccess))
{
return false;
}
return true;
@@ -41,87 +41,18 @@ public static class KernelPthreadCompatExports
private sealed class PthreadMutexState
{
private long _ownerThreadId;
private int _recursionCount;
private int _queuedWaiterCount;
public Lock SyncRoot { get; } = new();
public ulong OwnerThreadId
{
get => unchecked((ulong)Volatile.Read(ref _ownerThreadId));
set => Volatile.Write(ref _ownerThreadId, unchecked((long)value));
}
public int RecursionCount
{
get => Volatile.Read(ref _recursionCount);
set => Volatile.Write(ref _recursionCount, value);
}
public int QueuedWaiterCount => Volatile.Read(ref _queuedWaiterCount);
public ulong OwnerThreadId { get; set; }
public int RecursionCount { get; set; }
public int Type { get; set; } = MutexTypeErrorCheck;
public int Protocol { get; set; }
public LinkedList<PthreadMutexWaiter> Waiters { get; } = new();
public bool TryAcquireUncontended(ulong threadId, bool allowWaiterBarge)
{
if (!allowWaiterBarge && QueuedWaiterCount != 0)
{
return false;
}
return TryAcquireOwner(threadId);
}
public bool TryAcquireOwner(ulong threadId)
{
if (Interlocked.CompareExchange(
ref _ownerThreadId,
unchecked((long)threadId),
0) != 0)
{
return false;
}
Volatile.Write(ref _recursionCount, 1);
return true;
}
public bool TryReleaseUncontended(ulong threadId)
{
if (QueuedWaiterCount != 0 || RecursionCount != 1)
{
return false;
}
Volatile.Write(ref _recursionCount, 0);
if (Interlocked.CompareExchange(
ref _ownerThreadId,
0,
unchecked((long)threadId)) == unchecked((long)threadId))
{
return true;
}
Volatile.Write(ref _recursionCount, 1);
return false;
}
public int IncrementRecursion() => Interlocked.Increment(ref _recursionCount);
public int DecrementRecursion() => Interlocked.Decrement(ref _recursionCount);
public void WaiterAddedLocked() => Interlocked.Increment(ref _queuedWaiterCount);
public void WaiterRemovedLocked() => Interlocked.Decrement(ref _queuedWaiterCount);
}
private sealed class PthreadMutexWaiter
{
public required ulong ThreadId { get; init; }
public required string WakeKey { get; init; }
public required bool Cooperative { get; set; }
public ManualResetEventSlim? HostSignal { get; set; }
public required bool Cooperative { get; init; }
public LinkedListNode<PthreadMutexWaiter>? Node { get; set; }
public int Granted;
}
@@ -163,10 +94,7 @@ public static class KernelPthreadCompatExports
public static int PthreadSelf(CpuContext ctx)
{
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
if (GuestThreadExecution.CurrentGuestThreadHandle != currentThreadHandle)
{
GuestThreadExecution.Scheduler?.RegisterGuestThreadContext(currentThreadHandle, ctx);
}
GuestThreadExecution.Scheduler?.RegisterGuestThreadContext(currentThreadHandle, ctx);
ctx[CpuRegister.Rax] = currentThreadHandle;
TracePthreadSelf(ctx, currentThreadHandle);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
@@ -211,13 +139,6 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "B5GmVDKwpn0",
ExportName = "pthread_yield",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadYield(CpuContext ctx) => PthreadYield(ctx);
[SysAbiExport(
Nid = "GBUY7ywdULE",
ExportName = "scePthreadRename",
@@ -650,30 +571,6 @@ public static class KernelPthreadCompatExports
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
/// <summary>
/// The POSIX-named alias of <see cref="PthreadOnce"/>. libKernel exports the
/// same routine under two NIDs, and shipped middleware links the plain name:
/// DOOM's libcohtml, PlayFab and party modules all import this one rather
/// than scePthreadOnce.
/// </summary>
[SysAbiExport(
Nid = "Z4QosVuAsA0",
ExportName = "pthread_once",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadOncePOSIX(CpuContext ctx) => PthreadOnce(ctx);
/// <summary>
/// The POSIX-named alias of <see cref="PthreadRename"/>, following the same
/// two-NID pattern as <see cref="PthreadOncePOSIX"/>.
/// </summary>
[SysAbiExport(
Nid = "9vyP6Z7bqzc",
ExportName = "pthread_rename_np",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadRenameNpPOSIX(CpuContext ctx) => PthreadRename(ctx);
private static int PthreadMutexInitCore(CpuContext ctx, ulong mutexAddress, ulong attrAddress)
{
if (mutexAddress == 0)
@@ -724,7 +621,7 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
lock (state.SyncRoot)
lock (state)
{
if (state.OwnerThreadId != 0 || state.RecursionCount != 0 || state.Waiters.Count != 0)
{
@@ -756,63 +653,11 @@ public static class KernelPthreadCompatExports
}
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
if (state.TryAcquireUncontended(currentThreadId, allowWaiterBarge: tryOnly))
{
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (state.OwnerThreadId == currentThreadId)
{
if (state.Type == MutexTypeRecursive)
{
state.IncrementRecursion();
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (!tryOnly && state.Type == MutexTypeAdaptiveNp &&
IsGuestTrackedSelfLock(ctx, mutexAddress, currentThreadId))
{
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
}
if (state.Type == MutexTypeAdaptiveNp)
{
var adaptiveResult = tryOnly
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY
: (int)OrbisGen2Result.ORBIS_GEN2_OK;
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock-idempotent", mutexAddress, resolvedAddress, state, currentThreadId, adaptiveResult);
return adaptiveResult;
}
if (state.Type == MutexTypeNormal)
{
if (tryOnly)
{
TracePthreadMutex(ctx, "trylock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
}
state.IncrementRecursion();
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
var ownedResult = tryOnly
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, ownedResult);
return ownedResult;
}
var canCooperativelyBlock = !tryOnly &&
GuestThreadExecution.IsGuestThread &&
GuestThreadExecution.TryGetCurrentImportCallFrame(out _);
PthreadMutexWaiter? waiter = null;
var acquiredWhileQueueing = false;
lock (state.SyncRoot)
lock (state)
{
if (state.OwnerThreadId == currentThreadId)
{
@@ -823,30 +668,7 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (!tryOnly && state.Type == MutexTypeAdaptiveNp &&
IsGuestTrackedSelfLock(ctx, mutexAddress, currentThreadId))
{
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
}
if (state.Type == MutexTypeAdaptiveNp)
{
if (tryOnly)
{
TracePthreadMutex(ctx, "trylock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
}
// Gen5 runtime wrappers can layer an adaptive lock call over
// scePthreadMutexLock for one logical acquisition, followed by
// only one unlock. Keep the duplicate acquisition idempotent so
// the matching unlock fully releases the HLE mutex.
TracePthreadMutex(ctx, "lock-idempotent", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (state.Type == MutexTypeNormal)
if (state.Type is MutexTypeNormal or MutexTypeAdaptiveNp)
{
if (tryOnly)
{
@@ -855,7 +677,7 @@ public static class KernelPthreadCompatExports
}
// Several Gen5 runtimes layer their own owner/count bookkeeping
// over a NORMAL kernel mutex. Returning EDEADLK here
// over a NORMAL or ADAPTIVE kernel mutex. Returning EDEADLK here
// leaves that guest bookkeeping out of sync with the HLE owner and
// turns the wrapper into a permanent lock/unlock retry loop. Keep
// the compatibility recursion used by the original implementation;
@@ -881,10 +703,10 @@ public static class KernelPthreadCompatExports
// waiter wedge a spin-on-trylock loop forever even though the mutex
// is free (owner==0). The blocking lock still honours FIFO so real
// blocked waiters are not starved by a barging locker.
if (state.OwnerThreadId == 0 &&
(tryOnly || state.Waiters.Count == 0) &&
state.TryAcquireOwner(currentThreadId))
if (state.OwnerThreadId == 0 && (tryOnly || state.Waiters.Count == 0))
{
state.OwnerThreadId = currentThreadId;
state.RecursionCount = 1;
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -896,14 +718,6 @@ public static class KernelPthreadCompatExports
}
waiter = EnqueueMutexWaiterLocked(state, currentThreadId, canCooperativelyBlock);
acquiredWhileQueueing = TryGrantMutexWaiterLocked(state, waiter);
}
if (acquiredWhileQueueing)
{
waiter!.HostSignal?.Dispose();
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (canCooperativelyBlock && waiter is not null &&
@@ -937,29 +751,8 @@ public static class KernelPthreadCompatExports
}
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
if (state.OwnerThreadId == currentThreadId)
{
if (state.RecursionCount > 1)
{
state.DecrementRecursion();
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (state.TryReleaseUncontended(currentThreadId))
{
if (state.QueuedWaiterCount != 0)
{
WakeFirstMutexWaiter(state);
}
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}
PthreadMutexWaiter? nextWaiter = null;
lock (state.SyncRoot)
string? nextWakeKey = null;
lock (state)
{
if (state.RecursionCount <= 0)
{
@@ -977,29 +770,16 @@ public static class KernelPthreadCompatExports
if (state.RecursionCount == 0)
{
state.OwnerThreadId = 0;
// Hand the mutex directly to the head waiter instead of only
// waking it and relying on it to re-acquire. A woken waiter that
// fails to self-grant (its wake races or is lost) would leave the
// mutex "free with a queued waiter"; the fast-acquire path refuses
// such a mutex (OwnerThreadId == 0 && Waiters.Count == 0), so every
// later locker — including the game's main thread — then queues
// behind a head that never advances and the process wedges.
if (state.Waiters.First is { } headNode &&
TryGrantMutexWaiterLocked(state, headNode.Value))
{
nextWaiter = headNode.Value;
if (!nextWaiter.Cooperative)
{
nextWaiter.HostSignal!.Set();
}
}
nextWakeKey = state.Waiters.First?.Value.Cooperative == true
? state.Waiters.First.Value.WakeKey
: null;
Monitor.PulseAll(state);
}
}
if (nextWaiter is { Cooperative: true })
if (nextWakeKey is not null)
{
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(nextWaiter.WakeKey, 1);
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(nextWakeKey, 1);
}
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
@@ -1459,7 +1239,7 @@ public static class KernelPthreadCompatExports
}
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
lock (mutexState.SyncRoot)
lock (mutexState)
{
if (mutexState.OwnerThreadId == 0 && mutexState.RecursionCount == 0)
{
@@ -1472,10 +1252,10 @@ public static class KernelPthreadCompatExports
// mutex held (the unlock below is skipped), wedging every thread
// that later blocks on pthread_mutex_lock. Adopt ownership so the
// unlock/wait/re-lock cycle is balanced and releases the mutex.
_ = mutexState.TryAcquireOwner(currentThreadId);
mutexState.OwnerThreadId = currentThreadId;
mutexState.RecursionCount = 1;
}
if (mutexState.OwnerThreadId != currentThreadId || mutexState.RecursionCount != 1)
else if (mutexState.OwnerThreadId != currentThreadId || mutexState.RecursionCount != 1)
{
return mutexState.OwnerThreadId == currentThreadId
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT
@@ -1644,7 +1424,6 @@ public static class KernelPthreadCompatExports
if (node.Value.ThreadId == threadId)
{
state.Waiters.Remove(node);
state.WaiterRemovedLocked();
node.Value.Node = null;
}
@@ -1659,10 +1438,8 @@ public static class KernelPthreadCompatExports
WakeKey = cooperative
? wakeKey ?? $"pthread_mutex_waiter:{Interlocked.Increment(ref _nextSynchronizationWaiterId)}"
: string.Empty,
HostSignal = cooperative ? null : new ManualResetEventSlim(initialState: false),
};
waiter.Node = state.Waiters.AddLast(waiter);
state.WaiterAddedLocked();
return waiter;
}
@@ -1672,7 +1449,7 @@ public static class KernelPthreadCompatExports
var mutex = new PthreadMutexState();
PthreadMutexWaiter first;
PthreadMutexWaiter second;
lock (mutex.SyncRoot)
lock (mutex)
{
first = EnqueueMutexWaiterLocked(mutex, 0x101, cooperative: false);
second = EnqueueMutexWaiterLocked(mutex, 0x202, cooperative: false);
@@ -1716,72 +1493,26 @@ public static class KernelPthreadCompatExports
return false;
}
if (!state.TryAcquireOwner(waiter.ThreadId))
{
return false;
}
state.Waiters.Remove(waiter.Node);
state.WaiterRemovedLocked();
waiter.Node = null;
state.OwnerThreadId = waiter.ThreadId;
state.RecursionCount = 1;
Volatile.Write(ref waiter.Granted, 1);
Monitor.PulseAll(state);
return true;
}
private static void WakeFirstMutexWaiter(PthreadMutexState state)
{
PthreadMutexWaiter? nextWaiter;
lock (state.SyncRoot)
{
if (state.OwnerThreadId != 0)
{
return;
}
nextWaiter = state.Waiters.First?.Value;
if (nextWaiter is { Cooperative: false })
{
nextWaiter.HostSignal!.Set();
}
}
if (nextWaiter is { Cooperative: true })
{
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(nextWaiter.WakeKey, 1);
}
}
private static int WaitForHostMutexLock(PthreadMutexState state, PthreadMutexWaiter waiter)
{
ManualResetEventSlim? hostSignal = null;
try
lock (state)
{
while (true)
while (!TryGrantMutexWaiterLocked(state, waiter))
{
lock (state.SyncRoot)
{
if (waiter.HostSignal is null)
{
waiter.Cooperative = false;
waiter.HostSignal = new ManualResetEventSlim(initialState: false);
}
hostSignal = waiter.HostSignal;
if (TryGrantMutexWaiterLocked(state, waiter))
{
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
hostSignal.Reset();
}
hostSignal.Wait();
Monitor.Wait(state);
}
}
finally
{
hostSignal?.Dispose();
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool TryGrantBlockedMutexLock(
@@ -1792,7 +1523,7 @@ public static class KernelPthreadCompatExports
PthreadMutexWaiter waiter)
{
var granted = false;
lock (state.SyncRoot)
lock (state)
{
granted = TryGrantMutexWaiterLocked(state, waiter);
}
@@ -1826,10 +1557,6 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
}
private static bool IsGuestTrackedSelfLock(CpuContext ctx, ulong mutexAddress, ulong currentThreadId) =>
KernelMemoryCompatExports.TryReadUInt64Compat(ctx, mutexAddress + 8, out var guestOwner) &&
guestOwner == currentThreadId;
private static bool CompleteCondWaiterLocked(
PthreadCondState state,
PthreadCondWaiter waiter,
@@ -1845,7 +1572,7 @@ public static class KernelPthreadCompatExports
waiter.TimeoutTimer?.Dispose();
waiter.TimeoutTimer = null;
lock (waiter.MutexState.SyncRoot)
lock (waiter.MutexState)
{
waiter.MutexWaiter = EnqueueMutexWaiterLocked(
waiter.MutexState,
@@ -1893,7 +1620,7 @@ public static class KernelPthreadCompatExports
return false;
}
lock (waiter.MutexState.SyncRoot)
lock (waiter.MutexState)
{
return TryGrantMutexWaiterLocked(waiter.MutexState, mutexWaiter);
}
@@ -860,18 +860,6 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
/// <summary>
/// The POSIX-named alias of <see cref="PthreadAttrGetschedparam"/>. libKernel
/// exports the same routine under two NIDs; middleware compiled against the
/// plain POSIX headers links this one rather than scePthreadAttrGetschedparam.
/// </summary>
[SysAbiExport(
Nid = "qlk9pSLsUmM",
ExportName = "pthread_attr_getschedparam",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadAttrGetschedparamPOSIX(CpuContext ctx) => PthreadAttrGetschedparam(ctx);
[SysAbiExport(
Nid = "FXPWHNk8Of0",
ExportName = "scePthreadAttrGetschedparam",
@@ -1145,90 +1133,6 @@ public static class KernelPthreadExtendedCompatExports
LibraryName = "libKernel")]
public static int PosixPthreadRwlockWrlock(CpuContext ctx) => PthreadRwlockWrlock(ctx);
[SysAbiExport(
Nid = "SFxTMOfuCkE",
ExportName = "pthread_rwlock_tryrdlock",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadRwlockTryrdlock(CpuContext ctx) =>
PthreadRwlockTryLockCore(ctx, ctx[CpuRegister.Rdi], write: false);
[SysAbiExport(
Nid = "XhWHn6P5R7U",
ExportName = "pthread_rwlock_trywrlock",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadRwlockTrywrlock(CpuContext ctx) =>
PthreadRwlockTryLockCore(ctx, ctx[CpuRegister.Rdi], write: true);
/// <summary>
/// Non-blocking counterpart of <see cref="PthreadRwlockLockCore"/>: acquires
/// only if the lock is free right now, otherwise reports BUSY.
/// </summary>
/// <remarks>
/// Deliberately not routed through TryAcquireBlockedRwlock. That helper exists
/// for the scheduler resume path and decrements WaitingWriters on success,
/// which is correct only for a thread that previously incremented it. A fresh
/// try never did, so reusing it would silently consume another thread's
/// waiter count and let a queued writer be skipped.
/// </remarks>
private static int PthreadRwlockTryLockCore(CpuContext ctx, ulong rwlockAddress, bool write)
{
if (rwlockAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: true, out var resolvedAddress, out var rwlock))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
lock (rwlock.SyncRoot)
{
if (write)
{
if (rwlock.WriterThreadId == currentThreadId || rwlock.GetReaderCount(currentThreadId) > 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
}
// Mirrors the blocking path's re-entrant compat-writer grant so the
// two agree on what counts as already owning the lock.
if (rwlock.CompatWriterCounts.GetValueOrDefault(currentThreadId) > 0)
{
rwlock.AddCompatWriter(currentThreadId);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (rwlock.WriterThreadId != 0 ||
rwlock.ReaderTotalCount != 0 ||
rwlock.CompatWriterTotalCount != 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
}
DetectRwlockWriterConflict(resolvedAddress, rwlock, currentThreadId, "trywrlock");
rwlock.WriterThreadId = currentThreadId;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (rwlock.WriterThreadId == currentThreadId)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
}
if (ReaderMustWaitForRwlock(rwlock, currentThreadId))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
}
rwlock.AddReader(currentThreadId);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}
[SysAbiExport(
Nid = "+L98PIbGttk",
ExportName = "scePthreadRwlockUnlock",
@@ -1915,94 +1819,4 @@ public static class KernelPthreadExtendedCompatExports
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
return ctx.Memory.TryWrite(address, bytes);
}
// POSIX-named aliases. libKernel exports each of these routines under two
// NIDs -- a scePthread* name and the plain POSIX name -- and middleware
// compiled against POSIX headers links the latter. Both take identical
// arguments and, per the convention already used by scePthreadOnce's alias,
// return the same OrbisGen2Result rather than translating to errno.
[SysAbiExport(
Nid = "a2P9wYGeZvc",
ExportName = "pthread_setprio",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadSetprioPOSIX(CpuContext ctx) => PthreadSetprio(ctx);
[SysAbiExport(
Nid = "FIs3-UQT9sg",
ExportName = "pthread_getschedparam",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadGetschedparamPOSIX(CpuContext ctx) => PthreadGetschedparam(ctx);
[SysAbiExport(
Nid = "vQm4fDEsWi8",
ExportName = "pthread_attr_getstack",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadAttrGetstackPOSIX(CpuContext ctx) => PthreadAttrGetstack(ctx);
[SysAbiExport(
Nid = "Ucsu-OK+els",
ExportName = "pthread_attr_get_np",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadAttrGetNpPOSIX(CpuContext ctx) => PthreadAttrGet(ctx);
[SysAbiExport(
Nid = "JarMIy8kKEY",
ExportName = "pthread_attr_setschedpolicy",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadAttrSetschedpolicyPOSIX(CpuContext ctx) => PthreadAttrSetschedpolicy(ctx);
[SysAbiExport(
Nid = "E+tyo3lp5Lw",
ExportName = "pthread_attr_setdetachstate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadAttrSetdetachstatePOSIX(CpuContext ctx) => PthreadAttrSetdetachstate(ctx);
[SysAbiExport(
Nid = "euKRgm0Vn2M",
ExportName = "pthread_attr_setschedparam",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadAttrSetschedparamPOSIX(CpuContext ctx) => PthreadAttrSetschedparam(ctx);
[SysAbiExport(
Nid = "7ZlAakEf0Qg",
ExportName = "pthread_attr_setinheritsched",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadAttrSetinheritschedPOSIX(CpuContext ctx) => PthreadAttrSetinheritsched(ctx);
[SysAbiExport(
Nid = "0qOtCR-ZHck",
ExportName = "pthread_attr_getstacksize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadAttrGetstacksizePOSIX(CpuContext ctx) => PthreadAttrGetstacksize(ctx);
[SysAbiExport(
Nid = "VUT1ZSrHT0I",
ExportName = "pthread_attr_getdetachstate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadAttrGetdetachstatePOSIX(CpuContext ctx) => PthreadAttrGetdetachstate(ctx);
[SysAbiExport(
Nid = "JKyG3SWyA10",
ExportName = "pthread_attr_setguardsize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadAttrSetguardsizePOSIX(CpuContext ctx) => PthreadAttrSetguardsize(ctx);
[SysAbiExport(
Nid = "JNkVVsVDmOk",
ExportName = "pthread_attr_getguardsize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadAttrGetguardsizePOSIX(CpuContext ctx) => PthreadAttrGetguardsize(ctx);
}
@@ -2058,13 +2058,6 @@ public static class KernelRuntimeCompatExports
LibraryName = "libKernel")]
public static int KernelNanosleep(CpuContext ctx) => NanosleepCore(ctx, posix: false);
[SysAbiExport(
Nid = "NhpspxdjEKU",
ExportName = "_nanosleep",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixNanosleepUnderscore(CpuContext ctx) => NanosleepCore(ctx, posix: true);
[SysAbiExport(
Nid = "yS8U2TGCe1A",
ExportName = "nanosleep",
@@ -428,22 +428,6 @@ public static class KernelSemaphoreCompatExports
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "GEnUkDZoUwY",
ExportName = "scePthreadSemInit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadSemInit(CpuContext ctx)
{
// scePthreadSemInit(sem, flag, value, name) seems to only support private semaphores
if (ctx[CpuRegister.Rsi] != 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
return PosixSemInit(ctx);
}
[SysAbiExport(
Nid = "YCV5dGGBcCo",
ExportName = "sem_wait",
@@ -462,13 +446,6 @@ public static class KernelSemaphoreCompatExports
return KernelWaitSema(ctx);
}
[SysAbiExport(
Nid = "C36iRE0F5sE",
ExportName = "scePthreadSemWait",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadSemWait(CpuContext ctx) => PosixSemWait(ctx);
[SysAbiExport(
Nid = "WBWzsRifCEA",
ExportName = "sem_trywait",
@@ -486,19 +463,6 @@ public static class KernelSemaphoreCompatExports
return KernelPollSema(ctx, handle, 1);
}
[SysAbiExport(
Nid = "H2a+IN9TP0E",
ExportName = "scePthreadSemTrywait",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadSemTryWait(CpuContext ctx)
{
var result = PosixSemTryWait(ctx);
return result == (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN)
: result;
}
[SysAbiExport(
Nid = "w5IHyvahg-o",
ExportName = "sem_timedwait",
@@ -535,13 +499,6 @@ public static class KernelSemaphoreCompatExports
return KernelSignalSema(ctx, handle, 1);
}
[SysAbiExport(
Nid = "aishVAiFaYM",
ExportName = "scePthreadSemPost",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadSemPost(CpuContext ctx) => PosixSemPost(ctx);
[SysAbiExport(
Nid = "Bq+LRV-N6Hk",
ExportName = "sem_getvalue",
@@ -592,13 +549,6 @@ public static class KernelSemaphoreCompatExports
return result;
}
[SysAbiExport(
Nid = "Vwc+L05e6oE",
ExportName = "scePthreadSemDestroy",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadSemDestroy(CpuContext ctx) => PosixSemDestroy(ctx);
private static bool TryGetPosixSemaphoreHandle(CpuContext ctx, ulong semaphoreAddress, out uint handle)
{
handle = 0;
@@ -1,140 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
using System.Threading;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Kernel;
// libKernel's address-wait primitives (sceKernelSyncOnAddress*) are the PS5's
// futex-style wait/wake: a thread parks on a guest address until another thread
// wakes that address. Guest runtimes (seen driving Juicy Realm, PPSA19268)
// build their own spinlocks/queues on top of it and call the wait in a hot
// loop; left unimplemented, every wait returns immediately and the runtime
// busy-spins forever (millions of calls, no forward progress).
//
// This implements wait/wake over the existing cooperative-block scheduler,
// keyed on the address. The real primitive takes a compare value so the wait
// only sleeps while the address still holds the expected value; that exact
// value is not recovered here, so each wait is given a bounded deadline and
// treated as a spurious-wakeup-tolerant park: a genuinely missed wake
// self-heals when the deadline expires and the guest re-checks its own
// condition, which futex callers already tolerate. A matching wake releases
// waiters immediately through the same key.
public static class KernelSyncOnAddressCompatExports
{
// Safety-net poll interval. Real releases come from the wake side (generation
// bump + WakeBlockedThreads); this only bounds how long a wait that genuinely
// raced/missed its wake stays parked before the guest re-evaluates. Kept
// large: a short interval turns every parked waiter into a hot re-poll that
// steals scheduler bandwidth from the threads that actually make progress
// (including the ones that would issue the wake), so it must be a rare last
// resort, not a spin substitute.
private static readonly TimeSpan WaitSelfHealTimeout = TimeSpan.FromMilliseconds(100);
// Per-address host gate for the non-cooperative (host main thread) fallback,
// which cannot use the guest-thread scheduler's block mechanism.
private static readonly ConcurrentDictionary<ulong, object> _hostAddressGates = new();
// Per-address wake generation. A wait captures the current generation and
// its wake predicate stays unsatisfied (keeps the thread parked) until a
// wake bumps it. This is what actually holds the thread blocked: a bare
// "always satisfied" predicate is treated as an immediate late-arrival by
// the dispatcher's race guard and never yields, leaving the guest to
// busy-spin. The generation also closes the register-vs-park race for free:
// a wake landing in that window bumps the generation, so the predicate is
// already satisfied and the guest correctly resumes at once.
private static readonly ConcurrentDictionary<ulong, long> _wakeGenerations = new();
private static long CurrentGeneration(ulong address) =>
_wakeGenerations.TryGetValue(address, out var generation) ? generation : 0;
private static string WakeKey(ulong address) => $"sceKernelSyncOnAddress:{address:X16}";
[SysAbiExport(
Nid = "Hc4CaR6JBL0",
ExportName = "sceKernelSyncOnAddressWait",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int SyncOnAddressWait(CpuContext ctx)
{
var address = ctx[CpuRegister.Rdi];
if (address == 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var observedGeneration = CurrentGeneration(address);
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(WaitSelfHealTimeout);
// Cooperative path: stay parked until a wake bumps this address's
// generation (or the deadline expires as a self-heal). The guest
// re-evaluates its own condition after resuming.
if (GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelSyncOnAddressWait",
WakeKey(address),
resumeHandler: () => (int)OrbisGen2Result.ORBIS_GEN2_OK,
wakeHandler: () => CurrentGeneration(address) != observedGeneration,
deadline))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
// Non-cooperative caller (host main thread): bounded host wait so a
// missed wake self-heals instead of hanging.
var gate = _hostAddressGates.GetOrAdd(address, static _ => new object());
lock (gate)
{
if (CurrentGeneration(address) == observedGeneration)
{
Monitor.Wait(gate, WaitSelfHealTimeout);
}
}
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "q2y-wDIVWZA",
ExportName = "sceKernelSyncOnAddressWake",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int SyncOnAddressWake(CpuContext ctx)
{
var address = ctx[CpuRegister.Rdi];
if (address == 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
// rsi carries the number of waiters to release (1 = wake-one, a large
// value = wake-all); default to all if it looks unset.
var requested = unchecked((long)ctx[CpuRegister.Rsi]);
var wakeCount = requested is > 0 and < int.MaxValue ? (int)requested : int.MaxValue;
// Bump the generation first so a wait that has registered but not yet
// parked sees the change and resumes instead of missing this wake.
_wakeGenerations.AddOrUpdate(address, 1, static (_, current) => current + 1);
GuestThreadExecution.Scheduler?.WakeBlockedThreads(WakeKey(address), wakeCount);
if (_hostAddressGates.TryGetValue(address, out var gate))
{
lock (gate)
{
Monitor.PulseAll(gate);
}
}
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
{
var value = (int)result;
ctx[CpuRegister.Rax] = unchecked((ulong)value);
return value;
}
}
@@ -18,8 +18,7 @@ internal static class KernelVirtualRangeAllocator
bool allowSearch,
bool allowAllocateAtAlternative,
string traceName,
out ulong mappedAddress,
bool backPartialOverlap = false)
out ulong mappedAddress)
{
mappedAddress = 0;
if (length == 0)
@@ -43,18 +42,6 @@ internal static class KernelVirtualRangeAllocator
return true;
}
// Fixed mappings must cover the whole requested window even when part of
// it is already backed by another allocation. The single-call AllocateAt
// below is all-or-nothing and fails outright on partial overlap, leaving
// the untouched pages unmapped for the guest to fault into. Fill the free
// pages directly instead.
if (backPartialOverlap &&
addressSpace.TryBackFixedRange(desiredAddress, length, executable))
{
mappedAddress = desiredAddress;
return true;
}
var allocated = addressSpace.AllocateAt(desiredAddress, length, executable, allowAllocateAtAlternative);
if (allocated == 0)
{
-206
View File
@@ -184,212 +184,6 @@ public static class NetExports
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
/// <summary>
/// POSIX alias of <see cref="NetSetsockopt"/>; identical
/// (fd, level, option, value, length) argument order.
/// </summary>
[SysAbiExport(
Nid = "fFxGkxF2bVo",
ExportName = "setsockopt",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixSetsockopt(CpuContext ctx) => NetSetsockopt(ctx);
/// <summary>
/// Reads back the socket options this backend actually tracks: SO_NBIO,
/// SO_REUSEADDR and SO_ERROR.
/// </summary>
/// <remarks>
/// Anything else returns EINVAL rather than a zero-filled buffer. A caller
/// that receives success for an option nobody stored would treat whatever
/// happens to be in its output buffer as the real setting, which is a harder
/// failure to trace than an explicit rejection.
/// </remarks>
[SysAbiExport(
Nid = "6O8EwYOgH9Y",
ExportName = "getsockopt",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixGetsockopt(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
var level = unchecked((int)ctx[CpuRegister.Rsi]);
var option = unchecked((int)ctx[CpuRegister.Rdx]);
var valueAddress = ctx[CpuRegister.Rcx];
var lengthAddress = ctx[CpuRegister.R8];
if (!_sockets.TryGetValue(id, out var socket))
{
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
}
if (valueAddress == 0 || lengthAddress == 0 || level != 0xFFFF)
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
Span<byte> lengthBytes = stackalloc byte[sizeof(int)];
if (!ctx.Memory.TryRead(lengthAddress, lengthBytes))
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
if (BinaryPrimitives.ReadInt32LittleEndian(lengthBytes) < sizeof(int))
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
int value;
switch (option)
{
// ORBIS_NET_SO_NBIO: mirrors what sceNetSetsockopt stored.
case 0x1200:
value = socket.Blocking ? 0 : 1;
break;
case 0x0004:
value = (int)socket.GetSocketOption(
SocketOptionLevel.Socket,
SocketOptionName.ReuseAddress)! != 0 ? 1 : 0;
break;
// ORBIS_NET_SO_ERROR: nothing here records per-socket async errors,
// so report "no pending error" rather than inventing one.
case 0x1007:
value = 0;
break;
default:
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
Span<byte> valueBytes = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(valueBytes, value);
BinaryPrimitives.WriteInt32LittleEndian(lengthBytes, sizeof(int));
if (!ctx.Memory.TryWrite(valueAddress, valueBytes) ||
!ctx.Memory.TryWrite(lengthAddress, lengthBytes))
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
TraceNet("socket.getsockopt", id, unchecked((uint)option), unchecked((uint)value), 0);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "fZOeZIOEmLw",
ExportName = "send",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixSend(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
var bufferAddress = ctx[CpuRegister.Rsi];
var length = unchecked((int)ctx[CpuRegister.Rdx]);
if (!_sockets.TryGetValue(id, out var socket))
{
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
}
if (length < 0 || (length != 0 && bufferAddress == 0))
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
if (length == 0)
{
return ctx.SetReturn(0);
}
var payload = new byte[length];
if (!ctx.Memory.TryRead(bufferAddress, payload))
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
try
{
var sent = socket.Send(payload, SocketFlags.None);
TraceNet("socket.send", id, unchecked((uint)length), unchecked((uint)sent), 0);
return ctx.SetReturn(sent);
}
catch (SocketException exception)
when (exception.SocketErrorCode == SocketError.WouldBlock)
{
return SetNetError(ctx, NetErrorWouldBlock, NetErrnoWouldBlock);
}
catch (SocketException)
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
catch (ObjectDisposedException)
{
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
}
}
/// <summary>
/// Formats a binary address as text. Pure conversion with no socket state,
/// so it behaves identically to the console version for AF_INET/AF_INET6.
/// </summary>
[SysAbiExport(
Nid = "5jRCs2axtr4",
ExportName = "inet_ntop",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixInetNtop(CpuContext ctx)
{
var family = unchecked((int)ctx[CpuRegister.Rdi]);
var sourceAddress = ctx[CpuRegister.Rsi];
var destinationAddress = ctx[CpuRegister.Rdx];
var destinationSize = unchecked((int)ctx[CpuRegister.Rcx]);
if (sourceAddress == 0 || destinationAddress == 0 || destinationSize <= 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
// ORBIS_NET_AF_INET / ORBIS_NET_AF_INET6, matching TryMapAddressFamily.
var addressLength = family switch
{
2 => 4,
28 => 16,
_ => 0,
};
if (addressLength == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var rawAddress = new byte[addressLength];
if (!ctx.Memory.TryRead(sourceAddress, rawAddress))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var text = new IPAddress(rawAddress).ToString();
var encoded = Encoding.ASCII.GetBytes(text);
// POSIX requires the terminator to fit as well; a truncated address string
// is worse than a reported failure because the caller cannot detect it.
if (encoded.Length + 1 > destinationSize)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var buffer = new byte[encoded.Length + 1];
encoded.CopyTo(buffer, 0);
if (!ctx.Memory.TryWrite(destinationAddress, buffer))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
// inet_ntop returns the destination pointer on success.
ctx[CpuRegister.Rax] = destinationAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "bErx49PgxyY",
ExportName = "sceNetBind",
-17
View File
@@ -69,23 +69,6 @@ public static class NpManagerExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
/// <summary>
/// Accepts the reachability callback and never invokes it. Reachability
/// transitions only ever fire on a real PSN connection, which an offline
/// session does not have, so registering successfully and staying silent is
/// the accurate emulation of a signed-out console rather than a stub.
/// </summary>
[SysAbiExport(
Nid = "hw5KNqAAels",
ExportName = "sceNpRegisterNpReachabilityStateCallback",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpManager")]
public static int NpRegisterNpReachabilityStateCallback(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "qQJfO8HAiaY",
ExportName = "sceNpRegisterStateCallbackA",
-19
View File
@@ -80,25 +80,6 @@ public static class NpTrophy2Exports
LibraryName = "libSceNpTrophy2")]
public static int NpTrophy2ShowTrophyList(CpuContext ctx) => ReturnOk(ctx);
/// <summary>
/// Gen5 ABI: context, handle, trophy id, then SceNpTrophy2Details and
/// SceNpTrophy2Data output pointers.
/// </summary>
/// <remarks>
/// Reports "no such trophy" rather than succeeding. Succeeding would require
/// filling both output structures, and their exact layouts are not confirmed
/// here — a title that trusted zeroed details would read an empty name and a
/// grade of zero as real data. NOT_FOUND is a documented outcome that callers
/// must already handle, so it degrades along a path the game tests.
/// </remarks>
[SysAbiExport(
Nid = "EwNylPdWUTM",
ExportName = "sceNpTrophy2GetTrophyInfo",
Target = Generation.Gen5,
LibraryName = "libSceNpTrophy2")]
public static int NpTrophy2GetTrophyInfo(CpuContext ctx) =>
SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
private static int WriteIdAndReturn(CpuContext ctx, ulong outAddress, ref int nextId)
{
if (outAddress == 0)
+7 -50
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
@@ -698,22 +698,14 @@ public static class PlayGoExports
var hasMetadata = File.Exists(playGoDat) || File.Exists(scenarioJson) || File.Exists(chunkDefsXml);
if (!hasMetadata)
{
// No PlayGo sidecar: derive the installed chunk set from the pak files
// actually present on disk. A locally dumped title has all of its data
// installed, and a package that splits content across chunks names them
// pakchunk<N>-<platform>.pak, so those N are exactly the chunks that
// exist. Reporting only chunk 0 told such a title its remaining content
// was missing: The Invincible (PPSA06426) ships pakchunk0..8 and spun
// forever re-querying scePlayGoGetLocus for a chunk that never became
// available. Available must stay true or scePlayGoOpen fails with
// NotSupportPlayGo (fatal PS5-component init failure for UE titles).
// Ids outside the discovered set still return BAD_CHUNK_ID, so
// title-side chunk enumeration still terminates.
var installedChunkIds = DiscoverInstalledChunkIds(app0Root);
TracePlayGo($"metadata_missing; fully-installed chunks=[{string.Join(',', installedChunkIds)}]");
// No PlayGo sidecar: report a fully-installed single chunk. Available must
// stay true or scePlayGoOpen fails with NotSupportPlayGo (fatal PS5-component
// init failure for UE titles); chunk 0 reports LocalFast and every other id
// returns BAD_CHUNK_ID, terminating title-side chunk enumeration.
TracePlayGo("metadata_missing; fully-installed single chunk");
return new PlayGoMetadata(
true,
installedChunkIds,
[(ushort)0],
PlayGoChunkIdKnowledge.Authoritative);
}
@@ -726,41 +718,6 @@ public static class PlayGoExports
: PlayGoChunkIdKnowledge.Authoritative);
}
// Chunk ids for a title that ships no PlayGo sidecar, taken from the
// pakchunk<N>-<platform>.pak files on disk. Chunk 0 is always included: it
// is the base chunk and must resolve even for a title with no pak files at
// all (which keeps the single-chunk behaviour for such titles).
private static ushort[] DiscoverInstalledChunkIds(string app0Root)
{
var ids = new SortedSet<ushort> { 0 };
try
{
foreach (var pakFile in Directory.EnumerateFiles(app0Root, "pakchunk*.pak", SearchOption.AllDirectories))
{
var name = Path.GetFileNameWithoutExtension(pakFile);
var digits = name.AsSpan("pakchunk".Length);
var length = 0;
while (length < digits.Length && char.IsAsciiDigit(digits[length]))
{
length++;
}
if (length > 0 && ushort.TryParse(digits[..length], out var chunkId))
{
ids.Add(chunkId);
}
}
}
catch (IOException)
{
}
catch (UnauthorizedAccessException)
{
}
return ids.ToArray();
}
private static ushort[] LoadChunkIds(string chunkDefsXml)
{
if (!File.Exists(chunkDefsXml))
+20 -17
View File
@@ -792,15 +792,28 @@ public static class SaveDataExports
var id = (uint)Interlocked.Increment(ref _nextTransactionResource);
// A small RDX value is a flag, and RCX contains the output address.
// A larger RDX value is the output address for the older ABI.
// The resource-out pointer's argument slot varies by SDK revision: some
// callers pass it in rdx, others in rcx (a 4-arg form where rdx holds a
// count/flag). Void Terrarium passes rdx=0x1 (not a pointer) and the
// real out-pointer in rcx. Probe the plausible candidates and write the
// handle to the first writable one instead of faulting on a bad rdx.
// This is a stub-level create (matches shadPS4's return-OK semantics);
// never return MEMORY_FAULT for it, or the guest treats savedata init as
// failed and never advances.
var resourceAddress = 0UL;
var selectedAddress = SelectTransactionResourceAddress(
ctx[CpuRegister.Rdx],
ctx[CpuRegister.Rcx]);
if (selectedAddress != 0 && TryWriteUInt32(ctx, selectedAddress, id))
foreach (var candidate in new[]
{
ctx[CpuRegister.Rdx],
ctx[CpuRegister.Rcx],
ctx[CpuRegister.R8],
ctx[CpuRegister.R9],
})
{
resourceAddress = selectedAddress;
if (candidate != 0 && TryWriteUInt32(ctx, candidate, id))
{
resourceAddress = candidate;
break;
}
}
TraceSaveData(
@@ -809,16 +822,6 @@ public static class SaveDataExports
return SetReturn(ctx, 0);
}
internal static ulong SelectTransactionResourceAddress(ulong rdx, ulong rcx)
{
if (rdx == 0)
{
return 0;
}
return rdx <= ushort.MaxValue ? rcx : rdx;
}
[SysAbiExport(
Nid = "lJUQuaKqoKY",
ExportName = "sceSaveDataDeleteTransactionResource",
+7 -10
View File
@@ -36,7 +36,6 @@ public static class PerfOverlay
private static long _presentedInWindow;
private static long _submittedInWindow;
private static long _drawsInWindow;
private static long _guestBufferCacheBytes;
// Refreshed once per second so per-frame fills never allocate.
private static long _statsWindowStart = Stopwatch.GetTimestamp();
@@ -75,8 +74,11 @@ public static class PerfOverlay
if (last != 0)
{
var milliseconds = (now - last) * 1000.0 / Stopwatch.Frequency;
_frameMilliseconds[_frameHistoryIndex] = milliseconds;
_frameHistoryIndex = (_frameHistoryIndex + 1) % FrameHistorySize;
if (milliseconds < 1000.0)
{
_frameMilliseconds[_frameHistoryIndex] = milliseconds;
_frameHistoryIndex = (_frameHistoryIndex + 1) % FrameHistorySize;
}
}
}
@@ -86,9 +88,6 @@ public static class PerfOverlay
/// <summary>Called per translated draw/dispatch executed.</summary>
public static void RecordDraw() => Interlocked.Increment(ref _drawsInWindow);
public static void SetGuestBufferCacheBytes(ulong bytes) =>
Interlocked.Exchange(ref _guestBufferCacheBytes, checked((long)bytes));
/// <summary>
/// Rasterizes the panel into a BGRA byte span of PanelWidth x PanelHeight.
/// Runs on the render thread.
@@ -166,7 +165,7 @@ public static class PerfOverlay
Environment.ProcessorCount;
_lastCpuTime = cpuTime;
var drawsPerFrame = _fps > 0 ? _drawsPerSecond / _fps : 0;
var drawsPerFrame = _fps > 0.5 ? _drawsPerSecond / _fps : 0;
var sessionStart = Interlocked.Read(ref _sessionStartTimestamp);
var elapsedSeconds = sessionStart == 0
? 0L
@@ -177,9 +176,7 @@ public static class PerfOverlay
_line1 = $"FPS {_fps:0.0} FLIP {_submittedFps:0.0} {_averageFrameMs:0.0} MS";
_line2 = $"DRAWS {_drawsPerSecond:0}/S {drawsPerFrame:0}/F Q {pendingWork}+{inFlightSubmissions}";
_line3 = $"ALLOC {_allocatedMbPerSecond:0.0} MB/S GC {_gen0PerWindow}/{_gen1PerWindow}/{_gen2PerWindow}";
var heapMb = GC.GetTotalMemory(false) / (1024 * 1024);
var guestBufferMb = Interlocked.Read(ref _guestBufferCacheBytes) / (1024 * 1024);
_line4 = $"MEM {heapMb}M BUF {guestBufferMb}M CPU {_cpuPercent:0}%";
_line4 = $"CPU {_cpuPercent:0}% HEAP {GC.GetTotalMemory(false) / (1024 * 1024)} MB F1 HIDE";
_line5 = $"TIME {elapsedHours:00}:{elapsedMinutes:00}:{elapsedRemainingSeconds:00}";
}
}
File diff suppressed because it is too large Load Diff
@@ -953,22 +953,22 @@ public static partial class Gen5SpirvTranslator
case "VPkMulF16":
case "VPkMinF16":
case "VPkMaxF16":
case "VPkFmaF16":
if (!TryEmitPackedF16(instruction, out result, out error))
{
return false;
}
break;
case "VFmaMixF32":
case "VFmaMixloF16":
case "VFmaMixhiF16":
if (!TryEmitFmaMix(instruction, destination, out result, out error))
{
return false;
}
break;
case "VPkFmaF16":
// Deliberately loud: a fused f16 FMA rounds the product+add once,
// whereas doing the multiply-add in f32 and rounding to f16 at the
// end double-rounds. Concrete miss: fma(0x4100, 0x7522, 0x04EA) is
// 0x7A6B fused but 0x7A6A via f32. Exact emulation (round-to-odd
// f32 product then RNE pack) is a planned follow-up slice.
error =
$"unsupported vop3p opcode {instruction.Opcode} " +
"(fused f16 FMA requires single-rounding; deferred to a later slice)";
return false;
default:
error = $"unsupported vector opcode {instruction.Opcode}";
return false;
@@ -1008,9 +1008,8 @@ public static partial class Gen5SpirvTranslator
// even. For add and mul this is bit-exact to a true f16 op (the f32 result
// rounds losslessly to f16 by the double-rounding theorem; a f16 product even
// fits in f32 exactly). min/max carry no rounding, so they are exact once the
// conversions are. v_pk_fma_f16 cannot be reproduced by a plain f32
// multiply-add plus a pack (that double-rounds), so it goes through the
// round-to-odd sequence in EmitPackedF16FusedMultiplyAdd instead.
// conversions are. v_pk_fma_f16 is intentionally not routed here because a
// fused f16 FMA cannot be reproduced by an f32 multiply-add plus a pack.
private bool TryEmitPackedF16(
Gen5ShaderInstruction instruction,
out uint result,
@@ -1024,8 +1023,13 @@ public static partial class Gen5SpirvTranslator
return false;
}
var sourceCount = instruction.Opcode == "VPkFmaF16" ? 3 : 2;
for (var index = 0; index < sourceCount; index++)
if (control.Clamp)
{
error = $"unsupported vop3p modifiers (clamp) for {instruction.Opcode}";
return false;
}
for (var index = 0; index < 2; index++)
{
var source = instruction.Sources[index];
if (source.Kind is not (Gen5OperandKind.VectorRegister or Gen5OperandKind.ScalarRegister))
@@ -1042,112 +1046,7 @@ public static partial class Gen5SpirvTranslator
return true;
}
// V_FMA_MIX_F32 / _MIXLO_F16 / _MIXHI_F16 (VOP3P opcodes 0x20 / 0x21 /
// 0x22). Unlike the packed v_pk_* ops these compute a single f32
// fma(a, b, c): each of the three sources is *independently* read as
// either a full f32 register/constant or one f16 half widened to f32,
// selected per operand by op_sel_hi (read as f16 when set) and op_sel
// (which half feeds the f32). For the mix ops the VOP3P neg_hi field is
// the absolute-value modifier and neg negates, applied abs-then-neg to
// match the hardware and shadPS4's GetSrcMix. _MIXLO / _MIXHI round the
// f32 result back to f16 and write it into the low / high 16 bits of
// vdst, leaving the other half intact.
private bool TryEmitFmaMix(
Gen5ShaderInstruction instruction,
uint destination,
out uint result,
out string error)
{
result = 0;
error = string.Empty;
if (instruction.Control is not Gen5Vop3pControl control)
{
error = $"missing vop3p control for {instruction.Opcode}";
return false;
}
var product = Bitcast(
_uintType,
Ext(
50,
_floatType,
EmitFmaMixOperand(instruction, control, 0),
EmitFmaMixOperand(instruction, control, 1),
EmitFmaMixOperand(instruction, control, 2)));
if (control.Clamp)
{
product = EmitClampToUnitInterval(product);
}
if (instruction.Opcode == "VFmaMixF32")
{
result = product;
return true;
}
// _MIXLO / _MIXHI: narrow to f16 and merge into one half of vdst.
var half = EmitFloatToHalf(product);
var existing = LoadV(destination);
result = instruction.Opcode == "VFmaMixloF16"
? BitwiseOr(BitwiseAnd(existing, UInt(0xFFFF_0000)), half)
: BitwiseOr(
BitwiseAnd(existing, UInt(0x0000_FFFF)),
ShiftLeftLogical(half, UInt(16)));
return true;
}
// Reads one V_FMA_MIX source as an f32. op_sel_hi selects whether a
// register operand is taken as an f16 (the half picked by op_sel, widened
// exactly to f32) or as a full f32; inline constants are always f32. The
// per-operand neg_hi bit takes the absolute value and neg negates, in that
// order (abs-then-neg), reusing the VOP3P modifier fields the way the mix
// ops define them rather than the packed low/high-lane meaning.
private uint EmitFmaMixOperand(
Gen5ShaderInstruction instruction,
Gen5Vop3pControl control,
int index)
{
var source = instruction.Sources[index];
var readAsHalf =
((control.OpSelHiMask >> index) & 1) != 0 &&
source.Kind is Gen5OperandKind.VectorRegister or Gen5OperandKind.ScalarRegister;
uint value;
if (readAsHalf)
{
var raw = GetRawSource(instruction, index);
var half = ((control.OpSelMask >> index) & 1) != 0
? ShiftRightLogical(raw, UInt(16))
: raw;
value = Bitcast(_floatType, EmitHalfToFloat(half));
}
else
{
value = GetFloatSource(instruction, index);
}
if (((control.NegHiMask >> index) & 1) != 0)
{
value = Ext(4, _floatType, value);
}
if (((control.NegLoMask >> index) & 1) != 0)
{
value = _module.AddInstruction(SpirvOp.FNegate, _floatType, value);
}
return value;
}
// Computes one result lane (low or high) as a packed 16-bit f16 value.
// The op runs in f32 and its result is narrowed back to f16 exactly (see
// EmitFloatToHalf). When the clamp modifier is set the pre-narrowing f32
// value is saturated to [0, 1] first; because 0.0 and 1.0 are exact in both
// f32 and f16 and the clamp is monotonic, clamping before the narrowing
// gives the same f16 the hardware produces by clamping the f16 result. For
// the fused multiply-add the pre-narrowing value is the round-to-odd f32
// from EmitPackedF16FusedMultiplyAdd, and round-to-odd preserves that
// equivalence through the final round-to-nearest-even.
private uint EmitPackedF16Lane(
Gen5ShaderInstruction instruction,
Gen5Vop3pControl control,
@@ -1155,113 +1054,15 @@ public static partial class Gen5SpirvTranslator
{
var left = EmitPackedF16Operand(instruction, control, 0, highLane);
var right = EmitPackedF16Operand(instruction, control, 1, highLane);
uint value;
if (instruction.Opcode == "VPkFmaF16")
var value = instruction.Opcode switch
{
var addend = EmitPackedF16Operand(instruction, control, 2, highLane);
value = EmitPackedF16FusedMultiplyAdd(left, right, addend);
}
else
{
value = Bitcast(_uintType, instruction.Opcode switch
{
"VPkAddF16" => _module.AddInstruction(SpirvOp.FAdd, _floatType, left, right),
"VPkMulF16" => _module.AddInstruction(SpirvOp.FMul, _floatType, left, right),
"VPkMinF16" => EmitPackedF16MinMax(left, right, isMax: false),
"VPkMaxF16" => EmitPackedF16MinMax(left, right, isMax: true),
_ => left,
});
}
if (control.Clamp)
{
value = EmitClampToUnitInterval(value);
}
return EmitFloatToHalf(value);
}
// Saturates an f32 bit pattern to [0, 1] the way the VOP3P clamp modifier
// does: below 0 (and NaN, since the ordered compare is false for it) becomes
// 0, above 1 becomes 1. Ordered compares match the hardware's NaN-to-zero
// behaviour without a separate IsNan test.
private uint EmitClampToUnitInterval(uint valueBits)
{
var value = Bitcast(_floatType, valueBits);
var aboveZero = _module.AddInstruction(SpirvOp.FOrdGreaterThan, _boolType, value, Float(0));
var lowerBounded = _module.AddInstruction(SpirvOp.Select, _floatType, aboveZero, value, Float(0));
var belowOne = _module.AddInstruction(SpirvOp.FOrdLessThan, _boolType, lowerBounded, Float(1));
var clamped = _module.AddInstruction(SpirvOp.Select, _floatType, belowOne, lowerBounded, Float(1));
return Bitcast(_uintType, clamped);
}
// Fused f16 multiply-add with a single rounding, emulated in f32 without the
// Float16 capability. The f32 product of two widened f16 values is exact
// (11-bit significands, and the exponent stays inside the f32 normal range:
// any non-zero product magnitude is in [2^-48, 2^33]), so only the addition
// rounds. An f32 add then an f16 pack would round twice; instead the add is
// corrected to round-to-odd, which a following round-to-nearest-even pack
// turns into the exactly-once-rounded fused result (innocuous double rounding
// holds because f32 carries 24 significand bits >= 11 + 2).
//
// sum = RN(product + addend); Knuth's 2Sum recovers the exact residual
// (product + addend) - sum from four more RN ops. 2Sum is exact for any two
// finite f32 inputs; no intermediate here can overflow (|product| < 2^33,
// |addend| < 2^16) and none can enter the f32 subnormal range (every finite
// value in play is a multiple of 2^-48 by construction), so implementation
// f32 denorm-flush modes never see a denormal. If the residual says the sum
// was inexact and the sum's significand is even, step one ulp towards the
// true value: consecutive floats have consecutive sign-magnitude encodings,
// so that neighbour is the enclosing float with the odd significand.
//
// Inf/NaN inputs make the residual NaN (e.g. sum - addend = Inf - Inf); the
// ordered compare below is then false and the IEEE sum passes through
// unchanged. A residual of zero also covers the exact-sum case, where the
// parity fix must not fire. Returns the round-to-odd f32 bit pattern.
private uint EmitPackedF16FusedMultiplyAdd(uint left, uint right, uint addend)
{
var product = EmitPreciseFloat(SpirvOp.FMul, left, right);
var sum = EmitPreciseFloat(SpirvOp.FAdd, product, addend);
var productPart = EmitPreciseFloat(SpirvOp.FSub, sum, addend);
var addendPart = EmitPreciseFloat(SpirvOp.FSub, sum, productPart);
var productError = EmitPreciseFloat(SpirvOp.FSub, product, productPart);
var addendError = EmitPreciseFloat(SpirvOp.FSub, addend, addendPart);
var residual = EmitPreciseFloat(SpirvOp.FAdd, productError, addendError);
var sumBits = Bitcast(_uintType, sum);
var residualBits = Bitcast(_uintType, residual);
var inexact = _module.AddInstruction(
SpirvOp.FOrdNotEqual, _boolType, residual, Float(0));
var evenSignificand = Equal(BitwiseAnd(sumBits, UInt(1)), 0);
var adjust = _module.AddInstruction(
SpirvOp.LogicalAnd, _boolType, inexact, evenSignificand);
// Residual sign relative to the sum picks the step direction: same sign
// means the true value lies away from zero (encoding + 1), opposite sign
// means towards zero (encoding - 1). The sum cannot be zero here (any
// inexact sum has magnitude >= 2^-48) and cannot be the largest finite
// value (its significand is odd), so the step never crosses zero or Inf.
var towardZero = IsNotZero(
BitwiseAnd(BitwiseXor(sumBits, residualBits), UInt(0x8000_0000)));
var stepped = SelectU(
towardZero,
ISubU(sumBits, UInt(1)),
IAdd(sumBits, UInt(1)));
return SelectU(adjust, stepped, sumBits);
}
// A float op the driver must evaluate exactly as written. The 2Sum
// residual above is error-free only op by op; without NoContraction
// driver compilers fold the sequence (e.g. contract product+sum into an
// f32 fma and simplify the rebuilt terms), collapsing the residual to
// zero. Observed on AMD RDNA3 Windows: the pinned midpoint case decays
// to the double-rounded result unless every op in the chain is marked.
private uint EmitPreciseFloat(SpirvOp operation, uint left, uint right)
{
var value = _module.AddInstruction(operation, _floatType, left, right);
_module.AddDecoration(value, SpirvDecoration.NoContraction);
return value;
"VPkAddF16" => _module.AddInstruction(SpirvOp.FAdd, _floatType, left, right),
"VPkMulF16" => _module.AddInstruction(SpirvOp.FMul, _floatType, left, right),
"VPkMinF16" => EmitPackedF16MinMax(left, right, isMax: false),
"VPkMaxF16" => EmitPackedF16MinMax(left, right, isMax: true),
_ => left,
};
return EmitFloatToHalf(Bitcast(_uintType, value));
}
// Reads source `index`, selects the half feeding this lane (op_sel / op_sel_hi),
@@ -313,8 +313,7 @@ public static partial class Gen5SpirvTranslator
uint ComponentType,
uint VectorType,
ImageComponentKind ComponentKind,
bool IsStorage,
bool Arrayed);
bool IsStorage);
private readonly record struct SpirvVertexInput(
uint Variable,
@@ -1001,13 +1000,11 @@ public static partial class Gen5SpirvTranslator
SpirvCapability.StorageImageExtendedFormats);
}
var isArrayed = !isStorage &&
Gen5ShaderTranslator.IsArrayedImageBinding(binding);
var imageType = _module.TypeImage(
componentType,
SpirvImageDim.Dim2D,
depth: false,
arrayed: isArrayed,
arrayed: false,
multisampled: false,
sampled: isStorage ? 2u : 1u,
isStorage ? format : SpirvImageFormat.Unknown);
@@ -1034,8 +1031,7 @@ public static partial class Gen5SpirvTranslator
componentType,
_module.TypeVector(componentType, 4),
componentKind,
isStorage,
isArrayed));
isStorage));
_interfaces.Add(variable);
}
}
@@ -3533,16 +3529,12 @@ public static partial class Gen5SpirvTranslator
addressCursor += 4;
}
var coordinates = resource.Arrayed
? BuildFloatArrayCoordinates(image, addressCursor)
: BuildFloatCoordinates(image, addressCursor);
var coordinates = BuildFloatCoordinates(image, addressCursor);
var explicitLod = hasGradients || hasZeroLod || hasLod;
var lod = hasZeroLod
? Float(0)
: hasLod
? LoadImageFloatAddress(
image,
addressCursor + (resource.Arrayed ? 3 : 2))
? LoadImageFloatAddress(image, addressCursor + 2)
: lodOrBias;
if (hasOffset)
{
@@ -3627,9 +3619,7 @@ public static partial class Gen5SpirvTranslator
addressCursor += ImageFullAddressSlots(image);
}
var coordinates = resource.Arrayed
? BuildFloatArrayCoordinates(image, addressCursor)
: BuildFloatCoordinates(image, addressCursor);
var coordinates = BuildFloatCoordinates(image, addressCursor);
var operands = new List<uint>
{
imageObject,
@@ -3833,19 +3823,6 @@ public static partial class Gen5SpirvTranslator
y);
}
private uint BuildFloatArrayCoordinates(Gen5ImageControl image, int start)
{
var x = LoadImageFloatAddress(image, start);
var y = LoadImageFloatAddress(image, start + 1);
var slice = LoadImageFloatAddress(image, start + 2);
return _module.AddInstruction(
SpirvOp.CompositeConstruct,
_vec3Type,
x,
y,
slice);
}
private static int ImageAddressRegister(
Gen5ImageControl image,
int component) => image.A16 ? component / 2 : component;
@@ -4163,20 +4140,9 @@ public static partial class Gen5SpirvTranslator
signedLod);
var size = _module.AddInstruction(
SpirvOp.ImageQuerySizeLod,
resource.Arrayed ? _module.TypeVector(_intType, 3) : ivec2,
ivec2,
image,
clampedLod);
if (resource.Arrayed)
{
size = _module.AddInstruction(
SpirvOp.VectorShuffle,
ivec2,
size,
size,
0u,
1u);
}
var sizeFloat = _module.AddInstruction(
SpirvOp.ConvertSToF,
_vec2Type,
@@ -4190,34 +4156,11 @@ public static partial class Gen5SpirvTranslator
_vec2Type,
offsetFloat,
sizeFloat);
if (!resource.Arrayed)
{
return _module.AddInstruction(
SpirvOp.FAdd,
_vec2Type,
coordinates,
normalizedOffset);
}
var offsetVec3 = _module.AddInstruction(
SpirvOp.CompositeConstruct,
_vec3Type,
_module.AddInstruction(
SpirvOp.CompositeExtract,
_floatType,
normalizedOffset,
0u),
_module.AddInstruction(
SpirvOp.CompositeExtract,
_floatType,
normalizedOffset,
1u),
Float(0));
return _module.AddInstruction(
SpirvOp.FAdd,
_vec3Type,
_vec2Type,
coordinates,
offsetVec3);
normalizedOffset);
}
private bool TryEmitExport(
@@ -5347,20 +5290,10 @@ public static partial class Gen5SpirvTranslator
UInt(0x108));
}
// A wave-mask SGPR (VCC/EXEC) consumed as a per-lane predicate — the
// condition of VCndmask, a VCC/EXEC branch, or the derived _vcc/_exec
// bool — must be tested at the CURRENT lane's bit, exactly as the
// hardware does, not as "the 64-bit value is non-zero". The two coincide
// for comparison results (only the lane's own bit is ever set), so the
// single-lane path historically used a cheaper whole-word non-zero test.
// But bitwise-complement wave-mask idioms (S_NOT/S_ORN2/S_ANDN2/S_NAND/
// S_NOR on a 64-bit mask) set the unused upper 63 bits; a whole-word test
// then reports "lane active" even when this lane's bit is clear. Unity's
// PostProcessing NaN killer does exactly this (`anyNaN | ~allFinite`),
// which made every valid pixel read as NaN and get replaced with 0 —
// zeroing the whole scene before tonemap. Extract the lane bit always.
private uint IsWaveMaskActive(uint mask) =>
IsCurrentLaneSet(mask);
_subgroupInvocationIdInput == 0
? IsNotZero64(mask)
: IsCurrentLaneSet(mask);
private uint IsCurrentLaneSet(uint mask) =>
IsNotZero64(
@@ -238,7 +238,6 @@ public enum SpirvDecoration : uint
Binding = 33,
DescriptorSet = 34,
Offset = 35,
NoContraction = 42,
}
public enum SpirvBuiltIn : uint
@@ -2350,8 +2350,7 @@ public static class Gen5ShaderScalarEvaluator
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
{
Span<byte> bytes = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(address, bytes) &&
FallbackMemoryReader?.Invoke(address, bytes) != true)
if (!ctx.Memory.TryRead(address, bytes))
{
value = 0;
return false;
@@ -1192,10 +1192,8 @@ public static class Gen5ShaderTranslator
// Opcode numbers taken from LLVM's AMDGPU VOP3PInstructions.td and the
// gfx9/gfx10 MC test encodings; they are unchanged across gfx9 and gfx10.
// The mix ops (0x20/0x21/0x22) are V_MAD_MIX_* on gfx9 and V_FMA_MIX_*
// (fused) on the gfx10 the PS5 targets; both share these opcodes. Any
// remaining packed opcode (integer, ...) stays opaque here and fails
// loudly at emission rather than being silently mis-emitted.
// Unhandled packed opcodes (integer, fma_mix, ...) stay opaque here and
// fail loudly at emission rather than being silently mis-emitted.
name = opcode switch
{
0x0E => "VPkFmaF16",
@@ -1203,9 +1201,6 @@ public static class Gen5ShaderTranslator
0x10 => "VPkMulF16",
0x11 => "VPkMinF16",
0x12 => "VPkMaxF16",
0x20 => "VFmaMixF32",
0x21 => "VFmaMixloF16",
0x22 => "VFmaMixhiF16",
_ => $"Vop3pRaw{opcode:X2}",
};
@@ -1612,11 +1607,6 @@ public static class Gen5ShaderTranslator
binding.ResourceDescriptor.SequenceEqual(candidate.ResourceDescriptor));
}
public static bool IsArrayedImageBinding(Gen5ImageBinding binding) =>
binding.Control.IsArray &&
(binding.Opcode.StartsWith("ImageSample", StringComparison.Ordinal) ||
binding.Opcode.StartsWith("ImageGather4", StringComparison.Ordinal));
public static bool IsDataShareAtomic(string name) => name switch
{
"DsAddU32" or "DsSubU32" or "DsIncU32" or "DsDecU32" or
@@ -1,135 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.HLE;
using SharpEmu.ShaderCompiler;
using SharpEmu.ShaderCompiler.Vulkan;
using Xunit;
namespace SharpEmu.Libs.Tests.Agc;
// Regression tests for the VOP3P mix ops V_FMA_MIX_F32 / _MIXLO_F16 / _MIXHI_F16
// (opcodes 0x20 / 0x21 / 0x22). The decoder leaves any unlowered VOP3P opcode
// opaque (Vop3pRaw20/21/22); before these were lowered they hit the vector-ALU
// switch default and failed emission ("unsupported vector opcode"), which drops
// the whole shader. Unity HDR / tone-mapping / auto-exposure shaders use
// V_FMA_MIX_F32 and so failed to translate entirely.
//
// Each mix op computes a single f32 fma(a, b, c) where every source is read
// *independently* as either a full f32 register or one f16 half widened to f32,
// selected per operand by op_sel_hi (f16 when set) and op_sel (which half). The
// mix ops also repurpose the VOP3P neg_hi field as an absolute-value modifier.
public sealed class Gen5FmaMixSpirvTests
{
private const ulong ShaderAddress = 0x1_0000_0000;
// GLSL.std.450 extended-instruction numbers used by the lowering.
private const uint GlslFma = 50;
private const uint GlslFAbs = 4;
[Fact]
public void FmaMixF32_TranslatesToFmaAndDoesNotDropShader()
{
// V_FMA_MIX_F32 v3, v0, v1, v2
// op_sel_hi = 0b011 -> src0/src1 read as f16, src2 as full f32
// op_sel = 0b010 -> src1 takes its high f16 half (src0 low half)
// neg_hi = 0b001 -> abs(src0)
// neg = 0b100 -> -src2
// Reaching TryCompileComputeShader == true already proves the shader is no
// longer dropped at the VOP3P default error path.
var spirv = Compile([0xCC201103u, 0x9C0A0300u]);
Assert.True(
ContainsExtInst(spirv, GlslFma),
"V_FMA_MIX_F32 must lower to a GLSL.std.450 Fma");
Assert.True(
ContainsExtInst(spirv, GlslFAbs),
"the neg_hi modifier on a mix source must lower to an FAbs (abs-then-neg)");
}
[Fact]
public void FmaMixLoF16_TranslatesWithoutDroppingShader()
{
// V_FMA_MIXLO_F16 v3, v0, v1, v2 with op_sel_hi = 0b111 (all sources read
// as f16 low halves). The f32 fma result is narrowed to f16 and merged
// into the low 16 bits of vdst; the fma itself is still emitted.
var spirv = Compile([0xCC214003u, 0x1C0A0300u]);
Assert.True(
ContainsExtInst(spirv, GlslFma),
"V_FMA_MIXLO_F16 must still lower its multiply-add to a GLSL.std.450 Fma");
}
// True when the module contains an OpExtInst selecting the given GLSL.std.450
// instruction number.
private static bool ContainsExtInst(byte[] spirv, uint instruction)
{
foreach (var (op, wordCount, offset) in EnumerateInstructions(spirv))
{
// OpExtInst = 12: (opcode, resultType, resultId, set, instruction, ...).
if (op != 12 || wordCount < 5)
{
continue;
}
if (ReadWord(spirv, offset + 16) == instruction)
{
return true;
}
}
return false;
}
private static IEnumerable<(ushort Op, int WordCount, int Offset)> EnumerateInstructions(
byte[] spirv)
{
// 5-word SPIR-V header, then (wordCount << 16 | opcode) packed instructions.
for (var offset = 5 * sizeof(uint); offset + sizeof(uint) <= spirv.Length;)
{
var word = ReadWord(spirv, offset);
var wordCount = (int)(word >> 16);
if (wordCount <= 0)
{
yield break;
}
yield return ((ushort)word, wordCount, offset);
offset += wordCount * sizeof(uint);
}
}
private static uint ReadWord(byte[] spirv, int offset) =>
BinaryPrimitives.ReadUInt32LittleEndian(spirv.AsSpan(offset, sizeof(uint)));
private static byte[] Compile(uint[] programWords)
{
var memory = new FakeCpuMemory(ShaderAddress, 0x2000);
var ctx = new CpuContext(memory, Generation.Gen5);
Gen5ShaderAtomicDecodeTests.WriteProgram(memory, ShaderAddress, programWords);
var shaderRegisters = new Dictionary<uint, uint>
{
[Gen5ShaderAtomicDecodeTests.ComputePgmRsrc2Register] = 16u << 1,
};
Assert.True(
Gen5ShaderTranslator.TryCreateState(
ctx,
ShaderAddress,
0,
shaderRegisters,
Gen5ShaderAtomicDecodeTests.ComputeUserDataRegister,
out var state,
out var error),
error);
Assert.True(
Gen5ShaderScalarEvaluator.TryEvaluate(ctx, state, out var evaluation, out error),
error);
Assert.True(
Gen5SpirvTranslator.TryCompileComputeShader(
state, evaluation, 1, 1, 1, out var shader, out error),
error);
return shader.Spirv;
}
}
@@ -1,99 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.HLE;
using SharpEmu.ShaderCompiler;
using Xunit;
namespace SharpEmu.Libs.Tests.Agc;
public sealed class Gen5ScalarMemoryFallbackTests
{
private const ulong ScalarTableAddress = 0x4_4665_4FD0;
private static readonly object FallbackReaderGate = new();
[Fact]
public void ScalarLoadReadsTrackedFallbackMemory()
{
var expected = new uint[]
{
0x4665_4F70,
0x0000_0004,
0x4EA7_FCE0,
0x0000_0004,
};
var table = new byte[expected.Length * sizeof(uint)];
for (var index = 0; index < expected.Length; index++)
{
BinaryPrimitives.WriteUInt32LittleEndian(
table.AsSpan(index * sizeof(uint), sizeof(uint)),
expected[index]);
}
var load = new Gen5ShaderInstruction(
0,
Gen5ShaderEncoding.Smem,
"SLoadDwordx4",
[],
[Gen5Operand.Scalar(0)],
[
Gen5Operand.Scalar(16),
Gen5Operand.Scalar(17),
Gen5Operand.Scalar(18),
Gen5Operand.Scalar(19),
],
new Gen5ScalarMemoryControl(4, 0, null));
var end = new Gen5ShaderInstruction(
8,
Gen5ShaderEncoding.Sopp,
"SEndpgm",
[],
[],
[],
null);
var state = new Gen5ShaderState(
new Gen5ShaderProgram(0, [load, end]),
[unchecked((uint)ScalarTableAddress), (uint)(ScalarTableAddress >> 32)],
null);
var ctx = new CpuContext(new FakeCpuMemory(0x1000, 0x100), Generation.Gen5);
lock (FallbackReaderGate)
{
var previousReader = Gen5ShaderScalarEvaluator.FallbackMemoryReader;
try
{
Gen5ShaderScalarEvaluator.FallbackMemoryReader = ReadFallback;
Assert.True(
Gen5ShaderScalarEvaluator.TryEvaluate(
ctx,
state,
out var evaluation,
out var error),
error);
Assert.Equal(expected, evaluation.ScalarRegisters.Skip(16).Take(4));
}
finally
{
Gen5ShaderScalarEvaluator.FallbackMemoryReader = previousReader;
}
}
bool ReadFallback(ulong address, Span<byte> destination)
{
if (address < ScalarTableAddress)
{
return false;
}
var offset = address - ScalarTableAddress;
if (offset + (ulong)destination.Length > (ulong)table.Length)
{
return false;
}
table.AsSpan((int)offset, destination.Length).CopyTo(destination);
return true;
}
}
}
@@ -1,140 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.HLE;
using SharpEmu.ShaderCompiler;
using SharpEmu.ShaderCompiler.Vulkan;
using Xunit;
namespace SharpEmu.Libs.Tests.Agc;
// Regression tests for how a VCC/EXEC wave mask consumed as a per-lane predicate
// is lowered to SPIR-V. A wave mask must be tested at the current lane's bit
// (mask & lane_bit) — exactly as the hardware evaluates the VCndmask condition or
// a VCC/EXEC branch — not with a whole-word "the 64-bit value is non-zero" test.
//
// The two agree for comparison results (only the lane's own bit is ever set), but
// diverge for the bitwise-complement wave-mask idioms (S_NOT / S_ORN2 / S_ANDN2 /
// S_NAND / S_NOR), which set the unused upper 63 bits. A whole-word test then
// reports the lane active even when its bit is clear. Unity's PostProcessing NaN
// killer combines its channels as `anyNaN | ~allFinite` (S_ORN2_B64); under the
// whole-word test every valid pixel read as NaN and was replaced with 0, zeroing
// the whole HDR scene before tone-mapping.
public sealed class Gen5WaveMaskSpirvTests
{
private const ulong ShaderAddress = 0x1_0000_0000;
[Fact]
public void WaveMaskPredicate_IsTestedAtCurrentLaneBit()
{
// V_CMP_EQ_F32 vcc, v0, v1 writes VCC at run time, which re-materialises
// the per-lane _vcc predicate from the wave mask via IsWaveMaskActive.
var spirv = Compile([0x7C04_0300u]);
// The lane's bit in single-lane emulation is the 64-bit constant 1, so the
// predicate is `(mask & 1) != 0`. The whole-word bug emitted `mask != 0`
// with no such mask. Require the lane-bit AND to be present.
Assert.True(
ContainsLaneBitMaskedWaveTest(spirv),
"wave-mask predicate must be tested at the current lane bit "
+ "(mask & lane_bit), not as a whole-word non-zero test");
}
// True when the module contains an OpBitwiseAnd whose operand is a 64-bit
// constant of value 1 — the current-lane bit that IsCurrentLaneSet masks the
// wave mask with before the non-zero test.
private static bool ContainsLaneBitMaskedWaveTest(byte[] spirv)
{
var laneBitConstIds = new HashSet<uint>();
// Pass 1: collect 64-bit OpConstant result-ids whose value is 1.
foreach (var (op, wordCount, offset) in EnumerateInstructions(spirv))
{
// OpConstant = 43; a 64-bit constant occupies 5 words
// (opcode, resultType, resultId, valueLow, valueHigh).
if (op != 43 || wordCount != 5)
{
continue;
}
var resultId = ReadWord(spirv, offset + 8);
var low = ReadWord(spirv, offset + 12);
var high = ReadWord(spirv, offset + 16);
if (low == 1 && high == 0)
{
laneBitConstIds.Add(resultId);
}
}
// Pass 2: look for an OpBitwiseAnd that consumes one of those constants.
foreach (var (op, wordCount, offset) in EnumerateInstructions(spirv))
{
// OpBitwiseAnd = 199 (opcode, resultType, resultId, operand0, operand1).
if (op != 199 || wordCount != 5)
{
continue;
}
var operand0 = ReadWord(spirv, offset + 12);
var operand1 = ReadWord(spirv, offset + 16);
if (laneBitConstIds.Contains(operand0) || laneBitConstIds.Contains(operand1))
{
return true;
}
}
return false;
}
private static IEnumerable<(ushort Op, int WordCount, int Offset)> EnumerateInstructions(
byte[] spirv)
{
// 5-word SPIR-V header, then (wordCount << 16 | opcode) packed instructions.
for (var offset = 5 * sizeof(uint); offset + sizeof(uint) <= spirv.Length;)
{
var word = ReadWord(spirv, offset);
var wordCount = (int)(word >> 16);
if (wordCount <= 0)
{
yield break;
}
yield return ((ushort)word, wordCount, offset);
offset += wordCount * sizeof(uint);
}
}
private static uint ReadWord(byte[] spirv, int offset) =>
BinaryPrimitives.ReadUInt32LittleEndian(spirv.AsSpan(offset, sizeof(uint)));
private static byte[] Compile(uint[] programWords)
{
var memory = new FakeCpuMemory(ShaderAddress, 0x2000);
var ctx = new CpuContext(memory, Generation.Gen5);
Gen5ShaderAtomicDecodeTests.WriteProgram(memory, ShaderAddress, programWords);
var shaderRegisters = new Dictionary<uint, uint>
{
[Gen5ShaderAtomicDecodeTests.ComputePgmRsrc2Register] = 16u << 1,
};
Assert.True(
Gen5ShaderTranslator.TryCreateState(
ctx,
ShaderAddress,
0,
shaderRegisters,
Gen5ShaderAtomicDecodeTests.ComputeUserDataRegister,
out var state,
out var error),
error);
Assert.True(
Gen5ShaderScalarEvaluator.TryEvaluate(ctx, state, out var evaluation, out error),
error);
Assert.True(
Gen5SpirvTranslator.TryCompileComputeShader(
state, evaluation, 1, 1, 1, out var shader, out error),
error);
return shader.Spirv;
}
}
@@ -90,133 +90,6 @@ public sealed class AprStreamingContractTests
}
}
[Fact]
public void ResolveFilepathsToIdsAndFileSizes_MissingFile_FailsFastWithErrorIndex()
{
const ulong memoryBase = 0x1_0000_0000;
const ulong pathListAddress = memoryBase + 0x100;
const ulong pathAddress = memoryBase + 0x200;
const ulong idsAddress = memoryBase + 0x800;
const ulong sizesAddress = memoryBase + 0x880;
const ulong errorIndexAddress = memoryBase + 0x8F0;
var memory = new FakeCpuMemory(memoryBase, 0x4000);
var context = new CpuContext(memory, Generation.Gen5);
var missingHostPath = Path.Combine(
Path.GetTempPath(),
$"sharpemu-apr-missing-{Guid.NewGuid():N}.bin");
memory.WriteCString(pathAddress, missingHostPath);
WriteUInt64(memory, pathListAddress, pathAddress);
context[CpuRegister.Rdi] = pathListAddress;
context[CpuRegister.Rsi] = 1;
context[CpuRegister.Rdx] = idsAddress;
context[CpuRegister.Rcx] = sizesAddress;
context[CpuRegister.R8] = errorIndexAddress;
Assert.Equal(-1, KernelMemoryCompatExports.KernelAprResolveFilepathsToIdsAndFileSizes(context));
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
Assert.Equal(uint.MaxValue, ReadUInt32(memory, idsAddress));
Assert.Equal(0ul, ReadUInt64(memory, sizesAddress));
Assert.Equal(0u, ReadUInt32(memory, errorIndexAddress));
}
[Fact]
public void ResolveFilepathsToIdsAndFileSizes_InvalidErrorIndex_ReturnsMemoryFault()
{
const ulong memoryBase = 0x1_0000_0000;
const ulong pathListAddress = memoryBase + 0x100;
const ulong pathAddress = memoryBase + 0x200;
const ulong idsAddress = memoryBase + 0x800;
const ulong sizesAddress = memoryBase + 0x880;
var memory = new FakeCpuMemory(memoryBase, 0x4000);
var context = new CpuContext(memory, Generation.Gen5);
var missingHostPath = Path.Combine(
Path.GetTempPath(),
$"sharpemu-apr-missing-{Guid.NewGuid():N}.bin");
memory.WriteCString(pathAddress, missingHostPath);
WriteUInt64(memory, pathListAddress, pathAddress);
context[CpuRegister.Rdi] = pathListAddress;
context[CpuRegister.Rsi] = 1;
context[CpuRegister.Rdx] = idsAddress;
context[CpuRegister.Rcx] = sizesAddress;
context[CpuRegister.R8] = memoryBase + 0x5000;
Assert.Equal(
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT,
KernelMemoryCompatExports.KernelAprResolveFilepathsToIdsAndFileSizes(context));
}
[Fact]
public void ResolveFilepathsToIdsAndFileSizes_MissingMidBatch_StopsAtFailingEntry()
{
const ulong memoryBase = 0x1_0000_0000;
const ulong pathListAddress = memoryBase + 0x100;
const ulong idsAddress = memoryBase + 0x800;
const ulong sizesAddress = memoryBase + 0x880;
const ulong errorIndexAddress = memoryBase + 0x8F0;
byte[] fileContents = [1, 2, 3, 4, 5];
var hostPath = Path.GetTempFileName();
var missingHostPath = Path.Combine(
Path.GetTempPath(),
$"sharpemu-apr-missing-{Guid.NewGuid():N}.bin");
try
{
File.WriteAllBytes(hostPath, fileContents);
var memory = new FakeCpuMemory(memoryBase, 0x4000);
var context = new CpuContext(memory, Generation.Gen5);
memory.WriteCString(memoryBase + 0x200, hostPath);
memory.WriteCString(memoryBase + 0x400, missingHostPath);
memory.WriteCString(memoryBase + 0x600, hostPath);
WriteUInt64(memory, pathListAddress, memoryBase + 0x200);
WriteUInt64(memory, pathListAddress + 8, memoryBase + 0x400);
WriteUInt64(memory, pathListAddress + 16, memoryBase + 0x600);
WriteUInt32(memory, idsAddress + 8, 0x1234_5678); // sentinel: entry 2 untouched
WriteUInt64(memory, sizesAddress + 16, 0xDEAD);
context[CpuRegister.Rdi] = pathListAddress;
context[CpuRegister.Rsi] = 3;
context[CpuRegister.Rdx] = idsAddress;
context[CpuRegister.Rcx] = sizesAddress;
context[CpuRegister.R8] = errorIndexAddress;
Assert.Equal(-1, KernelMemoryCompatExports.KernelAprResolveFilepathsToIdsAndFileSizes(context));
Assert.NotEqual(uint.MaxValue, ReadUInt32(memory, idsAddress));
Assert.Equal((ulong)fileContents.Length, ReadUInt64(memory, sizesAddress));
Assert.Equal(uint.MaxValue, ReadUInt32(memory, idsAddress + 4));
Assert.Equal(0ul, ReadUInt64(memory, sizesAddress + 8));
Assert.Equal(1u, ReadUInt32(memory, errorIndexAddress));
Assert.Equal(0x1234_5678u, ReadUInt32(memory, idsAddress + 8));
Assert.Equal(0xDEADul, ReadUInt64(memory, sizesAddress + 16));
}
finally
{
File.Delete(hostPath);
}
}
private static uint ReadUInt32(FakeCpuMemory memory, ulong address)
{
Span<byte> bytes = stackalloc byte[sizeof(uint)];
Assert.True(memory.TryRead(address, bytes));
return BinaryPrimitives.ReadUInt32LittleEndian(bytes);
}
private static ulong ReadUInt64(FakeCpuMemory memory, ulong address)
{
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
Assert.True(memory.TryRead(address, bytes));
return BinaryPrimitives.ReadUInt64LittleEndian(bytes);
}
private static void WriteUInt32(FakeCpuMemory memory, ulong address, uint value)
{
Span<byte> bytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
Assert.True(memory.TryWrite(address, bytes));
}
private static void WriteUInt64(FakeCpuMemory memory, ulong address, ulong value)
{
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
@@ -40,65 +40,6 @@ public sealed class AvPlayerPathTests : IDisposable
AssertPathIsInsideApp0(resolved);
}
[Fact]
public void UnrealRelativeRawPathAnchorsAtApp0AndResolvesMedia()
{
var mediaPath = CreateFile("SampleProject/Content/Movies/Startup.mp4");
var resolved = AvPlayerExports.ResolveGuestPath(
"../../../SampleProject/Content/Movies/Startup.mp4");
Assert.NotNull(resolved);
Assert.Equal(File.ReadAllBytes(mediaPath), File.ReadAllBytes(resolved));
AssertPathIsInsideApp0(resolved);
}
[Fact]
public void UnrealRelativeRawPathCannotEscapeApp0()
{
var outsidePath = Path.Combine(_tempRoot, "outside.mp4");
File.WriteAllBytes(outsidePath, [0x7F]);
CreateFile("outside.mp4");
Assert.Null(AvPlayerExports.ResolveGuestPath("../../../outside.mp4"));
}
[Fact]
public void CurrentDirectoryRawPathResolvesInsideApp0()
{
var mediaPath = CreateFile("Movies/Intro.mp4");
var resolved = AvPlayerExports.ResolveGuestPath("./Movies/Intro.mp4");
Assert.NotNull(resolved);
Assert.Equal(Path.GetFullPath(mediaPath), resolved);
AssertPathIsInsideApp0(resolved);
}
[Theory]
[InlineData(false, "ffmpeg", "ffprobe")]
[InlineData(true, "ffmpeg.exe", "ffprobe.exe")]
public void MediaToolLookupUsesPlatformNames(
bool isWindows,
string ffmpegName,
string ffprobeName)
{
var toolDirectory = Path.Combine(_tempRoot, "Media Tools");
Directory.CreateDirectory(toolDirectory);
var ffmpeg = Path.Combine(toolDirectory, ffmpegName);
File.WriteAllBytes(ffmpeg, []);
var resolved = AvPlayerExports.FindFfmpeg(
configured: null,
searchPath: $"\"{toolDirectory}\"",
isWindows);
Assert.Equal(ffmpeg, resolved);
Assert.Equal(
Path.Combine(toolDirectory, ffprobeName),
AvPlayerExports.GetFfprobePath(ffmpeg, isWindows));
}
[Fact]
public void RelativeFileUriCannotEscapeApp0()
{
@@ -202,14 +143,12 @@ public sealed class AvPlayerPathTests : IDisposable
private void AssertPathIsInsideApp0(string resolved)
{
var relative = Path.GetRelativePath(
Path.GetFullPath(_app0Root),
Path.GetFullPath(resolved));
Assert.False(Path.IsPathFullyQualified(relative));
Assert.NotEqual("..", relative);
Assert.False(
relative.StartsWith(
".." + Path.DirectorySeparatorChar,
StringComparison.Ordinal));
var rootWithSeparator =
Path.TrimEndingDirectorySeparator(Path.GetFullPath(_app0Root)) +
Path.DirectorySeparatorChar;
Assert.StartsWith(
rootWithSeparator,
Path.GetFullPath(resolved),
StringComparison.OrdinalIgnoreCase);
}
}
@@ -1,126 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.HLE;
using SharpEmu.Libs.AvPlayer;
using Xunit;
namespace SharpEmu.Libs.Tests.AvPlayer;
public sealed class AvPlayerStreamInfoTests
{
private const string StreamInfoExNid = "ctTAcF5DiKQ";
private const ulong BaseAddress = 0x1_0000_0000;
private const int MemorySize = 0x2000;
private const ulong InfoAddress = BaseAddress + 0x100;
private const ulong Handle = 0xA0_0000_0001;
private const ulong DurationMilliseconds = 0x0102_0304_0506_0708;
private const byte Sentinel = 0xAB;
[Theory]
[InlineData(false, 0u)]
[InlineData(true, 0u)]
[InlineData(false, 1u)]
[InlineData(true, 1u)]
public void GetStreamInfoFunctionsDoNotWritePastThe32ByteStructure(
bool useExtendedFunction,
uint streamIndex)
{
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
var context = new CpuContext(memory, Generation.Gen5);
AvPlayerExports.RegisterPlayerForTest(Handle, 1280, 720, DurationMilliseconds);
try
{
Span<byte> window = stackalloc byte[40];
window.Fill(Sentinel);
Assert.True(memory.TryWrite(InfoAddress, window));
context[CpuRegister.Rdi] = Handle;
context[CpuRegister.Rsi] = streamIndex;
context[CpuRegister.Rdx] = InfoAddress;
var resultCode = useExtendedFunction
? AvPlayerExports.AvPlayerGetStreamInfoEx(context)
: AvPlayerExports.AvPlayerGetStreamInfo(context);
Assert.Equal(0, resultCode);
Span<byte> result = stackalloc byte[40];
Assert.True(memory.TryRead(InfoAddress, result));
Assert.Equal(streamIndex, BinaryPrimitives.ReadUInt32LittleEndian(result));
if (streamIndex == 0)
{
Assert.Equal(1280u, BinaryPrimitives.ReadUInt32LittleEndian(result[8..]));
Assert.Equal(720u, BinaryPrimitives.ReadUInt32LittleEndian(result[12..]));
}
else
{
Assert.Equal(2, BinaryPrimitives.ReadUInt16LittleEndian(result[8..]));
Assert.Equal(48_000u, BinaryPrimitives.ReadUInt32LittleEndian(result[12..]));
}
Assert.Equal(DurationMilliseconds, BinaryPrimitives.ReadUInt64LittleEndian(result[24..]));
for (var index = 32; index < result.Length; index++)
{
Assert.Equal(Sentinel, result[index]);
}
}
finally
{
AvPlayerExports.RemovePlayerForTest(Handle);
}
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void GetStreamInfoFunctionsRejectInvalidArguments(bool useExtendedFunction)
{
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
var context = new CpuContext(memory, Generation.Gen5);
AvPlayerExports.RegisterPlayerForTest(Handle, 1280, 720, DurationMilliseconds);
try
{
context[CpuRegister.Rdi] = Handle;
context[CpuRegister.Rsi] = 2;
context[CpuRegister.Rdx] = InfoAddress;
Assert.NotEqual(0, InvokeGetStreamInfo(context, useExtendedFunction));
context[CpuRegister.Rsi] = 0;
context[CpuRegister.Rdx] = 0;
Assert.NotEqual(0, InvokeGetStreamInfo(context, useExtendedFunction));
context[CpuRegister.Rdi] = Handle + 1;
context[CpuRegister.Rdx] = InfoAddress;
Assert.NotEqual(0, InvokeGetStreamInfo(context, useExtendedFunction));
}
finally
{
AvPlayerExports.RemovePlayerForTest(Handle);
}
}
[Fact]
public void StreamInfoExExportIsRegisteredForGen5Only()
{
var gen4Manager = new ModuleManager();
gen4Manager.RegisterExports(
SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen4));
Assert.False(gen4Manager.TryGetExport(StreamInfoExNid, out _));
var gen5Manager = new ModuleManager();
gen5Manager.RegisterExports(
SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen5));
Assert.True(gen5Manager.TryGetExport(StreamInfoExNid, out var export));
Assert.Equal("sceAvPlayerGetStreamInfoEx", export.Name);
Assert.Equal("libSceAvPlayer", export.LibraryName);
Assert.Equal(Generation.Gen5, export.Target);
}
private static int InvokeGetStreamInfo(CpuContext context, bool useExtendedFunction) =>
useExtendedFunction
? AvPlayerExports.AvPlayerGetStreamInfoEx(context)
: AvPlayerExports.AvPlayerGetStreamInfo(context);
}
@@ -1,26 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Cpu.Native;
using Xunit;
namespace SharpEmu.Libs.Tests.Cpu;
public sealed unsafe class JitStubsTests
{
[Fact]
public void FindTlsAccessPatterns_IncludesLastValidOffset()
{
var pattern = JitStubs.TlsAccessPattern;
var code = new byte[pattern.Length + 3];
pattern.CopyTo(code.AsSpan(3));
fixed (byte* codePointer = code)
{
var matches = JitStubs.FindTlsAccessPatterns(codePointer, code.Length);
var match = Assert.Single(matches);
Assert.Equal((nint)(codePointer + 3), match);
}
}
}
@@ -1,147 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Cpu.Emulation;
using Xunit;
namespace SharpEmu.Libs.Tests.Cpu;
// These exercise the pure EXTRQ/INSERTQ bit-field semantics used by the general SSE4a
// illegal-instruction software fallback (DirectExecutionBackend.Amd64Compat.cs). Expected values
// were computed from the AMD64 Architecture Programmer's Manual definitions and cross-checked
// with an independent Python re-implementation before being written here, so a regression in the
// ported bit math fails in this file without needing a live guest or a Windows host.
public sealed class Sse4aBitFieldEmulatorTests
{
[Fact]
public void ExtractBitField_ExtractsLowByte()
{
var result = Sse4aBitFieldEmulator.ExtractBitField(0x1234_5678_9ABC_DEF0, length: 8, index: 0);
Assert.Equal(0xF0UL, result);
}
[Fact]
public void ExtractBitField_ExtractsMidFieldAtNonZeroIndex()
{
// bits [31:16] of 0x1234_5678_9ABC_DEF0 == 0x9ABC
var result = Sse4aBitFieldEmulator.ExtractBitField(0x1234_5678_9ABC_DEF0, length: 16, index: 16);
Assert.Equal(0x9ABCUL, result);
}
[Fact]
public void ExtractBitField_LengthZeroMeansSixtyFour()
{
var result = Sse4aBitFieldEmulator.ExtractBitField(0xFFFF_FFFF_FFFF_FFFF, length: 0, index: 0);
Assert.Equal(0xFFFF_FFFF_FFFF_FFFFUL, result);
}
[Fact]
public void ExtractBitField_MasksImmediatesToLowSixBits()
{
// length=0x28 (40) and index=0 is exactly the idiom SharpEmu's load-time
// Sse4aExtrqBlendPatch already recognizes; the general emulator must agree with it.
var result = Sse4aBitFieldEmulator.ExtractBitField(0x0000_0000_0000_00FF, length: 0x28, index: 0);
Assert.Equal(0xFFUL, result);
}
[Theory]
[InlineData(0x1234_5678_9ABC_DEF0UL)]
[InlineData(0x0000_0000_0000_0000UL)]
[InlineData(0xFFFF_FFFF_FFFF_FFFFUL)]
[InlineData(0x00FF_00FF_00FF_00FFUL)]
public void ExtractBitField_AgreesWithSse4aExtrqBlendPatchsByteFourRule(ulong value)
{
// Sse4aExtrqBlendPatch's own comment states that after "EXTRQ xmmN, 0x28, 0x00", dword
// lane 1 (bits 63:32) of the result equals byte 4 of the source zero-extended. The
// general emulator (used for every other EXTRQ occurrence) must produce a result
// consistent with that independently-reverse-engineered rule for the one idiom both
// paths can be checked against.
var extractedLow64 = Sse4aBitFieldEmulator.ExtractBitField(value, length: 0x28, index: 0);
var dword1 = (uint)(extractedLow64 >> 32);
var byteFourZeroExtended = (uint)((value >> 32) & 0xFF);
Assert.Equal(byteFourZeroExtended, dword1);
}
[Fact]
public void ExtractBitField_RejectsUndefinedFieldPastRegisterEnd()
{
Assert.False(Sse4aBitFieldEmulator.IsValidBitField(length: 8, index: 60));
Assert.Equal(0UL, Sse4aBitFieldEmulator.ExtractBitField(
0xFFFF_FFFF_FFFF_FFFF,
length: 8,
index: 60));
}
[Fact]
public void ExtractBitField_RejectsZeroLengthAtNonZeroIndex()
{
Assert.False(Sse4aBitFieldEmulator.IsValidBitField(length: 0, index: 1));
}
[Fact]
public void InsertBitField_InsertsFieldAtNonZeroIndexWithoutDisturbingOtherBits()
{
var result = Sse4aBitFieldEmulator.InsertBitField(
destination: 0x0000_0000_0000_0000,
source: 0xFFFF_FFFF_FFFF_FFFF,
length: 8,
index: 8);
Assert.Equal(0x0000_0000_0000_FF00UL, result);
}
[Fact]
public void InsertBitField_ClearsExactlyTheDestinationWindowBeforeInserting()
{
var result = Sse4aBitFieldEmulator.InsertBitField(
destination: 0xFFFF_FFFF_FFFF_FFFF,
source: 0x0000_0000_0000_0000,
length: 16,
index: 16);
Assert.Equal(0xFFFF_FFFF_0000_FFFFUL, result);
}
[Fact]
public void InsertBitField_InsertsLowByteAtIndexZero()
{
var result = Sse4aBitFieldEmulator.InsertBitField(
destination: 0x1122_3344_5566_7788,
source: 0xAABB_CCDD_EEFF_0011,
length: 8,
index: 0);
Assert.Equal(0x1122_3344_5566_7711UL, result);
}
[Fact]
public void InsertBitField_LengthZeroMeansSixtyFourAndOverwritesEverything()
{
var result = Sse4aBitFieldEmulator.InsertBitField(
destination: 0x1111_1111_1111_1111,
source: 0xFFFF_FFFF_FFFF_FFFF,
length: 0,
index: 0);
Assert.Equal(0xFFFF_FFFF_FFFF_FFFFUL, result);
}
[Fact]
public void InsertBitField_ZeroSourceFieldClearsOnlyItsOwnWindow()
{
// A zero-valued 12-bit field inserted at index 20 clears exactly bits [31:20]
// (0x234 -> 0x000) and leaves every bit outside that window untouched.
var result = Sse4aBitFieldEmulator.InsertBitField(
destination: 0xABCD_EF01_2345_6789,
source: 0,
length: 12,
index: 20);
Assert.Equal(0xABCD_EF01_0005_6789UL, result);
}
}
@@ -1,332 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.HLE;
using SharpEmu.Libs.Fiber;
using Xunit;
namespace SharpEmu.Libs.Tests.Fiber;
/// <summary>
/// Contract tests for the libSceFiber HLE exports. These pin the current
/// validation and layout behaviour of <see cref="FiberExports"/>; they do not
/// exercise a live guest thread scheduler.
/// </summary>
public sealed class FiberExportsTests
{
private const ulong Base = 0x3_0000_0000UL;
private const int RegionSize = 0x2000;
private const int ErrorNull = unchecked((int)0x80590001);
private const int ErrorAlignment = unchecked((int)0x80590002);
private const int ErrorRange = unchecked((int)0x80590003);
private const int ErrorInvalid = unchecked((int)0x80590004);
private const int ErrorPermission = unchecked((int)0x80590005);
private const uint SignatureStart = 0xDEF1649Cu;
private const uint SignatureEnd = 0xB37592A0u;
private const ulong StackSignature = 0x7149F2CA7149F2CAUL;
private const uint StateIdle = 2;
private const ulong FiberAddress = Base;
private const ulong NameAddress = Base + 0x200;
private const ulong ContextAddress = Base + 0x400;
private const ulong EntryAddress = 0x4_0000_1000UL;
private const ulong InfoAddress = Base + 0x800;
public FiberExportsTests()
{
FiberExports.ResetRuntimeState();
}
[Fact]
public void OptParamInitialize_NullParam_ReturnsNullError()
{
var memory = new FakeCpuMemory(Base, RegionSize);
var context = new CpuContext(memory, Generation.Gen5);
context[CpuRegister.Rdi] = 0;
var result = FiberExports.FiberOptParamInitialize(context);
Assert.Equal(ErrorNull, result);
}
[Fact]
public void GetSelf_NullOutAddress_ReturnsNullError()
{
var memory = new FakeCpuMemory(Base, RegionSize);
var context = new CpuContext(memory, Generation.Gen5);
context[CpuRegister.Rdi] = 0;
var result = FiberExports.FiberGetSelf(context);
Assert.Equal(ErrorNull, result);
}
[Fact]
public void GetSelf_OutsideFiberContext_ReturnsPermissionError()
{
var memory = new FakeCpuMemory(Base, RegionSize);
var context = new CpuContext(memory, Generation.Gen5);
context[CpuRegister.Rdi] = Base + 0x100;
var result = FiberExports.FiberGetSelf(context);
Assert.Equal(ErrorPermission, result);
}
[Fact]
public void GetInfo_NullInfo_ReturnsNullError()
{
var memory = new FakeCpuMemory(Base, RegionSize);
var context = new CpuContext(memory, Generation.Gen5);
context[CpuRegister.Rdi] = FiberAddress;
context[CpuRegister.Rsi] = 0;
var result = FiberExports.FiberGetInfo(context);
Assert.Equal(ErrorNull, result);
}
[Fact]
public void Initialize_NullFiber_ReturnsNullError()
{
var memory = new FakeCpuMemory(Base, RegionSize);
var context = new CpuContext(memory, Generation.Gen5);
WriteCString(memory, NameAddress, "F");
context[CpuRegister.Rdi] = 0;
context[CpuRegister.Rsi] = NameAddress;
context[CpuRegister.Rdx] = EntryAddress;
var result = FiberExports.FiberInitialize(context);
Assert.Equal(ErrorNull, result);
}
[Fact]
public void Initialize_NullName_ReturnsNullError()
{
var memory = new FakeCpuMemory(Base, RegionSize);
var context = new CpuContext(memory, Generation.Gen5);
context[CpuRegister.Rdi] = FiberAddress;
context[CpuRegister.Rsi] = 0;
context[CpuRegister.Rdx] = EntryAddress;
var result = FiberExports.FiberInitialize(context);
Assert.Equal(ErrorNull, result);
}
[Fact]
public void Initialize_NullEntry_ReturnsNullError()
{
var memory = new FakeCpuMemory(Base, RegionSize);
var context = new CpuContext(memory, Generation.Gen5);
WriteCString(memory, NameAddress, "F");
context[CpuRegister.Rdi] = FiberAddress;
context[CpuRegister.Rsi] = NameAddress;
context[CpuRegister.Rdx] = 0;
var result = FiberExports.FiberInitialize(context);
Assert.Equal(ErrorNull, result);
}
[Fact]
public void Initialize_MisalignedFiber_ReturnsAlignmentError()
{
var memory = new FakeCpuMemory(Base, RegionSize);
var context = new CpuContext(memory, Generation.Gen5);
WriteCString(memory, NameAddress, "F");
context[CpuRegister.Rdi] = FiberAddress + 4;
context[CpuRegister.Rsi] = NameAddress;
context[CpuRegister.Rdx] = EntryAddress;
var result = FiberExports.FiberInitialize(context);
Assert.Equal(ErrorAlignment, result);
}
[Fact]
public void Initialize_TooSmallContextSize_ReturnsRangeError()
{
var memory = new FakeCpuMemory(Base, RegionSize);
var context = new CpuContext(memory, Generation.Gen5);
WriteCString(memory, NameAddress, "F");
context[CpuRegister.Rdi] = FiberAddress;
context[CpuRegister.Rsi] = NameAddress;
context[CpuRegister.Rdx] = EntryAddress;
context[CpuRegister.R8] = ContextAddress;
context[CpuRegister.R9] = 256;
var result = FiberExports.FiberInitialize(context);
Assert.Equal(ErrorRange, result);
}
[Fact]
public void Initialize_ContextAddressWithoutSize_ReturnsInvalidError()
{
var memory = new FakeCpuMemory(Base, RegionSize);
var context = new CpuContext(memory, Generation.Gen5);
WriteCString(memory, NameAddress, "F");
context[CpuRegister.Rdi] = FiberAddress;
context[CpuRegister.Rsi] = NameAddress;
context[CpuRegister.Rdx] = EntryAddress;
context[CpuRegister.R8] = ContextAddress;
context[CpuRegister.R9] = 0;
var result = FiberExports.FiberInitialize(context);
Assert.Equal(ErrorInvalid, result);
}
[Fact]
public void Initialize_Valid_WritesExpectedLayout()
{
var memory = new FakeCpuMemory(Base, RegionSize);
var context = new CpuContext(memory, Generation.Gen5);
const string name = "TestFiber";
WriteCString(memory, NameAddress, name);
const ulong argOnInitialize = 0xDEADUL;
const ulong contextSize = 512UL;
context[CpuRegister.Rdi] = FiberAddress;
context[CpuRegister.Rsi] = NameAddress;
context[CpuRegister.Rdx] = EntryAddress;
context[CpuRegister.Rcx] = argOnInitialize;
context[CpuRegister.R8] = ContextAddress;
context[CpuRegister.R9] = contextSize;
// RSP defaults to 0 (unmapped); ReadStackArg64 falls back to 0 ->
// optParam = 0, buildVersion = 0. ApplyInitializationFlags(0, 0, false) == 0.
var result = FiberExports.FiberInitialize(context);
Assert.Equal(0, result);
Assert.Equal(0UL, context[CpuRegister.Rax]);
Assert.Equal(SignatureStart, ReadUInt32(memory, FiberAddress + 0));
Assert.Equal(StateIdle, ReadUInt32(memory, FiberAddress + 4));
Assert.Equal(EntryAddress, ReadUInt64(memory, FiberAddress + 8));
Assert.Equal(argOnInitialize, ReadUInt64(memory, FiberAddress + 16));
Assert.Equal(ContextAddress, ReadUInt64(memory, FiberAddress + 24));
Assert.Equal(contextSize, ReadUInt64(memory, FiberAddress + 32));
AssertInlineName(memory, FiberAddress + 40, name);
Assert.Equal(0UL, ReadUInt64(memory, FiberAddress + 72));
Assert.Equal(0u, ReadUInt32(memory, FiberAddress + 80));
Assert.Equal(ContextAddress, ReadUInt64(memory, FiberAddress + 88));
Assert.Equal(ContextAddress + contextSize, ReadUInt64(memory, FiberAddress + 96));
Assert.Equal(SignatureEnd, ReadUInt32(memory, FiberAddress + 104));
Assert.Equal(StackSignature, ReadUInt64(memory, ContextAddress));
}
[Fact]
public void GetInfo_AfterInitialize_RoundTripsFields()
{
var memory = new FakeCpuMemory(Base, RegionSize);
var context = new CpuContext(memory, Generation.Gen5);
const string name = "RoundTrip";
WriteCString(memory, NameAddress, name);
const ulong argOnInitialize = 0xCAFEUL;
const ulong contextSize = 512UL;
context[CpuRegister.Rdi] = FiberAddress;
context[CpuRegister.Rsi] = NameAddress;
context[CpuRegister.Rdx] = EntryAddress;
context[CpuRegister.Rcx] = argOnInitialize;
context[CpuRegister.R8] = ContextAddress;
context[CpuRegister.R9] = contextSize;
Assert.Equal(0, FiberExports.FiberInitialize(context));
WriteUInt64(memory, InfoAddress, 128);
context[CpuRegister.Rdi] = FiberAddress;
context[CpuRegister.Rsi] = InfoAddress;
var result = FiberExports.FiberGetInfo(context);
Assert.Equal(0, result);
Assert.Equal(EntryAddress, ReadUInt64(memory, InfoAddress + 8));
Assert.Equal(argOnInitialize, ReadUInt64(memory, InfoAddress + 16));
Assert.Equal(ContextAddress, ReadUInt64(memory, InfoAddress + 24));
Assert.Equal(contextSize, ReadUInt64(memory, InfoAddress + 32));
AssertInlineName(memory, InfoAddress + 40, name);
Assert.Equal(ulong.MaxValue, ReadUInt64(memory, InfoAddress + 72));
}
[Fact]
public void GetInfo_WrongSize_ReturnsInvalidError()
{
var memory = new FakeCpuMemory(Base, RegionSize);
var context = new CpuContext(memory, Generation.Gen5);
WriteCString(memory, NameAddress, "F");
context[CpuRegister.Rdi] = FiberAddress;
context[CpuRegister.Rsi] = NameAddress;
context[CpuRegister.Rdx] = EntryAddress;
context[CpuRegister.R8] = ContextAddress;
context[CpuRegister.R9] = 512;
Assert.Equal(0, FiberExports.FiberInitialize(context));
WriteUInt64(memory, InfoAddress, 64);
context[CpuRegister.Rdi] = FiberAddress;
context[CpuRegister.Rsi] = InfoAddress;
var result = FiberExports.FiberGetInfo(context);
Assert.Equal(ErrorInvalid, result);
}
private static void WriteCString(FakeCpuMemory memory, ulong address, string text)
{
memory.WriteCString(address, text);
}
private static void WriteUInt64(FakeCpuMemory memory, ulong address, ulong value)
{
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
Assert.True(memory.TryWrite(address, buffer));
}
private static uint ReadUInt32(FakeCpuMemory memory, ulong address)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
Assert.True(memory.TryRead(address, buffer));
return BinaryPrimitives.ReadUInt32LittleEndian(buffer);
}
private static ulong ReadUInt64(FakeCpuMemory memory, ulong address)
{
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
Assert.True(memory.TryRead(address, buffer));
return BinaryPrimitives.ReadUInt64LittleEndian(buffer);
}
private static void AssertInlineName(FakeCpuMemory memory, ulong address, string expected)
{
Span<byte> buffer = stackalloc byte[32];
Assert.True(memory.TryRead(address, buffer));
var length = buffer.IndexOf((byte)0);
if (length < 0)
{
length = buffer.Length;
}
Assert.Equal(expected, System.Text.Encoding.UTF8.GetString(buffer[..length]));
}
}
@@ -1,100 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.GUI;
using Xunit;
namespace SharpEmu.Libs.Tests.GUI;
public sealed class GuiSettingsTests
{
[Fact]
public void NormalizeFromJson_AllPropertiesNull_FallsBackToDefaults()
{
const string json = """
{
"LogLevel": null,
"GameFolders": null,
"ExcludedGames": null,
"EnvironmentToggles": null,
"Language": null,
"DiscordClientId": null
}
""";
var settings = GuiSettings.NormalizeFromJson(json);
Assert.Equal("Info", settings.LogLevel);
Assert.Equal("en", settings.Language);
Assert.Equal("1525606762248540221", settings.DiscordClientId);
Assert.Empty(settings.GameFolders);
Assert.Empty(settings.ExcludedGames);
Assert.Empty(settings.EnvironmentToggles);
}
[Fact]
public void NormalizeFromJson_ValidValues_ArePreserved()
{
const string json = """
{
"LogLevel": "Debug",
"GameFolders": ["C:\\Games"],
"ExcludedGames": ["C:\\Games\\skip.bin"],
"EnvironmentToggles": ["SHARPEMU_TRACE"],
"Language": "pt-BR",
"DiscordClientId": "999"
}
""";
var settings = GuiSettings.NormalizeFromJson(json);
Assert.Equal("Debug", settings.LogLevel);
Assert.Equal("pt-BR", settings.Language);
Assert.Equal("999", settings.DiscordClientId);
Assert.Equal(["C:\\Games"], settings.GameFolders);
Assert.Equal(["C:\\Games\\skip.bin"], settings.ExcludedGames);
Assert.Equal(["SHARPEMU_TRACE"], settings.EnvironmentToggles);
}
// An empty Discord client ID intentionally disables Rich Presence.
[Fact]
public void NormalizeFromJson_EmptyDiscordClientId_IsPreservedNotNormalized()
{
const string json = """{ "DiscordClientId": "" }""";
var settings = GuiSettings.NormalizeFromJson(json);
Assert.Equal(string.Empty, settings.DiscordClientId);
}
[Fact]
public void NormalizeFromJson_NullOrEmptyListEntries_AreFilteredOut()
{
const string json = """
{
"GameFolders": ["C:\\Games", null, ""],
"ExcludedGames": [null],
"EnvironmentToggles": [null, "SHARPEMU_TRACE", ""]
}
""";
var settings = GuiSettings.NormalizeFromJson(json);
Assert.Equal(["C:\\Games"], settings.GameFolders);
Assert.Empty(settings.ExcludedGames);
Assert.Equal(["SHARPEMU_TRACE"], settings.EnvironmentToggles);
}
[Fact]
public void NormalizeFromJson_EmptyObject_UsesConstructorDefaults()
{
var settings = GuiSettings.NormalizeFromJson("{}");
Assert.Equal("Info", settings.LogLevel);
Assert.Equal("en", settings.Language);
Assert.Equal("1525606762248540221", settings.DiscordClientId);
Assert.Empty(settings.GameFolders);
Assert.Empty(settings.ExcludedGames);
Assert.Empty(settings.EnvironmentToggles);
}
}
@@ -1,60 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.GUI;
using Xunit;
namespace SharpEmu.Libs.Tests.GUI;
public sealed class PerGameSettingsTests
{
// Invalid entries must not reach Environment.SetEnvironmentVariable.
[Fact]
public void NormalizeFromJson_NullOrEmptyToggleEntries_AreFilteredOut()
{
const string json = """
{ "EnvironmentToggles": [null, "SHARPEMU_TRACE", ""] }
""";
var settings = PerGameSettings.NormalizeFromJson(json);
Assert.NotNull(settings);
Assert.Equal(["SHARPEMU_TRACE"], settings.EnvironmentToggles);
}
// A null list means that the global setting should be inherited.
[Fact]
public void NormalizeFromJson_NullToggleList_StaysNull()
{
const string json = """{ "EnvironmentToggles": null }""";
var settings = PerGameSettings.NormalizeFromJson(json);
Assert.NotNull(settings);
Assert.Null(settings.EnvironmentToggles);
}
[Fact]
public void NormalizeFromJson_EmptyToggleList_StaysEmpty()
{
const string json = """{ "EnvironmentToggles": [] }""";
var settings = PerGameSettings.NormalizeFromJson(json);
Assert.NotNull(settings);
Assert.Empty(Assert.IsType<List<string>>(settings.EnvironmentToggles));
}
[Fact]
public void NormalizeFromJson_ValidToggles_ArePreserved()
{
const string json = """
{ "EnvironmentToggles": ["SHARPEMU_TRACE", "SHARPEMU_NO_JIT"] }
""";
var settings = PerGameSettings.NormalizeFromJson(json);
Assert.NotNull(settings);
Assert.Equal(["SHARPEMU_TRACE", "SHARPEMU_NO_JIT"], settings.EnvironmentToggles);
}
}
@@ -1,77 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using Xunit;
namespace SharpEmu.Libs.Tests.HLE;
public sealed class GuestWriteWatchTests
{
[Theory]
[InlineData(0x0000001000000001, "torn")]
[InlineData(0x0000FFFF00000001, "torn")]
[InlineData(0x0000000008000000, "shift")]
[InlineData(0x0000000080015F00, "shift")]
[InlineData(0x0000000007FFFFFF, null)]
[InlineData(0x0000000009000000, null)]
[InlineData(0x000000003F800000, null)]
[InlineData(0x0000000080015F01, null)]
[InlineData(0x0001000000000001, null)]
public void ClassifyBulkValue_RecognizesCorruptionSignatures(ulong value, string? expected)
{
Assert.Equal(expected, GuestWriteWatch.ClassifyBulkValue(value));
}
[Theory]
[InlineData(0, 0)]
[InlineData(1, 7)]
[InlineData(3, 5)]
[InlineData(7, 1)]
[InlineData(8, 0)]
public void FirstAlignedOffset_ReturnsTheNextEightByteBoundary(ulong address, int expected)
{
Assert.Equal(expected, GuestWriteWatch.FirstAlignedOffset(address));
}
[Theory]
[InlineData(0x1000, 8, 0x1000, true)]
[InlineData(0x0FFF, 1, 0x1000, false)]
[InlineData(0x0FFF, 2, 0x1000, true)]
[InlineData(0x1008, 1, 0x1000, false)]
[InlineData(ulong.MaxValue - 3, 4, ulong.MaxValue - 1, true)]
[InlineData(ulong.MaxValue, 1, ulong.MaxValue, true)]
[InlineData(0x1000, 0, 0x1000, false)]
public void Overlaps_HandlesBoundariesWithoutOverflow(
ulong address,
int length,
ulong slot,
bool expected)
{
Assert.Equal(expected, GuestWriteWatch.Overlaps(address, length, slot));
}
[Theory]
[InlineData(0x10000, 0xF2, true)]
[InlineData(0x10001, 0xF2, false)]
[InlineData(0x10000, 0xF1, false)]
public void IsPoolMapping_RequiresTheExpectedSizeAndProtection(
ulong length,
int protection,
bool expected)
{
Assert.Equal(expected, GuestWriteWatch.IsPoolMapping(length, protection));
}
[Theory]
[InlineData(null, 0)]
[InlineData("", 0)]
[InlineData("not-hex", 0)]
[InlineData("80", 0x80)]
[InlineData("0x80", 0x80)]
[InlineData(" 0X801DB3BBB ", 0x801DB3BBB)]
public void Parse_HandlesHexadecimalWatchValues(string? text, ulong expected)
{
Assert.Equal(expected, GuestWriteWatch.Parse(text));
}
}
@@ -41,90 +41,6 @@ public sealed class KernelMemoryCompatExportsTests
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
}
[Fact]
public void PosixOpen_MissingFileReturnsMinusOne()
{
const ulong memoryBase = 0x1_0000_0000;
const ulong pathAddress = memoryBase + 0x100;
var memory = new FakeCpuMemory(memoryBase, 0x1000);
var context = new CpuContext(memory, Generation.Gen5);
memory.WriteCString(pathAddress, "/__sharpemu_test_missing__/il2cpp.usym");
context[CpuRegister.Rdi] = pathAddress;
context[CpuRegister.Rsi] = 0; // O_RDONLY
var result = KernelMemoryCompatExports.PosixOpen(context);
// A libc open() failure must be -1, not the raw 0x8002xxxx sentinel the
// guest would otherwise store as a valid fd and later dereference.
Assert.Equal(-1, result);
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
}
[Fact]
public void PosixFstat_BadDescriptorReturnsMinusOne()
{
const ulong memoryBase = 0x1_0000_0000;
const ulong statAddress = memoryBase + 0x400;
var memory = new FakeCpuMemory(memoryBase, 0x1000);
var context = new CpuContext(memory, Generation.Gen5);
context[CpuRegister.Rdi] = 0x80020002; // the not-found sentinel misused as an fd
context[CpuRegister.Rsi] = statAddress;
var result = KernelMemoryCompatExports.PosixFstat(context);
Assert.Equal(-1, result);
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
}
[Fact]
public void PosixClose_BadDescriptorReturnsMinusOne()
{
const ulong memoryBase = 0x1_0000_0000;
var memory = new FakeCpuMemory(memoryBase, 0x1000);
var context = new CpuContext(memory, Generation.Gen5);
context[CpuRegister.Rdi] = 0x80020002; // never-opened / sentinel fd
var result = KernelMemoryCompatExports.PosixClose(context);
Assert.Equal(-1, result);
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
}
[Fact]
public void PosixRead_BadDescriptorReturnsMinusOne()
{
const ulong memoryBase = 0x1_0000_0000;
const ulong bufferAddress = memoryBase + 0x200;
var memory = new FakeCpuMemory(memoryBase, 0x1000);
var context = new CpuContext(memory, Generation.Gen5);
context[CpuRegister.Rdi] = 0x80020002; // never-opened / sentinel fd
context[CpuRegister.Rsi] = bufferAddress;
context[CpuRegister.Rdx] = 0x40;
var result = KernelMemoryCompatExports.PosixRead(context);
Assert.Equal(-1, result);
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
}
[Fact]
public void PosixWrite_BadDescriptorReturnsMinusOne()
{
const ulong memoryBase = 0x1_0000_0000;
const ulong bufferAddress = memoryBase + 0x200;
var memory = new FakeCpuMemory(memoryBase, 0x1000);
var context = new CpuContext(memory, Generation.Gen5);
memory.WriteCString(bufferAddress, "payload");
context[CpuRegister.Rdi] = 0x80020002; // never-opened / sentinel fd
context[CpuRegister.Rsi] = bufferAddress;
context[CpuRegister.Rdx] = 0x7;
var result = KernelMemoryCompatExports.PosixWrite(context);
Assert.Equal(-1, result);
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
}
[Fact]
public void Sprintf_ReadsVariadicDoubleFromXmmRegister()
{
@@ -37,11 +37,10 @@ public sealed class KernelSocketCompatExportsTests
KernelMemoryCompatExports.PosixClose(context));
Assert.Equal(0UL, context[CpuRegister.Rax]);
// A second close of an already-closed fd fails per the POSIX ABI:
// -1 with errno set, not the raw Orbis NOT_FOUND sentinel.
context[CpuRegister.Rdi] = unchecked((ulong)guestFd);
Assert.Equal(-1, KernelMemoryCompatExports.PosixClose(context));
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
Assert.Equal(
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND,
KernelMemoryCompatExports.PosixClose(context));
}
finally
{
@@ -1,137 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.HLE;
using SharpEmu.Libs.LibcInternal;
using Xunit;
namespace SharpEmu.Libs.Tests.LibcInternal;
public sealed class LibcInternalExportsTests
{
private const ulong Base = 0x3_0000_0000;
private const ulong InfoAddress = Base + 0x100;
private const ulong ExpectedInfoSize = 32;
[Fact]
public void HeapGetTraceInfo_NullPointer_ReturnsInvalidArgument()
{
var memory = new FakeCpuMemory(Base, 0x1000);
var context = new CpuContext(memory, Generation.Gen5);
context[CpuRegister.Rdi] = 0;
var result = LibcInternalExports.LibcHeapGetTraceInfo(context);
Assert.Equal(
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT,
result);
Assert.Equal(0UL, context[CpuRegister.Rax]);
}
[Fact]
public void HeapGetTraceInfo_WrongSize_ReturnsInvalidArgument()
{
var memory = new FakeCpuMemory(Base, 0x1000);
var context = new CpuContext(memory, Generation.Gen5);
Span<byte> sizeBytes = stackalloc byte[sizeof(ulong)];
BinaryPrimitives.WriteUInt64LittleEndian(
sizeBytes,
ExpectedInfoSize - 1);
Assert.True(memory.TryWrite(InfoAddress, sizeBytes));
context[CpuRegister.Rdi] = InfoAddress;
var result = LibcInternalExports.LibcHeapGetTraceInfo(context);
Assert.Equal(
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT,
result);
Assert.Equal(0UL, context[CpuRegister.Rax]);
}
[Fact]
public void HeapGetTraceInfo_ValidBuffer_WritesStablePointers()
{
var memory = new FakeCpuMemory(Base, 0x1000);
var context = new CpuContext(memory, Generation.Gen5);
Span<byte> sizeBytes = stackalloc byte[sizeof(ulong)];
BinaryPrimitives.WriteUInt64LittleEndian(
sizeBytes,
ExpectedInfoSize);
Assert.True(memory.TryWrite(InfoAddress, sizeBytes));
context[CpuRegister.Rdi] = InfoAddress;
var firstResult =
LibcInternalExports.LibcHeapGetTraceInfo(context);
Assert.Equal(
(int)OrbisGen2Result.ORBIS_GEN2_OK,
firstResult);
Assert.Equal(0UL, context[CpuRegister.Rax]);
Assert.True(
context.TryReadUInt64(
InfoAddress + 16,
out var firstMaskAddress));
Assert.True(
context.TryReadUInt64(
InfoAddress + 24,
out var firstTableAddress));
Assert.NotEqual(0UL, firstMaskAddress);
Assert.Equal(firstMaskAddress + 8UL, firstTableAddress);
var secondResult =
LibcInternalExports.LibcHeapGetTraceInfo(context);
Assert.Equal(
(int)OrbisGen2Result.ORBIS_GEN2_OK,
secondResult);
Assert.True(
context.TryReadUInt64(
InfoAddress + 16,
out var secondMaskAddress));
Assert.True(
context.TryReadUInt64(
InfoAddress + 24,
out var secondTableAddress));
Assert.Equal(firstMaskAddress, secondMaskAddress);
Assert.Equal(firstTableAddress, secondTableAddress);
}
[Fact]
public void HeapGetTraceInfo_TruncatedOutput_ReturnsMemoryFault()
{
var memory = new FakeCpuMemory(Base, 31);
var context = new CpuContext(memory, Generation.Gen5);
Span<byte> sizeBytes = stackalloc byte[sizeof(ulong)];
BinaryPrimitives.WriteUInt64LittleEndian(
sizeBytes,
ExpectedInfoSize);
Assert.True(memory.TryWrite(Base, sizeBytes));
context[CpuRegister.Rdi] = Base;
var result = LibcInternalExports.LibcHeapGetTraceInfo(context);
Assert.Equal(
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT,
result);
Assert.Equal(0UL, context[CpuRegister.Rax]);
}
}
@@ -1,135 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
using SharpEmu.HLE;
using Xunit;
namespace SharpEmu.Libs.Tests.Memory;
/// <summary>
/// The write generation lets GPU caches detect guest CPU rewrites even after
/// another cache owner consumed the (single) dirty flag: the generation is
/// monotonic and survives consume/re-arm cycles and range replacement. These
/// invariants back the presenter's stale-upload detection for CPU-rewritten
/// images (video planes, streamed font atlases).
/// </summary>
public sealed unsafe class GuestImageWriteTrackerTests
{
// The tracker aligns to the guest's 4 KiB pages; the mprotect underneath
// operates on host pages, which are 16 KiB on Apple Silicon (the emulator
// itself always runs with 4 KiB host pages under Rosetta, but this test
// host may not). Align the allocation to the largest host page size so
// the kernel's rounding stays inside memory this test owns instead of
// spilling onto neighbouring heap pages.
private const nuint TrackedByteCount = 4096;
private const nuint HostPageAlignment = 16384;
private static ulong AllocateTrackedPages(out void* allocation)
{
allocation = NativeMemory.AlignedAlloc(2 * HostPageAlignment, HostPageAlignment);
return (ulong)allocation;
}
[Fact]
public void GenerationSurvivesDirtyConsume()
{
if (!GuestImageWriteTracker.Enabled)
{
return;
}
var address = AllocateTrackedPages(out var allocation);
try
{
GuestImageWriteTracker.Track(address, TrackedByteCount);
Assert.True(GuestImageWriteTracker.TryGetWriteGeneration(address, out var generation));
Assert.Equal(0, generation);
Assert.True(GuestImageWriteTracker.TryHandleWriteFault(address));
Assert.True(GuestImageWriteTracker.ConsumeDirty(address));
// Consuming the dirty flag must not roll back the generation:
// that is exactly what lets a second cache owner still observe
// the rewrite after the first owner consumed the flag.
Assert.True(GuestImageWriteTracker.TryGetWriteGeneration(address, out generation));
Assert.Equal(1, generation);
}
finally
{
GuestImageWriteTracker.Untrack(address);
NativeMemory.Free(allocation);
}
}
[Fact]
public void GenerationIncrementsOncePerArmedLifetime()
{
if (!GuestImageWriteTracker.Enabled)
{
return;
}
var address = AllocateTrackedPages(out var allocation);
try
{
GuestImageWriteTracker.Track(address, TrackedByteCount);
Assert.True(GuestImageWriteTracker.TryHandleWriteFault(address));
// The first fault disarmed the range; later writes are free-running
// and must not inflate the generation until the owner re-arms.
Assert.True(GuestImageWriteTracker.TryHandleWriteFault(address));
Assert.True(GuestImageWriteTracker.TryGetWriteGeneration(address, out var generation));
Assert.Equal(1, generation);
GuestImageWriteTracker.Rearm(address);
Assert.True(GuestImageWriteTracker.TryHandleWriteFault(address));
Assert.True(GuestImageWriteTracker.TryGetWriteGeneration(address, out generation));
Assert.Equal(2, generation);
}
finally
{
GuestImageWriteTracker.Untrack(address);
NativeMemory.Free(allocation);
}
}
[Fact]
public void GenerationCarriesAcrossRangeReplacement()
{
if (!GuestImageWriteTracker.Enabled)
{
return;
}
var address = AllocateTrackedPages(out var allocation);
try
{
GuestImageWriteTracker.Track(address, TrackedByteCount);
Assert.True(GuestImageWriteTracker.TryHandleWriteFault(address));
// Re-registering the same allocation with a different size retires
// the range object (the signal handler may still see the old
// snapshot) but must carry the generation, otherwise a resize
// would hide the rewrite from cache owners.
GuestImageWriteTracker.Track(address, 2 * TrackedByteCount);
Assert.True(GuestImageWriteTracker.TryGetWriteGeneration(address, out var generation));
Assert.Equal(1, generation);
}
finally
{
GuestImageWriteTracker.Untrack(address);
NativeMemory.Free(allocation);
}
}
[Fact]
public void UntrackedAddressHasNoGeneration()
{
if (!GuestImageWriteTracker.Enabled)
{
return;
}
Assert.False(GuestImageWriteTracker.TryGetWriteGeneration(0xDEAD_0000_0000UL, out _));
}
}
@@ -121,113 +121,6 @@ public sealed class GuestMemoryAllocatorTests
Assert.Equal([alignedAddress + 0x1000], host.FreedAddresses);
}
[Fact]
public void TryBackFixedRangeFillsOnlyTheFreePagesOfAPartiallyOccupiedRange()
{
const ulong rangeBase = 0x0000_0020_2F00_0000;
const ulong rangeSize = 0x40_0000;
const ulong occupiedSize = 0x4_0000;
using var host = new PartialOverlapHostMemory(rangeBase, occupiedSize, rangeSize);
using var memory = new PhysicalVirtualMemory(host);
Assert.True(memory.TryBackFixedRange(rangeBase, rangeSize, executable: false));
// Only the free tail is allocated; the already-occupied head is untouched.
Assert.Equal(
[(rangeBase + occupiedSize, rangeSize - occupiedSize)],
host.AllocationCalls);
}
[Fact]
public void TryBackFixedRangeReturnsFalseWhenRangeIsFullyOccupied()
{
const ulong rangeBase = 0x0000_0020_2F00_0000;
const ulong rangeSize = 0x40_0000;
using var host = new PartialOverlapHostMemory(rangeBase, rangeSize, rangeSize);
using var memory = new PhysicalVirtualMemory(host);
Assert.False(memory.TryBackFixedRange(rangeBase, rangeSize, executable: false));
Assert.Empty(host.AllocationCalls);
}
private sealed class PartialOverlapHostMemory : IHostMemory, IDisposable
{
private readonly ulong _rangeBase;
private readonly ulong _occupiedEnd;
private readonly ulong _rangeEnd;
public PartialOverlapHostMemory(ulong rangeBase, ulong occupiedSize, ulong rangeSize)
{
_rangeBase = rangeBase;
_occupiedEnd = rangeBase + occupiedSize;
_rangeEnd = rangeBase + rangeSize;
}
public List<(ulong Address, ulong Size)> AllocationCalls { get; } = [];
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection)
{
AllocationCalls.Add((desiredAddress, size));
return desiredAddress;
}
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) => 0;
public bool Commit(ulong address, ulong size, HostPageProtection protection) => true;
public bool Free(ulong address) => true;
public bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection)
{
rawOldProtection = 0;
return true;
}
public bool ProtectRaw(ulong address, ulong size, uint rawProtection, out uint rawOldProtection)
{
rawOldProtection = 0;
return true;
}
public bool Query(ulong address, out HostRegionInfo info)
{
// Report contiguous runs of same-state pages, exactly as VirtualQuery
// does: the occupied head first, then the free tail.
if (address < _occupiedEnd)
{
info = new HostRegionInfo(
address,
_rangeBase,
_occupiedEnd - address,
HostRegionState.Committed,
RawState: 0x1000,
HostPageProtection.ReadWrite,
RawProtection: 0x04,
RawAllocationProtection: 0x04);
return true;
}
info = new HostRegionInfo(
address,
AllocationBase: 0,
_rangeEnd - address,
HostRegionState.Free,
RawState: 0x10000,
HostPageProtection.NoAccess,
RawProtection: 0x01,
RawAllocationProtection: 0);
return true;
}
public void FlushInstructionCache(ulong address, ulong size)
{
}
public void Dispose()
{
}
}
private sealed class FakeHostMemory : IHostMemory
{
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) =>
@@ -39,60 +39,6 @@ public sealed class PhysicalVirtualMemoryTests
Assert.Equal([(page, 0x1000UL, HostPageProtection.ReadWrite)], host.CommitCalls);
}
[Fact]
public void RepeatedLazyReadUsesCommittedRangeCache()
{
using var host = new LazyZeroedHostMemory();
using var memory = new PhysicalVirtualMemory(host);
var address = memory.AllocateAt(0, (4UL << 30) + 0x1000, executable: false);
host.CommitCalls.Clear();
Span<byte> buffer = stackalloc byte[16];
Assert.True(memory.TryRead(address + 0x100, buffer));
var queryCallsAfterFirstRead = host.QueryCalls;
Assert.True(memory.TryRead(address + 0x108, buffer[..8]));
Assert.Equal(queryCallsAfterFirstRead, host.QueryCalls);
Assert.Single(host.CommitCalls);
}
[Fact]
public void TryCopyHandlesOverlappingIdentityMappedRanges()
{
using var host = new LazyZeroedHostMemory();
using var memory = new PhysicalVirtualMemory(host);
var address = memory.AllocateAt(0, (4UL << 30) + 0x1000, executable: false);
Assert.True(memory.TryWrite(address, new byte[] { 1, 2, 3, 4, 5, 6 }));
Assert.True(memory.TryCopy(address + 2, address, 4));
Span<byte> result = stackalloc byte[6];
Assert.True(memory.TryRead(address, result));
Assert.Equal(new byte[] { 1, 2, 1, 2, 3, 4 }, result.ToArray());
}
[Fact]
public void RepeatedTryCopyKeepsSourceAndDestinationCommitRangesCached()
{
using var host = new LazyZeroedHostMemory();
using var memory = new PhysicalVirtualMemory(host);
var address = memory.AllocateAt(0, (4UL << 30) + 0x1000, executable: false);
var source = address + 0x100;
var destination = address + 0x1100;
Assert.True(memory.TryWrite(source, new byte[] { 1, 2, 3, 4 }));
Assert.True(memory.TryWrite(destination, new byte[4]));
host.CommitCalls.Clear();
Assert.True(memory.TryCopy(destination, source, 4));
var queryCallsAfterFirstCopy = host.QueryCalls;
Assert.True(memory.TryCopy(destination, source, 4));
Assert.Equal(queryCallsAfterFirstCopy, host.QueryCalls);
}
// 2. Reserve-only region: GetPointer commits the page before returning it,
// so callers receive a valid (non-null) pointer. An unmapped address yields null.
[Fact]
@@ -179,14 +125,12 @@ public sealed class PhysicalVirtualMemoryTests
public LazyZeroedHostMemory()
{
_allocation = System.Runtime.InteropServices.NativeMemory.AllocZeroed(0x3000);
_allocation = System.Runtime.InteropServices.NativeMemory.AllocZeroed(0x2000);
_address = ((ulong)_allocation + 0xFFF) & ~0xFFFUL;
}
public List<(ulong Address, ulong Size, HostPageProtection Protection)> CommitCalls { get; } = [];
public int QueryCalls { get; private set; }
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) => _address;
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) => _address;
@@ -218,7 +162,6 @@ public sealed class PhysicalVirtualMemoryTests
public bool Query(ulong address, out HostRegionInfo info)
{
QueryCalls++;
var pageAddress = address & ~0xFFFUL;
info = new HostRegionInfo(
pageAddress,
@@ -10,7 +10,7 @@ namespace SharpEmu.Libs.Tests.Pthread;
public sealed class PthreadMutexSemanticsTests
{
[Fact]
public void AdaptiveMutex_SelfLockIsIdempotent()
public void AdaptiveMutex_SelfLockUsesCompatibilityRecursion()
{
const ulong memoryBase = 0x1_0000_0000;
const ulong mutexAddress = memoryBase + 0x100;
@@ -22,180 +22,7 @@ public sealed class PthreadMutexSemanticsTests
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(context));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(context));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
Assert.NotEqual(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
}
[Fact]
public void AdaptiveMutex_GuestTrackedSelfLockReturnsDeadlockAndSingleUnlockReleases()
{
const ulong memoryBase = 0x1_0001_0000;
const ulong mutexAddress = memoryBase + 0x100;
var memory = new AllocatingCpuMemory(memoryBase, 0x4000);
var context = new CpuContext(memory, Generation.Gen5);
Assert.True(context.TryWriteUInt64(mutexAddress, 1)); // Static adaptive initializer.
context[CpuRegister.Rdi] = mutexAddress;
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(context));
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
Assert.True(context.TryWriteUInt64(mutexAddress + 8, currentThreadHandle));
Assert.Equal(
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK,
KernelPthreadCompatExports.PthreadMutexLock(context));
Assert.True(context.TryWriteUInt64(mutexAddress + 8, 0));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexTrylock(context));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
}
[Fact]
public void RecursiveMutex_GuestTrackedSelfLockKeepsRecursiveSemantics()
{
const ulong memoryBase = 0x1_0002_0000;
const ulong attrAddress = memoryBase + 0x100;
const ulong mutexAddress = memoryBase + 0x200;
var memory = new AllocatingCpuMemory(memoryBase, 0x4000);
var context = new CpuContext(memory, Generation.Gen5);
context[CpuRegister.Rdi] = attrAddress;
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexattrInit(context));
context[CpuRegister.Rsi] = 2;
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexattrSettype(context));
context[CpuRegister.Rdi] = mutexAddress;
context[CpuRegister.Rsi] = attrAddress;
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexInit(context));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(context));
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
Assert.True(context.TryWriteUInt64(mutexAddress + 8, currentThreadHandle));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(context));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
Assert.NotEqual(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
}
[Fact]
public async Task ContendedMutex_HandsOffOneHostWaiterAtATime()
{
const ulong memoryBase = 0x2_0000_0000;
const ulong mutexAddress = memoryBase + 0x100;
var memory = new AllocatingCpuMemory(memoryBase, 0x4000);
var ownerContext = new CpuContext(memory, Generation.Gen5);
Assert.True(ownerContext.TryWriteUInt64(mutexAddress, 1));
ownerContext[CpuRegister.Rdi] = mutexAddress;
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(ownerContext));
using var waitersStarted = new CountdownEvent(2);
using var firstAcquired = new ManualResetEventSlim(false);
using var secondAcquired = new ManualResetEventSlim(false);
using var releaseFirst = new ManualResetEventSlim(false);
var acquisitionCount = 0;
Task<(int LockResult, int UnlockResult)> StartWaiter() =>
Task.Factory.StartNew(
() =>
{
var waiterContext = new CpuContext(memory, Generation.Gen5);
waiterContext[CpuRegister.Rdi] = mutexAddress;
waitersStarted.Signal();
var lockResult = KernelPthreadCompatExports.PthreadMutexLock(waiterContext);
if (lockResult != 0)
{
return (lockResult, int.MinValue);
}
if (Interlocked.Increment(ref acquisitionCount) == 1)
{
firstAcquired.Set();
releaseFirst.Wait(TimeSpan.FromSeconds(5));
}
else
{
secondAcquired.Set();
}
var unlockResult = KernelPthreadCompatExports.PthreadMutexUnlock(waiterContext);
return (lockResult, unlockResult);
},
CancellationToken.None,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
var firstWaiter = StartWaiter();
var secondWaiter = StartWaiter();
Assert.True(waitersStarted.Wait(TimeSpan.FromSeconds(5)));
Thread.Sleep(50);
Assert.Equal(0, Volatile.Read(ref acquisitionCount));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(ownerContext));
Assert.True(firstAcquired.Wait(TimeSpan.FromSeconds(5)));
Assert.Equal(1, Volatile.Read(ref acquisitionCount));
releaseFirst.Set();
Assert.True(secondAcquired.Wait(TimeSpan.FromSeconds(5)));
var results = await Task.WhenAll(firstWaiter, secondWaiter).WaitAsync(TimeSpan.FromSeconds(5));
Assert.All(results, result => Assert.Equal((0, 0), result));
Assert.Equal(2, Volatile.Read(ref acquisitionCount));
}
[Fact]
public async Task ContendedMutex_PreservesMutualExclusionUnderLoad()
{
const ulong memoryBase = 0x3_0000_0000;
const ulong mutexAddress = memoryBase + 0x100;
const int workerCount = 4;
const int iterationsPerWorker = 250;
var memory = new AllocatingCpuMemory(memoryBase, 0x4000);
var initializationContext = new CpuContext(memory, Generation.Gen5);
Assert.True(initializationContext.TryWriteUInt64(mutexAddress, 1));
initializationContext[CpuRegister.Rdi] = mutexAddress;
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(initializationContext));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(initializationContext));
using var start = new ManualResetEventSlim(false);
var insideCriticalSection = 0;
var mutualExclusionViolations = 0;
var protectedCounter = 0;
var workers = Enumerable.Range(0, workerCount)
.Select(_ => Task.Factory.StartNew(
() =>
{
var context = new CpuContext(memory, Generation.Gen5);
context[CpuRegister.Rdi] = mutexAddress;
start.Wait();
for (var iteration = 0; iteration < iterationsPerWorker; iteration++)
{
if (KernelPthreadCompatExports.PthreadMutexLock(context) != 0)
{
throw new InvalidOperationException("pthread mutex lock failed during contention stress.");
}
if (Interlocked.Increment(ref insideCriticalSection) != 1)
{
Interlocked.Increment(ref mutualExclusionViolations);
}
protectedCounter++;
Thread.SpinWait(20);
Interlocked.Decrement(ref insideCriticalSection);
if (KernelPthreadCompatExports.PthreadMutexUnlock(context) != 0)
{
throw new InvalidOperationException("pthread mutex unlock failed during contention stress.");
}
}
},
CancellationToken.None,
TaskCreationOptions.LongRunning,
TaskScheduler.Default))
.ToArray();
start.Set();
await Task.WhenAll(workers).WaitAsync(TimeSpan.FromSeconds(10));
Assert.Equal(0, Volatile.Read(ref mutualExclusionViolations));
Assert.Equal(workerCount * iterationsPerWorker, protectedCounter);
}
private sealed class AllocatingCpuMemory : ICpuMemory, IGuestMemoryAllocator
@@ -1,144 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.Libs.Kernel;
using Xunit;
namespace SharpEmu.Libs.Tests.Pthread;
public sealed class PthreadSemaphoreSemanticsTests
{
private const ulong MemoryBase = 0x1_0000_0000;
private const ulong SemaphoreAddress = MemoryBase + 0x100;
private const ulong ValueAddress = MemoryBase + 0x200;
[Theory]
[InlineData("C36iRE0F5sE", "scePthreadSemWait")]
[InlineData("aishVAiFaYM", "scePthreadSemPost")]
[InlineData("H2a+IN9TP0E", "scePthreadSemTrywait")]
[InlineData("GEnUkDZoUwY", "scePthreadSemInit")]
[InlineData("Vwc+L05e6oE", "scePthreadSemDestroy")]
public void RegistryResolvesPthreadSemaphoreExports(string nid, string exportName)
{
var manager = new ModuleManager();
manager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(
Generation.Gen4 | Generation.Gen5));
Assert.True(manager.TryGetExport(nid, out var export));
Assert.Equal(exportName, export.Name);
Assert.Equal("libKernel", export.LibraryName);
}
[Fact]
public void PthreadSemPostAndWaitShareSemaphoreState()
{
var context = CreateContext();
InitializeSemaphore(context, initialCount: 0);
context[CpuRegister.Rdi] = SemaphoreAddress;
Assert.Equal(0, KernelSemaphoreCompatExports.PthreadSemPost(context));
Assert.Equal(0UL, context[CpuRegister.Rax]);
Assert.Equal(1, ReadSemaphoreValue(context));
context[CpuRegister.Rdi] = SemaphoreAddress;
Assert.Equal(0, KernelSemaphoreCompatExports.PthreadSemWait(context));
Assert.Equal(0UL, context[CpuRegister.Rax]);
Assert.Equal(0, ReadSemaphoreValue(context));
DestroySemaphore(context);
}
[Fact]
public void PthreadSemTryWaitConsumesAvailableTokenAndReturnsTryAgainWhenEmpty()
{
var context = CreateContext();
InitializeSemaphore(context, initialCount: 1);
context[CpuRegister.Rdi] = SemaphoreAddress;
Assert.Equal(0, KernelSemaphoreCompatExports.PthreadSemTryWait(context));
Assert.Equal(0, ReadSemaphoreValue(context));
context[CpuRegister.Rdi] = SemaphoreAddress;
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN,
KernelSemaphoreCompatExports.PthreadSemTryWait(context));
Assert.Equal(unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN),
context[CpuRegister.Rax]);
DestroySemaphore(context);
}
[Fact]
public void PthreadSemInitRejectsNonPrivateFlag()
{
var context = CreateContext();
context[CpuRegister.Rdi] = SemaphoreAddress;
context[CpuRegister.Rsi] = 1;
context[CpuRegister.Rdx] = 0;
context[CpuRegister.Rcx] = 0;
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT,
KernelSemaphoreCompatExports.PthreadSemInit(context));
Assert.Equal(unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT),
context[CpuRegister.Rax]);
}
[Fact]
public void PthreadSemDestroyClearsSemaphoreAndRejectsSecondDestroy()
{
var context = CreateContext();
InitializeSemaphore(context, initialCount: 0);
context[CpuRegister.Rdi] = SemaphoreAddress;
Assert.Equal(0, KernelSemaphoreCompatExports.PthreadSemDestroy(context));
Assert.True(context.TryReadUInt32(SemaphoreAddress, out var handle));
Assert.Equal(0U, handle);
context[CpuRegister.Rdi] = SemaphoreAddress;
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT,
KernelSemaphoreCompatExports.PthreadSemDestroy(context));
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void PthreadSemOperationRejectsInvalidSemaphore(bool post)
{
var context = CreateContext();
context[CpuRegister.Rdi] = SemaphoreAddress;
var result = post
? KernelSemaphoreCompatExports.PthreadSemPost(context)
: KernelSemaphoreCompatExports.PthreadSemWait(context);
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT, result);
Assert.Equal(unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT),
context[CpuRegister.Rax]);
}
private static CpuContext CreateContext() => new(new FakeCpuMemory(MemoryBase, 0x1000), Generation.Gen5);
private static void InitializeSemaphore(CpuContext context, uint initialCount)
{
context[CpuRegister.Rdi] = SemaphoreAddress;
context[CpuRegister.Rsi] = 0;
context[CpuRegister.Rdx] = initialCount;
context[CpuRegister.Rcx] = 0;
Assert.Equal(0, KernelSemaphoreCompatExports.PthreadSemInit(context));
}
private static int ReadSemaphoreValue(CpuContext context)
{
context[CpuRegister.Rdi] = SemaphoreAddress;
context[CpuRegister.Rsi] = ValueAddress;
Assert.Equal(0, KernelSemaphoreCompatExports.PosixSemGetValue(context));
Assert.True(context.TryReadUInt32(ValueAddress, out var value));
return unchecked((int)value);
}
private static void DestroySemaphore(CpuContext context)
{
context[CpuRegister.Rdi] = SemaphoreAddress;
Assert.Equal(0, KernelSemaphoreCompatExports.PthreadSemDestroy(context));
}
}
@@ -31,9 +31,6 @@ public sealed class SaveDataExportsTests : IDisposable
private const ulong SyncParam = Base + 0xC80;
private const ulong SetupParam = Base + 0xD00;
private const ulong SetupResult = Base + 0xD80;
private const ulong TransactionOut = Base + 0xE00;
private const ulong StaleR8 = Base + 0xE08;
private const ulong StaleR9 = Base + 0xE10;
private const int NoEvent = unchecked((int)0x809F0008);
private const int ParameterError = unchecked((int)0x809F0000);
@@ -228,64 +225,4 @@ public sealed class SaveDataExportsTests : IDisposable
unchecked((int)0x809F0008),
SaveDataExports.SaveDataDelete(Reg(rdi: DeleteParam)));
}
[Fact]
public void CreateTransactionResource_WithoutOutPointer_DoesNotProbeStaleRegisters()
{
const uint sentinel = 0xA5A5A5A5;
Assert.True(_ctx.TryWriteUInt32(TransactionOut, sentinel));
Assert.True(_ctx.TryWriteUInt32(StaleR8, sentinel));
Assert.True(_ctx.TryWriteUInt32(StaleR9, sentinel));
var ctx = Reg(rdi: UserId, rdx: 0, rcx: TransactionOut);
ctx[CpuRegister.R8] = StaleR8;
ctx[CpuRegister.R9] = StaleR9;
Assert.Equal(0, SaveDataExports.SaveDataCreateTransactionResource(ctx));
Assert.True(_ctx.TryReadUInt32(TransactionOut, out var rcxValue));
Assert.True(_ctx.TryReadUInt32(StaleR8, out var r8Value));
Assert.True(_ctx.TryReadUInt32(StaleR9, out var r9Value));
Assert.Equal(sentinel, rcxValue);
Assert.Equal(sentinel, r8Value);
Assert.Equal(sentinel, r9Value);
}
[Fact]
public void CreateTransactionResource_WithOutPointerFlag_WritesOnlyRcx()
{
const uint sentinel = 0xA5A5A5A5;
Assert.True(_ctx.TryWriteUInt32(TransactionOut, 0));
Assert.True(_ctx.TryWriteUInt32(StaleR8, sentinel));
Assert.True(_ctx.TryWriteUInt32(StaleR9, sentinel));
var ctx = Reg(rdi: UserId, rdx: 1, rcx: TransactionOut);
ctx[CpuRegister.R8] = StaleR8;
ctx[CpuRegister.R9] = StaleR9;
Assert.Equal(0, SaveDataExports.SaveDataCreateTransactionResource(ctx));
Assert.True(_ctx.TryReadUInt32(TransactionOut, out var resource));
Assert.True(_ctx.TryReadUInt32(StaleR8, out var r8Value));
Assert.True(_ctx.TryReadUInt32(StaleR9, out var r9Value));
Assert.NotEqual(0u, resource);
Assert.Equal(sentinel, r8Value);
Assert.Equal(sentinel, r9Value);
}
[Fact]
public void CreateTransactionResource_WithLegacyOutPointer_WritesOnlyRdx()
{
const uint sentinel = 0xA5A5A5A5;
Assert.True(_ctx.TryWriteUInt32(TransactionOut, 0));
Assert.True(_ctx.TryWriteUInt32(StaleR8, sentinel));
Assert.Equal(
0,
SaveDataExports.SaveDataCreateTransactionResource(
Reg(rdi: UserId, rdx: TransactionOut, rcx: StaleR8)));
Assert.True(_ctx.TryReadUInt32(TransactionOut, out var resource));
Assert.True(_ctx.TryReadUInt32(StaleR8, out var rcxValue));
Assert.NotEqual(0u, resource);
Assert.Equal(sentinel, rcxValue);
}
}
@@ -14,7 +14,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ProjectReference Include="..\..\src\SharpEmu.Core\SharpEmu.Core.csproj" />
<ProjectReference Include="..\..\src\SharpEmu.Libs\SharpEmu.Libs.csproj" />
<ProjectReference Include="..\..\src\SharpEmu.HLE\SharpEmu.HLE.csproj" />
<ProjectReference Include="..\..\src\SharpEmu.GUI\SharpEmu.GUI.csproj" />
</ItemGroup>
<ItemGroup>
@@ -1,59 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Libs.VideoOut;
using Silk.NET.Vulkan;
using Xunit;
namespace SharpEmu.Libs.Tests.VideoOut;
public sealed class VulkanPresentEncodeFormatTests
{
[Theory]
[InlineData(Format.B8G8R8A8Unorm, Format.B8G8R8A8Srgb)]
[InlineData(Format.R8G8B8A8Unorm, Format.R8G8B8A8Srgb)]
public void UnormSwapchainFormatsHaveSrgbCounterparts(
Format swapchainFormat,
Format expected)
{
Assert.Equal(
expected,
VulkanVideoPresenter.GetSrgbCounterpart(swapchainFormat));
}
[Theory]
// Already-sRGB swapchains encode on store; the direct blit stays.
[InlineData(Format.B8G8R8A8Srgb)]
[InlineData(Format.R8G8B8A8Srgb)]
// No same-class sRGB counterpart exists; the raw blit must remain.
[InlineData(Format.A2B10G10R10UnormPack32)]
[InlineData(Format.R16G16B16A16Sfloat)]
[InlineData(Format.R5G6B5UnormPack16)]
[InlineData(Format.Undefined)]
public void OtherSwapchainFormatsKeepTheDirectBlit(Format swapchainFormat)
{
Assert.Equal(
Format.Undefined,
VulkanVideoPresenter.GetSrgbCounterpart(swapchainFormat));
}
[Theory]
[InlineData(Format.R16G16B16A16Sfloat)]
[InlineData(Format.R32G32B32A32Sfloat)]
public void FloatFlipSourcesNeedLinearToSrgbEncode(Format sourceFormat)
{
Assert.True(VulkanVideoPresenter.IsLinearFloatPresentSource(sourceFormat));
}
[Theory]
[InlineData(Format.B8G8R8A8Unorm)]
[InlineData(Format.R8G8B8A8Unorm)]
[InlineData(Format.B8G8R8A8Srgb)]
[InlineData(Format.A2B10G10R10UnormPack32)]
[InlineData(Format.B10G11R11UfloatPack32)]
[InlineData(Format.Undefined)]
public void NonFloatFlipSourcesKeepTheDirectBlit(Format sourceFormat)
{
Assert.False(VulkanVideoPresenter.IsLinearFloatPresentSource(sourceFormat));
}
}
@@ -12,13 +12,6 @@
// [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)
// [5] v_pk_fma_f16 fma(2.5h, 21024h, 7.496e-5h) -> 0x7A6B packed; the exact
// sum sits just above an f16 midpoint, so a double-rounded f32
// multiply-add would give 0x7A6A instead
// [6] the same fma with the addend negated -> 0x7A6A packed (just below the
// same midpoint), pinning the opposite rounding direction
// [7] the pinned fma with the clamp modifier -> 0x3C00 packed (both lanes
// exceed 1.0, so each saturates to 1.0)
// Every other word of the buffer must still hold the sentinel afterwards.
//
// Creating the compute pipeline doubles as a driver-acceptance check for the
@@ -42,15 +35,6 @@ var expectedHi = (uint)(product >> 32);
var expectedLo = (uint)product;
var expectedRestored = BitConverter.SingleToUInt32Bits(1.5f);
// v_pk_fma_f16 of (0x4100, 0x7522, 0x04EA) per lane: the exact product
// 2.5 * 21024 = 52560 is an f16 tie (between 0x7A6A and 0x7A6B), so the tiny
// addend decides the rounding direction under a single fused rounding.
const uint ExpectedPkFma = 0x7A6B_7A6B;
const uint ExpectedPkFmaNeg = 0x7A6A_7A6A;
// Both lanes of the pinned fma are ~52560, far above 1.0, so the clamp modifier
// saturates each to 1.0 (0x3C00 in f16).
const uint ExpectedPkFmaClamp = 0x3C00_3C00;
unsafe
{
var spvPath = args.Length > 0
@@ -368,9 +352,6 @@ unsafe
("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),
("v_pk_fma_f16 fused rounds up at midpoint", words[5], ExpectedPkFma),
("v_pk_fma_f16 neg addend rounds down", words[6], ExpectedPkFmaNeg),
("v_pk_fma_f16 clamp saturates to 1.0", words[7], ExpectedPkFmaClamp),
};
var failures = 0;
foreach (var (name, actual, expected) in results)
+1 -32
View File
@@ -42,25 +42,6 @@ const ulong ProgramAddress = 0x100000;
0xD56C0005, 0x00020501, // v_mul_hi_i32 v5, v1, v2
0xBF810000, // s_endpgm
]),
// Packed f16 (VOP3P) arithmetic, including the fused multiply-add. The
// constants pin the double-rounding regression from the VOP3P first slice:
// fma(0x4100, 0x7522, 0x04EA) must round once to 0x7A6B (an f32
// multiply-add then pack yields 0x7A6A). The last fma exercises the src2
// neg_lo/neg_hi modifier path.
("pk-f16", true, [
0x7E0002FF, 0x41004100, // v_mov_b32 v0, 0x41004100 (2.5 packed)
0x7E0202FF, 0x75227522, // v_mov_b32 v1, 0x75227522 (21024 packed)
0x7E0402FF, 0x04EA04EA, // v_mov_b32 v2, 0x04EA04EA (~7.496e-5 packed)
0xCC0E4003, 0x1C0A0300, // v_pk_fma_f16 v3, v0, v1, v2
0xCC0F4004, 0x18020500, // v_pk_add_f16 v4, v0, v2
0xCC104005, 0x18020300, // v_pk_mul_f16 v5, v0, v1
0xCC114006, 0x18020300, // v_pk_min_f16 v6, v0, v1
0xCC124007, 0x18020300, // v_pk_max_f16 v7, v0, v1
0xCC0E4408, 0x9C0A0300, // v_pk_fma_f16 v8, v0, v1, neg_lo:[0,0,1] neg_hi:[0,0,1] v2
0xCC0FC009, 0x18020000, // v_pk_add_f16 v9, v0, v0 clamp (2.5+2.5=5 -> saturates to 1.0)
0xCC0EC40A, 0x1C0A0300, // v_pk_fma_f16 v10, v0, v1, v2 clamp
0xBF810000, // s_endpgm
]),
("mrt", true, [
0x7E0002FF, 0x3F800000, // v_mov_b32 v0, 1.0f
0x7E0202FF, 0x00000000, // v_mov_b32 v1, 0.0f
@@ -134,10 +115,7 @@ const ulong ProgramAddress = 0x100000;
// Executable end-to-end test: compute with real ALU instructions, then
// buffer_store_dword results to guestBuffers[0] at offsets 0/4/8, prove
// that a store with EXEC=0 does not land (offset 12 stays sentinel), and
// that stores work again after EXEC is restored (offset 16). Offsets 20/24
// hold the packed fused f16 FMA and its negated-addend twin, whose exact
// results (0x7A6B7A6B / 0x7A6A7A6A) straddle an f16 midpoint and therefore
// catch any double-rounding regression on real hardware.
// that stores work again after EXEC is restored (offset 16).
("exec", true, [
0xBFA10001, // s_clause 0x1 (hint no-op in an executed program, needs #108)
0x7E0002FF, 0x3FC00000, // v_mov_b32 v0, 1.5f
@@ -155,15 +133,6 @@ const ulong ProgramAddress = 0x100000;
0xE070000C, 0x80020200, // buffer_store_dword v2, off, s[8:11], 0 offset:12 (masked, must not land)
0xBEFE03C1, // s_mov_b32 exec_lo, -1 -> lane active again
0xE0700010, 0x80020000, // buffer_store_dword v0, off, s[8:11], 0 offset:16
0x7E0E02FF, 0x41004100, // v_mov_b32 v7, 0x41004100 (2.5 packed)
0x7E1002FF, 0x75227522, // v_mov_b32 v8, 0x75227522 (21024 packed)
0x7E1202FF, 0x04EA04EA, // v_mov_b32 v9, 0x04EA04EA (~7.496e-5 packed)
0xCC0E400A, 0x1C261107, // v_pk_fma_f16 v10, v7, v8, v9
0xCC0E440B, 0x9C261107, // v_pk_fma_f16 v11, v7, v8, neg_lo:[0,0,1] neg_hi:[0,0,1] v9
0xCC0EC00C, 0x1C261107, // v_pk_fma_f16 v12, v7, v8, v9 clamp (>=1 -> saturates to 1.0)
0xE0700014, 0x80020A00, // buffer_store_dword v10, off, s[8:11], 0 offset:20
0xE0700018, 0x80020B00, // buffer_store_dword v11, off, s[8:11], 0 offset:24
0xE070001C, 0x80020C00, // buffer_store_dword v12, off, s[8:11], 0 offset:28
0xBF810000, // s_endpgm
]),
];