Compare commits

..

1 Commits

Author SHA1 Message Date
ParantezTech f69f81f682 [HLE] Add RandomExports HLE 2026-07-18 23:33:45 +03:00
48 changed files with 3060 additions and 2722 deletions
-1
View File
@@ -42,4 +42,3 @@ ehthumbs.db
.vs/
.idea/
packages.lock.json
+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
+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())
+5
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);
+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)' == ''">
+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,81 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Numerics;
namespace SharpEmu.Core.Cpu.Emulation;
/// <summary>
/// The four Intel SHA-extension operations used by SHA-1 code. This is the arithmetic half of
/// the direct-execution SIGILL fallback used when an x86-64 guest runs under Rosetta 2, which
/// does not expose Intel SHA instructions to translated processes.
/// </summary>
public static class Sha1InstructionEmulator
{
public static Sha1Vector MessageSchedule1(Sha1Vector destination, Sha1Vector source) => new(
destination.Lane0 ^ source.Lane2,
destination.Lane1 ^ source.Lane3,
destination.Lane2 ^ destination.Lane0,
destination.Lane3 ^ destination.Lane1);
public static Sha1Vector MessageSchedule2(Sha1Vector destination, Sha1Vector source)
{
var lane3 = BitOperations.RotateLeft(destination.Lane3 ^ source.Lane2, 1);
return new Sha1Vector(
BitOperations.RotateLeft(destination.Lane0 ^ lane3, 1),
BitOperations.RotateLeft(destination.Lane1 ^ source.Lane0, 1),
BitOperations.RotateLeft(destination.Lane2 ^ source.Lane1, 1),
lane3);
}
public static Sha1Vector NextE(Sha1Vector destination, Sha1Vector source) => new(
source.Lane0,
source.Lane1,
source.Lane2,
unchecked(source.Lane3 + BitOperations.RotateLeft(destination.Lane3, 30)));
public static Sha1Vector FourRounds(Sha1Vector destination, Sha1Vector source, byte function)
{
uint a = destination.Lane3;
uint b = destination.Lane2;
uint c = destination.Lane1;
uint d = destination.Lane0;
uint e = 0;
uint constant = (function & 3) switch
{
0 => 0x5A82_7999u,
1 => 0x6ED9_EBA1u,
2 => 0x8F1B_BCDCu,
_ => 0xCA62_C1D6u,
};
for (var round = 0; round < 4; round++)
{
uint choose = (function & 3) switch
{
0 => (b & c) ^ (~b & d),
2 => (b & c) ^ (b & d) ^ (c & d),
_ => b ^ c ^ d,
};
uint word = round switch
{
0 => source.Lane3,
1 => source.Lane2,
2 => source.Lane1,
_ => source.Lane0,
};
uint next = unchecked(choose + BitOperations.RotateLeft(a, 5) + word + e + constant);
e = d;
d = c;
c = BitOperations.RotateLeft(b, 30);
b = a;
a = next;
}
return new Sha1Vector(d, c, b, a);
}
}
/// <summary>The four little-endian 32-bit lanes of an XMM register.</summary>
public readonly record struct Sha1Vector(uint Lane0, uint Lane1, uint Lane2, uint Lane3);
@@ -1113,12 +1113,7 @@ public sealed partial class DirectExecutionBackend
}
}
private unsafe static bool TryReadHostBytes(ulong address, byte[] buffer) =>
TryReadHostBytes(address, buffer.AsSpan());
// Span overload so signal-handler recovery paths can read into a stackalloc
// buffer instead of allocating managed arrays inside the handler.
private unsafe static bool TryReadHostBytes(ulong address, Span<byte> buffer)
private unsafe static bool TryReadHostBytes(ulong address, byte[] buffer)
{
if (address < 65536)
{
@@ -1142,7 +1137,7 @@ public sealed partial class DirectExecutionBackend
try
{
new ReadOnlySpan<byte>((void*)address, buffer.Length).CopyTo(buffer);
Marshal.Copy((nint)address, buffer, 0, buffer.Length);
return true;
}
catch
@@ -8,21 +8,21 @@ using SharpEmu.Core.Cpu.Emulation;
namespace SharpEmu.Core.Cpu.Native;
// Software fallback for unsupported guest CPU instructions.
// Software fallback for the BMI1/BMI2/ABM general-purpose-register instructions.
//
// Guest code runs natively, so the guest and host share the same virtual address space and the
// same registers (the OS delivers them in the CONTEXT record on a fault). When the host CPU lacks
// one of these extensions it raises #UD instead of executing the opcode; without this the title
// simply aborts. Here we decode the faulting instruction, evaluate it against the trapped register
// and memory state, write the result back into the CONTEXT, step RIP past the instruction and ask
// the OS to continue. BMI1/BMI2/ABM GPR operations and the Intel SHA-1 XMM operations are handled;
// anything else falls through to the existing diagnostics unchanged.
// the OS to continue. Only the register-only BMI/ABM forms are handled; anything else returns false
// and falls through to the existing diagnostics unchanged, so this can never mis-handle an opcode it
// does not fully model.
public sealed partial class DirectExecutionBackend
{
// Windows x64 CONTEXT.EFlags lives just past the segment selectors. The GPR offsets it shares
// with the rest of the backend are the CTX_* constants declared in DirectExecutionBackend.cs.
private const int CTX_EFLAGS = 68;
private const int CTX_XMM0 = 0x1A0;
// STATUS_ILLEGAL_INSTRUCTION (#UD surfaced by the Windows vectored handler).
private const uint StatusIllegalInstruction = 0xC000001Du;
@@ -34,8 +34,6 @@ public sealed partial class DirectExecutionBackend
private static int _bmiSoftwareFallbackAnnounced;
private static long _bmiInstructionsEmulated;
private static int _sha1SoftwareFallbackAnnounced;
private static long _sha1InstructionsEmulated;
private unsafe bool TryRecoverIllegalInstruction(void* contextRecord, ulong rip)
{
@@ -44,11 +42,6 @@ public sealed partial class DirectExecutionBackend
return false;
}
if (TryRecoverSha1Instruction(contextRecord, rip, in instruction))
{
return true;
}
if (instruction.Op0Kind != OpKind.Register ||
!TryGetGprSlot(instruction.Op0Register, out var destOffset, out var size))
{
@@ -79,139 +72,6 @@ public sealed partial class DirectExecutionBackend
return true;
}
private unsafe bool TryRecoverSha1Instruction(
void* contextRecord,
ulong rip,
in Instruction instruction)
{
if ((!OperatingSystem.IsWindows() && !_posixVectorContextAvailable) ||
instruction.Mnemonic is not (Mnemonic.Sha1msg1 or Mnemonic.Sha1msg2 or
Mnemonic.Sha1nexte or Mnemonic.Sha1rnds4) ||
instruction.Op0Kind != OpKind.Register ||
!TryGetXmmIndex(instruction.Op0Register, out var destinationIndex) ||
!TryReadSha1VectorOperand(contextRecord, in instruction, 1, out var source))
{
return false;
}
var destination = ReadXmm(contextRecord, destinationIndex);
Sha1Vector result;
switch (instruction.Mnemonic)
{
case Mnemonic.Sha1msg1:
result = Sha1InstructionEmulator.MessageSchedule1(destination, source);
break;
case Mnemonic.Sha1msg2:
result = Sha1InstructionEmulator.MessageSchedule2(destination, source);
break;
case Mnemonic.Sha1nexte:
result = Sha1InstructionEmulator.NextE(destination, source);
break;
case Mnemonic.Sha1rnds4:
if (instruction.Op2Kind != OpKind.Immediate8)
{
return false;
}
result = Sha1InstructionEmulator.FourRounds(destination, source, instruction.Immediate8);
break;
default:
return false;
}
WriteXmm(contextRecord, destinationIndex, result);
WriteCtxU64(contextRecord, CTX_RIP, rip + (ulong)instruction.Length);
if (!_posixSignalWarmup)
{
Interlocked.Increment(ref _sha1InstructionsEmulated);
if (Interlocked.Exchange(ref _sha1SoftwareFallbackAnnounced, 1) == 0)
{
Console.Error.WriteLine(
"[LOADER][INFO] Host lacks Intel SHA instructions used by the guest; " +
"emulating SHA-1 instructions in software.");
}
}
return true;
}
private unsafe bool TryReadSha1VectorOperand(
void* contextRecord,
in Instruction instruction,
int operandIndex,
out Sha1Vector value)
{
switch (instruction.GetOpKind(operandIndex))
{
case OpKind.Register:
if (TryGetXmmIndex(instruction.GetOpRegister(operandIndex), out var sourceIndex))
{
value = ReadXmm(contextRecord, sourceIndex);
return true;
}
break;
case OpKind.Memory:
if (TryComputeMemoryAddress(contextRecord, in instruction, out var address))
{
Span<byte> bytes = stackalloc byte[16];
if (TryReadHostBytes(address, bytes))
{
value = new Sha1Vector(
BinaryPrimitives.ReadUInt32LittleEndian(bytes.Slice(0, 4)),
BinaryPrimitives.ReadUInt32LittleEndian(bytes.Slice(4, 4)),
BinaryPrimitives.ReadUInt32LittleEndian(bytes.Slice(8, 4)),
BinaryPrimitives.ReadUInt32LittleEndian(bytes.Slice(12, 4)));
return true;
}
}
break;
}
value = default;
return false;
}
private static unsafe Sha1Vector ReadXmm(void* contextRecord, int index)
{
uint* lanes = (uint*)((byte*)contextRecord + CTX_XMM0 + index * 16);
return new Sha1Vector(lanes[0], lanes[1], lanes[2], lanes[3]);
}
private static unsafe void WriteXmm(void* contextRecord, int index, Sha1Vector value)
{
uint* lanes = (uint*)((byte*)contextRecord + CTX_XMM0 + index * 16);
lanes[0] = value.Lane0;
lanes[1] = value.Lane1;
lanes[2] = value.Lane2;
lanes[3] = value.Lane3;
}
private static bool TryGetXmmIndex(Register register, out int index)
{
switch (register)
{
case Register.XMM0: index = 0; return true;
case Register.XMM1: index = 1; return true;
case Register.XMM2: index = 2; return true;
case Register.XMM3: index = 3; return true;
case Register.XMM4: index = 4; return true;
case Register.XMM5: index = 5; return true;
case Register.XMM6: index = 6; return true;
case Register.XMM7: index = 7; return true;
case Register.XMM8: index = 8; return true;
case Register.XMM9: index = 9; return true;
case Register.XMM10: index = 10; return true;
case Register.XMM11: index = 11; return true;
case Register.XMM12: index = 12; return true;
case Register.XMM13: index = 13; return true;
case Register.XMM14: index = 14; return true;
case Register.XMM15: index = 15; return true;
default: index = 0; return false;
}
}
private unsafe bool TryEvaluate(
void* contextRecord,
in Instruction instruction,
@@ -334,37 +194,19 @@ public sealed partial class DirectExecutionBackend
}
}
// A managed allocation inside a signal handler is unsafe: the fault can
// interrupt the GC mid-operation, and re-entering the allocator corrupts
// the thread's allocation context (observed as a hard "Invalid Program" at
// the next managed alloc, amplified by tight SHA-1 recovery loops). These
// thread-static objects are created once per thread and reused so the
// recovery path never allocates on the hot path.
[ThreadStatic]
private static byte[]? _decodeBuffer;
[ThreadStatic]
private static ByteArrayCodeReader? _decodeReader;
[ThreadStatic]
private static Decoder? _decoder;
private unsafe bool TryReadFaultingInstruction(ulong rip, out Instruction instruction)
{
var buffer = _decodeBuffer ??= new byte[MaxInstructionBytes];
var reader = _decodeReader ??= new ByteArrayCodeReader(buffer);
var decoder = _decoder ??= Decoder.Create(64, reader);
// Try the full instruction window first, then shrink so a fault near the end of a mapped
// page (where fewer than 15 bytes are readable) still decodes. The buffer is reused, so
// clear the tail past the readable window to keep decoding deterministic.
// page (where fewer than 15 bytes are readable) still decodes.
foreach (var attempt in DecodeWindowSizes)
{
if (!TryReadHostBytes(rip, buffer.AsSpan(0, attempt)))
var buffer = new byte[attempt];
if (!TryReadHostBytes(rip, buffer))
{
continue;
}
buffer.AsSpan(attempt).Clear();
reader.Position = 0;
var decoder = Decoder.Create(64, new ByteArrayCodeReader(buffer));
decoder.IP = rip;
decoder.Decode(out instruction);
if (instruction.Code != Code.INVALID && instruction.Length > 0 && instruction.Length <= attempt)
@@ -404,8 +246,8 @@ public sealed partial class DirectExecutionBackend
}
var byteCount = size == GprOperandSize.Bits64 ? 8 : 4;
Span<byte> buffer = stackalloc byte[8];
if (!TryReadHostBytes(address, buffer[..byteCount]))
var buffer = new byte[byteCount];
if (!TryReadHostBytes(address, buffer))
{
return false;
}
@@ -630,6 +630,27 @@ public sealed partial class DirectExecutionBackend
cpuContext[CpuRegister.Rax] = 18446744071562199298uL;
}
}
if (GuestThreadExecution.TryConsumeCurrentThreadBlock(
out var blockReason,
out var blockContinuation,
out var hasBlockContinuation,
out var blockWakeKey,
out var blockWaiter,
out var blockDeadlineTimestamp) &&
TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, blockReason))
{
if (hasBlockContinuation)
{
RegisterBlockedGuestThreadContinuation(
GuestThreadExecution.CurrentGuestThreadHandle,
blockContinuation,
blockWakeKey,
blockWaiter,
blockDeadlineTimestamp);
}
cpuContext[CpuRegister.Rax] = 0uL;
}
if (flag || flag2 || flag3)
{
Console.Error.WriteLine($"[LOADER][TRACE] ImportRet#{num}: nid={importStubEntry.Nid} result={orbisGen2Result} rax=0x{cpuContext[CpuRegister.Rax]:X16}");
@@ -1324,13 +1345,35 @@ public sealed partial class DirectExecutionBackend
}
}
var consumedThreadBlock = GuestThreadExecution.TryConsumeCurrentThreadBlock(
out var blockReason,
out var blockContinuation,
out var hasBlockContinuation,
out var blockWakeKey,
out var blockWaiter,
out var blockDeadlineTimestamp);
if (consumedThreadBlock &&
TryYieldGuestThreadToHostStub(argPackPtr, dispatchIndex, returnRip, importStubEntry.Nid, blockReason))
{
if (hasBlockContinuation)
{
RegisterBlockedGuestThreadContinuation(
GuestThreadExecution.CurrentGuestThreadHandle,
blockContinuation,
blockWakeKey,
blockWaiter,
blockDeadlineTimestamp);
}
cpuContext[CpuRegister.Rax] = 0uL;
}
if (probeLeafReturn)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] leaf-return-probe-exit nid={importStubEntry.Nid} " +
$"original=0x{returnRip:X16} final=0x{*(ulong*)(argPackPtr + 96):X16} " +
$"rsp=0x{leafStackPointer:X16} active_slot=0x{ActiveGuestReturnSlotAddress:X16} " +
$"yield={ActiveGuestThreadYieldRequested}");
$"block={consumedThreadBlock} yield={ActiveGuestThreadYieldRequested}");
}
result = cpuContext[CpuRegister.Rax];
@@ -1367,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
@@ -1395,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);
@@ -1411,7 +1449,6 @@ public sealed partial class DirectExecutionBackend
!expectedTimedWaitTimeout &&
!expectedEqueueTimeout &&
!expectedMutexTrylockBusy &&
!expectedSemaphoreTrywaitAgain &&
!expectedNetAcceptWouldBlock &&
!expectedUserServiceNoEvent &&
!expectedPrivacyInvalidParameter)
@@ -1534,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
@@ -50,15 +50,6 @@ public sealed unsafe partial class DirectExecutionBackend
private const int LinuxUcontextGregsOffset = 40;
private const int LinuxGregsErrOffset = 19 * 8;
// XMM0 starts after the exception/thread states and the floating-point header on Darwin.
// Linux mcontext_t instead stores a pointer to an FXSAVE-compatible _libc_fpstate after
// gregs[23], where XMM0 begins at byte 160.
private const int DarwinMcontextFloatStateOffset = 16 + 21 * 8;
private const int DarwinFloatStateXmmOffset = 168;
private const int LinuxFpregsPointerOffset = 23 * 8;
private const int LinuxFxsaveXmmOffset = 160;
private const int XmmRegisterBlockSize = 16 * 16;
// Byte offsets of the general registers relative to GetPosixRegisterBase,
// ordered to match the contiguous Win64 CONTEXT block CTX_RAX..CTX_RIP
// (rax, rcx, rdx, rbx, rsp, rbp, rsi, rdi, r8..r15, rip). Verified
@@ -79,8 +70,6 @@ public sealed unsafe partial class DirectExecutionBackend
[ThreadStatic]
private static int _posixSignalHandlerDepth;
[ThreadStatic]
private static bool _posixVectorContextAvailable;
private void SetupPosixExceptionHandler()
{
@@ -132,10 +121,8 @@ public sealed unsafe partial class DirectExecutionBackend
{
byte* fakeUcontext = stackalloc byte[512];
new Span<byte>(fakeUcontext, 512).Clear();
// Large enough for the Darwin mcontext64 XMM block the handler now
// round-trips (exception+thread state + float state = 708 bytes).
byte* fakeMcontext = stackalloc byte[768];
new Span<byte>(fakeMcontext, 768).Clear();
byte* fakeMcontext = stackalloc byte[512];
new Span<byte>(fakeMcontext, 512).Clear();
if (OperatingSystem.IsMacOS())
{
*(byte**)(fakeUcontext + DarwinUcontextMcontextOffset) = fakeMcontext;
@@ -167,33 +154,6 @@ public sealed unsafe partial class DirectExecutionBackend
record.ExceptionInformation[1] = 0x70000;
_ = TryHandleLazyCommittedPage(&record, 0, 0);
ChainPreviousPosixAction(0, 0, 0);
// Rosetta can deliver the first unsupported SHA instruction before the
// runtime is in a safe state to JIT the recovery path. Decode and execute
// each supported form once now, while signal handlers are not installed.
byte[][] shaWarmupOpcodes =
{
new byte[] { 0x0F, 0x38, 0xC9, 0xC1 }, // sha1msg1 xmm0, xmm1
new byte[] { 0x0F, 0x38, 0xCA, 0xC1 }, // sha1msg2 xmm0, xmm1
new byte[] { 0x0F, 0x38, 0xC8, 0xC1 }, // sha1nexte xmm0, xmm1
new byte[] { 0x0F, 0x3A, 0xCC, 0xC1, 0x00 }, // sha1rnds4 xmm0, xmm1, 0
};
_posixVectorContextAvailable = true;
try
{
foreach (byte[] opcode in shaWarmupOpcodes)
{
fixed (byte* instructionBytes = opcode)
{
WriteCtxU64(contextRecord, CTX_RIP, (ulong)instructionBytes);
_ = TryRecoverIllegalInstruction(contextRecord, (ulong)instructionBytes);
}
}
}
finally
{
_posixVectorContextAvailable = false;
}
}
finally
{
@@ -291,15 +251,6 @@ public sealed unsafe partial class DirectExecutionBackend
{
WriteCtxU64(contextRecord, CTX_RAX + i * 8, *(ulong*)(registers + offsets[i]));
}
// Only bridge the XMM block for SIGILL, the sole signal whose recovery
// may emulate an XMM instruction (Intel SHA). Copying 256 bytes in and
// out of the mcontext on every SIGSEGV (the hot demand-paging path) is
// pure overhead and needless surface for corruption.
byte* xmmRegisters = signal == PosixSigIll ? GetPosixXmmBase(registers) : null;
if (xmmRegisters != null)
{
CopyXmmRegisterBlock(contextRecord + CTX_XMM0, xmmRegisters);
}
EXCEPTION_RECORD record = default;
record.ExceptionAddress = (void*)ReadCtxU64(contextRecord, CTX_RIP);
@@ -343,21 +294,13 @@ public sealed unsafe partial class DirectExecutionBackend
// every fault anyway, and recovering here avoids dumping the full
// VectoredHandler diagnostics for each recoverable trap.
int disposition = 0;
_posixVectorContextAvailable = xmmRegisters != null;
try
if (_posixRawRecoveryEnabled)
{
if (_posixRawRecoveryEnabled)
{
disposition = TryRecoverUnresolvedSentinel(&pointers);
}
if (disposition != -1 && !_posixSignalWarmup && _posixSignalBackend is { } backend)
{
disposition = backend.VectoredHandler(&pointers);
}
disposition = TryRecoverUnresolvedSentinel(&pointers);
}
finally
if (disposition != -1 && !_posixSignalWarmup && _posixSignalBackend is { } backend)
{
_posixVectorContextAvailable = false;
disposition = backend.VectoredHandler(&pointers);
}
if (traceSignal)
{
@@ -374,10 +317,6 @@ public sealed unsafe partial class DirectExecutionBackend
{
*(ulong*)(registers + offsets[i]) = ReadCtxU64(contextRecord, CTX_RAX + i * 8);
}
if (xmmRegisters != null)
{
CopyXmmRegisterBlock(xmmRegisters, contextRecord + CTX_XMM0);
}
return true;
}
@@ -396,25 +335,6 @@ public sealed unsafe partial class DirectExecutionBackend
return (byte*)ucontext + LinuxUcontextGregsOffset;
}
private static byte* GetPosixXmmBase(byte* registers)
{
if (OperatingSystem.IsMacOS())
{
return registers + DarwinMcontextFloatStateOffset + DarwinFloatStateXmmOffset;
}
byte* fpregs = *(byte**)(registers + LinuxFpregsPointerOffset);
return fpregs == null ? null : fpregs + LinuxFxsaveXmmOffset;
}
private static void CopyXmmRegisterBlock(byte* destination, byte* source)
{
for (var offset = 0; offset < XmmRegisterBlockSize; offset += sizeof(ulong))
{
*(ulong*)(destination + offset) = *(ulong*)(source + offset);
}
}
private static ulong GetPosixFaultAddress(nint siginfo, byte* registers)
{
ulong address = siginfo != 0 ? *(ulong*)((byte*)siginfo + PosixSigInfoAddressOffset) : 0;
File diff suppressed because it is too large Load Diff
+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))
{
+10 -46
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();
@@ -429,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)
{
@@ -487,6 +463,11 @@ public partial class MainWindow : Window
LaunchSelected();
}
if ((pressed & HostGamepadButtons.Circle) != 0)
{
StopEmulator();
}
_previousPadButtons = pad.Buttons;
}
@@ -1645,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;
}
}
}
}
@@ -2038,7 +2009,7 @@ public partial class MainWindow : Window
ContentToolbar.IsVisible = false;
ConsolePanel.IsVisible = false;
LaunchBar.IsVisible = false;
HideSessionLoading();
SessionLoadingPopup.IsOpen = false;
UpdateSessionBarVisibility();
}
});
@@ -2138,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;
@@ -2222,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()
-116
View File
@@ -1,116 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE;
/// <summary>
/// Support for HLE synchronization primitives that block the guest thread's
/// host thread in place (inside the HLE call, on a host primitive) instead of
/// capturing a continuation and re-scheduling through the cooperative wake-key
/// machinery. In-place blocking makes block-and-wake atomic — the host
/// primitive owns the race — which removes the lost-wakeup window the
/// continuation path had between block registration and wake delivery.
/// </summary>
public static class GuestThreadBlocking
{
/// <summary>
/// Upper bound on a single host wait while a guest thread is parked. Waits
/// are sliced so parked threads observe <see cref="ShutdownRequested"/>
/// promptly at teardown; a wake via Monitor.Pulse still lands immediately.
/// </summary>
public const int WaitSliceMilliseconds = 50;
private static volatile bool _shutdownRequested;
// Guest thread handle -> what it is parked on. Populated only while a
// thread is blocked (the slow path), read by the stall watchdog so
// in-place-blocked threads are not reported as opaque "Running" threads.
private static readonly System.Collections.Concurrent.ConcurrentDictionary<ulong, string> _blockDescriptions = new();
/// <summary>True once emulator teardown has begun; parked guest threads unwind.</summary>
public static bool ShutdownRequested => _shutdownRequested;
/// <summary>Called by the execution backend when guest execution is being torn down.</summary>
public static void RequestShutdown() => _shutdownRequested = true;
/// <summary>Records what the given guest thread is about to park on (diagnostics only).</summary>
public static void NoteBlocked(ulong guestThreadHandle, string description)
{
if (guestThreadHandle != 0)
{
_blockDescriptions[guestThreadHandle] = description;
}
}
/// <summary>Clears the parked-state note recorded by <see cref="NoteBlocked"/>.</summary>
public static void NoteUnblocked(ulong guestThreadHandle)
{
if (guestThreadHandle != 0)
{
_blockDescriptions.TryRemove(guestThreadHandle, out _);
}
}
/// <summary>What the thread is parked on, or null if it is not parked in place.</summary>
public static string? DescribeBlock(ulong guestThreadHandle) =>
_blockDescriptions.TryGetValue(guestThreadHandle, out var description) ? description : null;
/// <summary>All currently parked threads (diagnostics; covers the primary thread too).</summary>
public static KeyValuePair<ulong, string>[] SnapshotBlockDescriptions() => _blockDescriptions.ToArray();
// Interrupt delivery for threads parked in place. A thread blocked inside
// an HLE wait keeps its executor busy, so it never reaches the import-return
// safe point where queued guest exceptions (IL2CPP stop-the-world suspend)
// are delivered. When an exception is queued for such a thread, its handle
// is flagged here; each sliced wait loop calls Checkpoint, which — on the
// thread's OWN host thread, with the wait's gate released — runs the
// registered deliverer (the same safe-point delivery used at import
// boundaries), then the loop re-checks its predicate and re-parks. This is
// the SA_RESTART-style "signal on top of a blocking wait" a real kernel
// provides. Dormant unless an exception is actually pending (empty-check
// fast path), so it adds no cost to normal blocking.
private static readonly System.Collections.Concurrent.ConcurrentDictionary<ulong, byte> _interrupted = new();
/// <summary>Set by the backend: delivers any exception queued for the current guest thread, in place.</summary>
public static Action? DeliverInterruptForCurrentThread { get; set; }
/// <summary>Flags a parked guest thread to deliver a queued exception at its next wait checkpoint.</summary>
public static void RequestInterrupt(ulong guestThreadHandle)
{
if (guestThreadHandle != 0)
{
_interrupted[guestThreadHandle] = 0;
}
}
/// <summary>
/// Called from every sliced wait loop while it holds <paramref name="gate"/>. If an
/// exception is pending for the current guest thread, releases the gate, delivers it on
/// this host thread, then re-acquires the gate so the loop re-checks its predicate.
/// </summary>
public static void Checkpoint(ulong guestThreadHandle, object gate)
{
if (_interrupted.IsEmpty || guestThreadHandle == 0 || !_interrupted.TryRemove(guestThreadHandle, out _))
{
return;
}
var deliver = DeliverInterruptForCurrentThread;
if (deliver is null)
{
return;
}
// Never run guest code (the handler) while holding an HLE gate — the
// handler may re-enter this same primitive. Release across delivery.
Monitor.Exit(gate);
try
{
deliver();
}
finally
{
Monitor.Enter(gate);
}
}
}
+213
View File
@@ -30,6 +30,13 @@ public readonly record struct GuestThreadSnapshot(
/// false leaves it parked. Resume runs later on the woken thread outside that gate, and
/// its return value becomes the guest's RAX for the resumed call.
/// </summary>
public interface IGuestThreadBlockWaiter
{
int Resume();
bool TryWake();
}
public interface IGuestThreadScheduler
{
bool SupportsGuestContextTransfer { get; }
@@ -50,6 +57,10 @@ public interface IGuestThreadScheduler
out ulong returnValue,
out string? error);
void Pump(CpuContext callerContext, string reason);
int WakeBlockedThreads(string wakeKey, int maxCount = int.MaxValue);
/// <summary>
/// Applies a new guest scheduling priority to a live thread, mapping it
/// onto the host thread if one is running. Returns false when the thread
@@ -141,12 +152,46 @@ public readonly record struct GuestCpuContinuation(
public static class GuestThreadExecution
{
private sealed class DelegateGuestThreadBlockWaiter : IGuestThreadBlockWaiter
{
private readonly Func<int> _resume;
private readonly Func<bool> _tryWake;
public DelegateGuestThreadBlockWaiter(Func<int> resume, Func<bool> tryWake)
{
_resume = resume;
_tryWake = tryWake;
}
public int Resume() => _resume();
public bool TryWake() => _tryWake();
}
[ThreadStatic]
private static ulong _currentGuestThreadHandle;
[ThreadStatic]
private static ulong _currentFiberAddress;
[ThreadStatic]
private static string? _pendingBlockReason;
[ThreadStatic]
private static bool _pendingBlockContinuationValid;
[ThreadStatic]
private static GuestCpuContinuation _pendingBlockContinuation;
[ThreadStatic]
private static string? _pendingBlockWakeKey;
[ThreadStatic]
private static IGuestThreadBlockWaiter? _pendingBlockWaiter;
[ThreadStatic]
private static long _pendingBlockDeadlineTimestamp;
[ThreadStatic]
private static bool _pendingEntryExit;
@@ -186,6 +231,12 @@ public static class GuestThreadExecution
{
var previous = _currentGuestThreadHandle;
_currentGuestThreadHandle = threadHandle;
_pendingBlockReason = null;
_pendingBlockContinuationValid = false;
_pendingBlockContinuation = default;
_pendingBlockWakeKey = null;
_pendingBlockWaiter = null;
_pendingBlockDeadlineTimestamp = 0;
_pendingEntryExit = false;
_pendingEntryExitValue = 0;
_pendingEntryExitReason = null;
@@ -201,6 +252,12 @@ public static class GuestThreadExecution
public static void RestoreGuestThread(ulong previousThreadHandle)
{
_currentGuestThreadHandle = previousThreadHandle;
_pendingBlockReason = null;
_pendingBlockContinuationValid = false;
_pendingBlockContinuation = default;
_pendingBlockWakeKey = null;
_pendingBlockWaiter = null;
_pendingBlockDeadlineTimestamp = 0;
_pendingEntryExit = false;
_pendingEntryExitValue = 0;
_pendingEntryExitReason = null;
@@ -224,6 +281,123 @@ public static class GuestThreadExecution
_currentFiberAddress = previousFiberAddress;
}
public static bool RequestCurrentThreadBlock(string reason) => RequestCurrentThreadBlock(null, reason);
public static bool RequestCurrentThreadBlock(
CpuContext? context,
string reason,
string? wakeKey = null,
IGuestThreadBlockWaiter? waiter = null,
long blockDeadlineTimestamp = 0)
{
if (!IsGuestThread)
{
return false;
}
_pendingBlockReason = string.IsNullOrWhiteSpace(reason) ? "guest_thread_blocked" : reason;
_pendingBlockWakeKey = string.IsNullOrWhiteSpace(wakeKey) ? _pendingBlockReason : wakeKey;
_pendingBlockWaiter = waiter;
_pendingBlockDeadlineTimestamp = blockDeadlineTimestamp;
if (context is not null && TryCaptureCurrentBlockContinuation(context, out var continuation))
{
_pendingBlockContinuation = continuation;
_pendingBlockContinuationValid = true;
}
else
{
_pendingBlockContinuation = default;
_pendingBlockContinuationValid = false;
}
return true;
}
// Compatibility bridge for exports that still describe blocked work as a
// resume/wake delegate pair. New hot paths should provide an
// IGuestThreadBlockWaiter directly to avoid allocating closures.
public static bool RequestCurrentThreadBlock(
CpuContext? context,
string reason,
string? wakeKey,
Func<int> resumeHandler,
Func<bool> wakeHandler,
long blockDeadlineTimestamp = 0) =>
RequestCurrentThreadBlock(
context,
reason,
wakeKey,
new DelegateGuestThreadBlockWaiter(resumeHandler, wakeHandler),
blockDeadlineTimestamp);
public static bool TryConsumeCurrentThreadBlock(out string reason)
{
return TryConsumeCurrentThreadBlock(out reason, out _, out _);
}
public static bool TryConsumeCurrentThreadBlock(
out string reason,
out GuestCpuContinuation continuation,
out bool hasContinuation)
{
return TryConsumeCurrentThreadBlock(
out reason,
out continuation,
out hasContinuation,
out _,
out _,
out _);
}
public static bool TryConsumeCurrentThreadBlock(
out string reason,
out GuestCpuContinuation continuation,
out bool hasContinuation,
out string wakeKey,
out IGuestThreadBlockWaiter? waiter)
{
return TryConsumeCurrentThreadBlock(
out reason,
out continuation,
out hasContinuation,
out wakeKey,
out waiter,
out _);
}
public static bool TryConsumeCurrentThreadBlock(
out string reason,
out GuestCpuContinuation continuation,
out bool hasContinuation,
out string wakeKey,
out IGuestThreadBlockWaiter? waiter,
out long blockDeadlineTimestamp)
{
reason = _pendingBlockReason ?? string.Empty;
if (string.IsNullOrEmpty(reason))
{
continuation = default;
hasContinuation = false;
wakeKey = string.Empty;
waiter = null;
blockDeadlineTimestamp = 0;
return false;
}
continuation = _pendingBlockContinuation;
hasContinuation = _pendingBlockContinuationValid;
wakeKey = _pendingBlockWakeKey ?? reason;
waiter = _pendingBlockWaiter;
blockDeadlineTimestamp = _pendingBlockDeadlineTimestamp;
_pendingBlockReason = null;
_pendingBlockContinuation = default;
_pendingBlockContinuationValid = false;
_pendingBlockWakeKey = null;
_pendingBlockWaiter = null;
_pendingBlockDeadlineTimestamp = 0;
return true;
}
public static long ComputeDeadlineTimestamp(TimeSpan timeout)
{
if (timeout <= TimeSpan.Zero)
@@ -243,6 +417,45 @@ public static class GuestThreadExecution
return now + Math.Max(1, ticks);
}
private static bool TryCaptureCurrentBlockContinuation(CpuContext context, out GuestCpuContinuation continuation)
{
if (!TryGetCurrentImportCallFrame(out var frame) ||
frame.ReturnRip < 65536 ||
frame.ResumeRsp == 0 ||
frame.ReturnSlotAddress == 0)
{
continuation = default;
return false;
}
continuation = new GuestCpuContinuation(
frame.ReturnRip,
frame.ResumeRsp,
frame.ReturnSlotAddress,
context.Rflags,
context.FsBase,
context.GsBase,
0,
context[CpuRegister.Rcx],
context[CpuRegister.Rdx],
context[CpuRegister.Rbx],
context[CpuRegister.Rbp],
context[CpuRegister.Rsi],
context[CpuRegister.Rdi],
context[CpuRegister.R8],
context[CpuRegister.R9],
context[CpuRegister.R10],
context[CpuRegister.R11],
context[CpuRegister.R12],
context[CpuRegister.R13],
context[CpuRegister.R14],
context[CpuRegister.R15],
context.FpuControlWord,
context.Mxcsr,
RestoreFullFpuState: false);
return true;
}
public static void RequestCurrentEntryExit(string reason, int status)
{
RequestCurrentEntryExit(reason, unchecked((ulong)(long)status));
+59 -118
View File
@@ -568,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(
@@ -1775,58 +1773,6 @@ public static partial class AgcExports
return ReturnPointer(ctx, commandAddress);
}
// Single-register variant of the SET_SH_REG builders: the register rides
// in rsi as a packed struct (low 16 bits = register offset, high dword =
// byte offset of this dword within a multi-dword register write) and the
// value in edx. Emits the same 3-dword SET_SH_REG packet the plural
// sceAgcCbSetShRegistersDirect path produces per run. Hades calls this
// ~1k times per boot; leaving it unresolved corrupted its DCB stream.
[SysAbiExport(
Nid = "pFLArOT53+w",
ExportName = "sceAgcDcbSetShRegisterDirect",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int DcbSetShRegisterDirect(CpuContext ctx)
{
var commandBufferAddress = ctx[CpuRegister.Rdi];
var packedRegister = ctx[CpuRegister.Rsi];
var value = (uint)ctx[CpuRegister.Rdx];
if (commandBufferAddress == 0)
{
return ReturnPointer(ctx, 0);
}
var offset = (uint)(packedRegister & 0xFFFFu) + (uint)((packedRegister >> 32) >> 2);
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 3, out var commandAddress) ||
!TryWriteUInt32(ctx, commandAddress, Pm4(3, ItSetShReg, 0)) ||
!TryWriteUInt32(ctx, commandAddress + 4, offset & 0xFFFFu) ||
!TryWriteUInt32(ctx, commandAddress + 8, value))
{
return ReturnPointer(ctx, 0);
}
TraceAgc($"agc.dcb_set_sh_register_direct buf=0x{commandBufferAddress:X16} reg=0x{offset:X4} value=0x{value:X8}");
return ReturnPointer(ctx, commandAddress);
}
// Size probe for the wait-on-address writer below: same argument prefix
// minus the command buffer, returns the byte size the writer will emit so
// the game can reserve DCB space (7 dwords for a standard WAIT_REG_MEM,
// 6/9 for the 32/64-bit polled-NOP forms).
[SysAbiExport(
Nid = "43WJ08sSugE",
ExportName = "sceAgcDcbWaitOnAddressGetSize",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int DcbWaitOnAddressGetSize(CpuContext ctx)
{
var size = (uint)(ctx[CpuRegister.Rdi] & 0xFF);
var operation = (uint)(ctx[CpuRegister.Rdx] & 0xFF);
var packetDwords = operation is 2 or 3 ? 7u : size == 0 ? 6u : 9u;
ctx[CpuRegister.Rax] = packetDwords * sizeof(uint);
return (int)ctx[CpuRegister.Rax];
}
[SysAbiExport(
Nid = "VmW0Tdpy420",
ExportName = "sceAgcDcbWaitRegMem",
@@ -2676,32 +2622,6 @@ public static partial class AgcExports
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
// Hands the game the driver-side context id its GPU event-queue packets
// reference. We key events purely on (equeue, ident, filter), so a single
// stable id satisfies the contract; the game only checks the call
// succeeded and threads the id back through later driver calls.
[SysAbiExport(
Nid = "Zw7uUVPulbw",
ExportName = "sceAgcDriverGetEqContextId",
Target = Generation.Gen5,
LibraryName = "libSceAgcDriver")]
public static int DriverGetEqContextId(CpuContext ctx)
{
var contextIdAddress = ctx[CpuRegister.Rdi];
if (contextIdAddress == 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (!TryWriteUInt32(ctx, contextIdAddress, 1))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceAgc($"agc.driver_get_eq_context_id out=0x{contextIdAddress:X16} -> 1");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "DL2RXaXOy88",
ExportName = "sceAgcDriverDeleteEqEvent",
@@ -3393,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 &&
@@ -3735,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()
@@ -4918,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);
}
}
@@ -5010,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) =>
@@ -5098,7 +5039,7 @@ public static partial class AgcExports
}
}
return resumedCount;
return;
}
if (woken is not null)
@@ -5106,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(
@@ -8956,7 +8894,7 @@ public static partial class AgcExports
out _);
var globalMemoryBuffers =
CreateTranslatedComputeGlobalBuffers(evaluation);
GuestGpu.Current.SubmitComputeDispatch(
var workSequence = GuestGpu.Current.SubmitComputeDispatch(
shaderAddress,
computeShader,
textures,
@@ -8975,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}";
}
}
}
@@ -27,7 +27,7 @@ internal static class AgcShaderCompilerHooks
internal static void Install()
{
Gen5ShaderScalarEvaluator.FallbackMemoryReader =
KernelMemoryCompatExports.TryReadShaderGuestMemory;
KernelMemoryCompatExports.TryReadTrackedLibcHeap;
Gen5ShaderScalarEvaluator.GlobalMemoryPool =
GuestDataPool.Shared;
}
+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)
{
+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);
}
@@ -228,17 +228,6 @@ internal static partial class MetalVideoPresenter
private static void RunWindowLoop()
{
// The Metal Performance HUD attaches to a process at Metal init;
// toggling developerHUDProperties on a live layer is inert when the
// HUD was never armed. SHARPEMU_METAL_HUD=1 arms it the documented way
// (setenv lands in the native environ before the device initializes),
// showing the HUD from launch; Cmd+F1 then toggles it per-layer.
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_METAL_HUD"), "1", StringComparison.Ordinal))
{
Environment.SetEnvironmentVariable("MTL_HUD_ENABLED", "1");
_metalHudVisible = true;
}
MetalNative.EnsureFrameworksLoaded();
_device = MetalNative.MTLCreateSystemDefaultDevice();
@@ -517,14 +506,6 @@ internal static partial class MetalVideoPresenter
Console.Error.WriteLine(
$"[LOADER][INFO] Metal Performance HUD {(_metalHudVisible ? "shown" : "hidden")} (Cmd+F1).");
if (_metalHudVisible &&
!string.Equals(Environment.GetEnvironmentVariable("MTL_HUD_ENABLED"), "1", StringComparison.Ordinal))
{
// The per-layer property only takes effect when the HUD attached at
// Metal init; without that the toggle is a no-op on some macOS builds.
Console.Error.WriteLine(
"[LOADER][INFO] If the HUD does not appear, relaunch with SHARPEMU_METAL_HUD=1 (arms Apple's HUD at startup).");
}
}
private static unsafe nint CreateRenderTimerTarget()
-21
View File
@@ -43,25 +43,4 @@ public static class ImeExports
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// No hardware keyboard is ever connected; zero the caller's info struct so
// it reads as "not connected" rather than uninitialized stack.
[SysAbiExport(
Nid = "VkqLPArfFdc",
ExportName = "sceImeKeyboardGetInfo",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceIme")]
public static int ImeKeyboardGetInfo(CpuContext ctx)
{
var infoAddress = ctx[CpuRegister.Rsi] != 0 ? ctx[CpuRegister.Rsi] : ctx[CpuRegister.Rdi];
if (infoAddress != 0)
{
Span<byte> info = stackalloc byte[0x40];
info.Clear();
_ = ctx.Memory.TryWrite(infoAddress, info);
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}
@@ -5,12 +5,14 @@ using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Text;
using SharpEmu.HLE;
using SharpEmu.Libs.Fiber;
namespace SharpEmu.Libs.Kernel;
public static class KernelEventFlagCompatExports
{
private const int MaxEventFlagNameLength = 31;
private const int HostWaitPumpMilliseconds = 1;
private const uint AttrThreadFifo = 0x01;
private const uint AttrThreadPriority = 0x02;
private const uint AttrSingle = 0x10;
@@ -23,7 +25,8 @@ public static class KernelEventFlagCompatExports
private static readonly ConcurrentDictionary<ulong, EventFlagState> _eventFlags = new();
private static long _nextEventFlagHandle = 1;
// Cached once: gating every call site avoids building the interpolated trace string when disabled.
// Cached once: gating every call site avoids building the interpolated
// trace string (and FormatFrameChain/FormatGuestWaitObject) when disabled.
private static readonly bool _traceEventFlag = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_LOG_EVENT_FLAG"), "1", StringComparison.Ordinal);
@@ -125,11 +128,11 @@ public static class KernelEventFlagCompatExports
lock (state.Gate)
{
state.Bits |= pattern;
// Wake threads parked in-place on the gate; each re-checks its pattern.
Monitor.PulseAll(state.Gate);
if (_traceEventFlag) TraceEventFlag($"set handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
}
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventFlagWakeKey(handle));
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
@@ -226,67 +229,134 @@ public static class KernelEventFlagCompatExports
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
// A zero-microsecond timeout degrades to an instant poll because the
// deadline is already in the past.
var hostDeadlineMs = timeoutAddress != 0
? Environment.TickCount64 + (timeoutUsec == 0
? 0L
: Math.Max(1L, (timeoutUsec + 999L) / 1000L))
: long.MaxValue;
lock (state.Gate)
Monitor.Enter(state.Gate);
try
{
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var immediateWaitResult))
{
if (_traceEventFlag) TraceEventFlag($"poll handle=0x{handle:X16} pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
return SetReturn(ctx, immediateWaitResult);
}
// In-place block on the flag gate. Monitor.Wait releases the gate
// and parks atomically, so a concurrent SetEventFlag's PulseAll
// cannot be lost between the satisfy check and the park. On wake
// the predicate is re-evaluated (Set semantics on FreeBSD-style
// event flags: OR/AND over the bit pattern, optional clear).
state.WaitingThreads++;
var guestThreadHandle = GuestThreadExecution.CurrentGuestThreadHandle;
if (_traceEventFlag) TraceEventFlag($"wait-block handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{guestThreadHandle:X16} ret=0x{returnRip:X16}");
GuestThreadBlocking.NoteBlocked(guestThreadHandle, "sceKernelWaitEventFlag");
try
{
while (true)
// Timed waits block on a deadline instead of returning TIMED_OUT
// immediately; a zero-microsecond timeout still degrades to an
// instant poll because the deadline is already in the past.
var deadline = timeoutAddress != 0
? GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromMicroseconds(timeoutUsec))
: 0;
var hostDeadlineMs = timeoutAddress != 0
? Environment.TickCount64 + (timeoutUsec == 0
? 0L
: Math.Max(1L, (timeoutUsec + 999L) / 1000L))
: long.MaxValue;
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
var managedThread = Environment.CurrentManagedThreadId;
var blockedWaitResult = OrbisGen2Result.ORBIS_GEN2_OK;
var satisfied = false;
var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitEventFlag",
GetEventFlagWakeKey(handle),
() =>
{
if (GuestThreadBlocking.ShutdownRequested)
if (satisfied)
{
if (timeoutAddress != 0) _ = TryWriteUInt32(ctx, timeoutAddress, 0);
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
return (int)blockedWaitResult;
}
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var wokeResult))
{
if (timeoutAddress != 0) _ = TryWriteUInt32(ctx, timeoutAddress, 0);
if (_traceEventFlag) TraceEventFlag($"wait-wake handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
return SetReturn(ctx, wokeResult);
}
var remaining = hostDeadlineMs - Environment.TickCount64;
if (timeoutAddress != 0 && remaining <= 0)
// Deadline expiry: report timeout with the current bits.
if (timeoutAddress != 0)
{
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
if (_traceEventFlag) TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
GuestThreadBlocking.Checkpoint(guestThreadHandle, state.Gate);
_ = Monitor.Wait(state.Gate, (int)Math.Min(remaining, GuestThreadBlocking.WaitSliceMilliseconds));
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
},
() =>
{
if (!TryPrepareBlockedWait(
ctx,
state,
pattern,
waitMode,
resultAddress,
out var preparedResult))
{
return false;
}
blockedWaitResult = preparedResult;
satisfied = true;
return true;
},
deadline);
if (_traceEventFlag) TraceEventFlag($"wait-unsatisfied handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} block={requestedBlock} ret=0x{returnRip:X16} frames={FormatFrameChain(ctx)}");
if (_traceEventFlag) TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}");
if (!requestedBlock)
{
var scheduler = GuestThreadExecution.Scheduler;
if (scheduler is null)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
}
state.WaitingThreads++;
if (_traceEventFlag) TraceEventFlag($"wait-pump handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
var releaseWaiter = true;
try
{
while (true)
{
Monitor.Exit(state.Gate);
try
{
scheduler.Pump(ctx, "sceKernelWaitEventFlag");
}
finally
{
Monitor.Enter(state.Gate);
}
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var pumpedWaitResult))
{
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
releaseWaiter = false;
if (_traceEventFlag) TraceEventFlag($"wait-wake handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} waiters={state.WaitingThreads} ret=0x{returnRip:X16}");
return SetReturn(ctx, pumpedWaitResult);
}
var remaining = hostDeadlineMs - Environment.TickCount64;
if (timeoutAddress != 0 && remaining <= 0)
{
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
releaseWaiter = false;
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
if (_traceEventFlag) TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
Monitor.Wait(state.Gate, (int)Math.Min(remaining, HostWaitPumpMilliseconds));
}
}
finally
{
if (releaseWaiter)
{
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
}
}
}
finally
{
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
GuestThreadBlocking.NoteUnblocked(guestThreadHandle);
}
state.WaitingThreads++;
if (_traceEventFlag) TraceEventFlag($"wait-block handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
finally
{
Monitor.Exit(state.Gate);
}
}
@@ -385,6 +455,41 @@ public static class KernelEventFlagCompatExports
return true;
}
private static bool TryPrepareBlockedWait(
CpuContext ctx,
EventFlagState state,
ulong pattern,
uint waitMode,
ulong resultAddress,
out OrbisGen2Result result)
{
lock (state.Gate)
{
result = OrbisGen2Result.ORBIS_GEN2_OK;
if (!IsSatisfied(state.Bits, pattern, waitMode))
{
return false;
}
if (!TryWriteResultPattern(ctx, resultAddress, state.Bits))
{
result = OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
else
{
ApplyClearMode(state, pattern, waitMode);
}
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
if (_traceEventFlag) TraceEventFlag(
$"wait-wake pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16} waiters={state.WaitingThreads}");
return true;
}
}
private static string GetEventFlagWakeKey(ulong handle) =>
$"event_flag:0x{handle:X16}";
private static bool TryWriteResultPattern(CpuContext ctx, ulong address, ulong bits) =>
address == 0 || ctx.TryWriteUInt64(address, bits);
@@ -414,6 +519,19 @@ public static class KernelEventFlagCompatExports
return true;
}
private static bool TryReadByte(CpuContext ctx, ulong address, out byte value)
{
Span<byte> buffer = stackalloc byte[1];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = buffer[0];
return true;
}
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
@@ -466,4 +584,95 @@ public static class KernelEventFlagCompatExports
? frame.ReturnRip
: 0UL;
private static string FormatFrameChain(CpuContext ctx)
{
Span<ulong> returns = stackalloc ulong[4];
var count = 0;
var frame = ctx[CpuRegister.Rbp];
for (var index = 0; index < returns.Length && frame != 0; index++)
{
if (!ctx.TryReadUInt64(frame, out var nextFrame) ||
!ctx.TryReadUInt64(frame + sizeof(ulong), out var returnAddress))
{
break;
}
returns[count++] = returnAddress;
if (nextFrame <= frame)
{
break;
}
frame = nextFrame;
}
return count switch
{
0 => "none",
1 => $"0x{returns[0]:X16}",
2 => $"0x{returns[0]:X16},0x{returns[1]:X16}",
3 => $"0x{returns[0]:X16},0x{returns[1]:X16},0x{returns[2]:X16}",
_ => $"0x{returns[0]:X16},0x{returns[1]:X16},0x{returns[2]:X16},0x{returns[3]:X16}",
};
}
private static string FormatGuestWaitObject(CpuContext ctx)
{
var r12 = ctx[CpuRegister.R12];
var r13 = ctx[CpuRegister.R13];
var objectAddress = r12 != 0
? r12
: r13 >= 0xA8
? r13 - 0xA8
: 0;
var builder = new StringBuilder(256);
builder.Append($"r12=0x{r12:X16} r13=0x{r13:X16}");
if (objectAddress == 0)
{
return builder.ToString();
}
builder.Append($" obj=0x{objectAddress:X16}");
AppendUInt32(builder, ctx, objectAddress + 0x58, "o58");
AppendUInt32(builder, ctx, objectAddress + 0x5C, "o5C");
AppendUInt64(builder, ctx, objectAddress + 0x60, "o60");
AppendByte(builder, ctx, objectAddress + 0x6C, "state6C");
AppendByte(builder, ctx, objectAddress + 0x6D, "o6D");
AppendByte(builder, ctx, objectAddress + 0xA0, "waitA0");
AppendByte(builder, ctx, objectAddress + 0xA1, "stateA1");
AppendByte(builder, ctx, objectAddress + 0xA2, "oA2");
AppendUInt64(builder, ctx, objectAddress + 0xA8, "eventA8");
if (r13 != 0)
{
AppendUInt64(builder, ctx, r13, "r13_0");
AppendUInt64(builder, ctx, r13 + 8, "r13_8");
}
return builder.ToString();
}
private static void AppendByte(StringBuilder builder, CpuContext ctx, ulong address, string name)
{
if (TryReadByte(ctx, address, out var value))
{
builder.Append($" {name}=0x{value:X2}");
}
}
private static void AppendUInt32(StringBuilder builder, CpuContext ctx, ulong address, string name)
{
if (TryReadUInt32(ctx, address, out var value))
{
builder.Append($" {name}=0x{value:X8}");
}
}
private static void AppendUInt64(StringBuilder builder, CpuContext ctx, ulong address, string name)
{
if (TryReadUInt64(ctx, address, out var value))
{
builder.Append($" {name}=0x{value:X16}");
}
}
}
@@ -93,6 +93,19 @@ public static class KernelEventQueueCompatExports
}
}
private sealed class EqueueWaiter : IGuestThreadBlockWaiter
{
public required CpuContext Ctx { get; init; }
public required ulong Handle { get; init; }
public required ulong EventsAddress { get; init; }
public required int EventCapacity { get; init; }
public required ulong OutCountAddress { get; init; }
public int Resume() => ResumeWaitEqueue(Ctx, Handle, EventsAddress, EventCapacity, OutCountAddress);
public bool TryWake() => HasPendingEvents(Handle);
}
[SysAbiExport(
Nid = "D0OdFMjp46I",
ExportName = "sceKernelCreateEqueue",
@@ -136,10 +149,10 @@ public static class KernelEventQueueCompatExports
_eventQueues.Remove(handle);
_pendingEvents.Remove(handle);
_registeredEvents.Remove(handle);
// Wake any thread parked on this queue so it observes the deletion.
Monitor.PulseAll(_eventQueueGate);
}
_wakeKeys.TryRemove(handle, out _);
TraceEventQueue(ctx, "delete", handle);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -399,81 +412,59 @@ public static class KernelEventQueueCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// No events ready: block this host thread in place on the queue gate.
// Monitor.Wait releases the gate and parks atomically, so an
// EnqueueEvent/TriggerDisplayEvent PulseAll issued the instant after
// the emptiness check cannot be lost. kqueue/kevent semantics: sleep
// until an event matching a registration is delivered or the timeout
// (usec, infinite when the arg pointer is null) lapses; a zero timeout
// degrades to an instant poll.
long deadline;
if (timeoutAddress == 0)
{
deadline = long.MaxValue;
}
else if (timeoutUsec == 0)
{
deadline = 0;
}
else
{
deadline = Environment.TickCount64 + Math.Max(1L, timeoutUsec / 1000L);
}
TraceEventQueue(ctx, "wait-block", handle);
var guestThreadHandle = GuestThreadExecution.CurrentGuestThreadHandle;
GuestThreadBlocking.NoteBlocked(guestThreadHandle, "sceKernelWaitEqueue");
try
{
lock (_eventQueueGate)
{
while (true)
if (timeoutAddress == 0 &&
GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitEqueue",
GetEventQueueWakeKey(handle),
new EqueueWaiter
{
if ((_pendingEvents.TryGetValue(handle, out var queue) && queue.Count != 0) ||
!_eventQueues.Contains(handle) ||
GuestThreadBlocking.ShutdownRequested)
{
break;
}
var remaining = deadline - Environment.TickCount64;
if (timeoutAddress != 0 && remaining <= 0)
{
break;
}
var slice = timeoutAddress == 0
? GuestThreadBlocking.WaitSliceMilliseconds
: (int)Math.Min(remaining, GuestThreadBlocking.WaitSliceMilliseconds);
GuestThreadBlocking.Checkpoint(guestThreadHandle, _eventQueueGate);
_ = Monitor.Wait(_eventQueueGate, slice);
}
}
}
finally
Ctx = ctx,
Handle = handle,
EventsAddress = eventsAddress,
EventCapacity = eventCapacity,
OutCountAddress = outCountAddress,
}))
{
GuestThreadBlocking.NoteUnblocked(guestThreadHandle);
}
deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (deliveredCount > 0)
{
TraceEventQueue(ctx, "wait-deliver", handle);
TraceEventQueue(ctx, "wait-block", handle);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (timeoutAddress != 0)
if (timeoutAddress != 0 && ctx.TryReadUInt64(timeoutAddress, out var timeoutRaw))
{
var timeoutMicros = timeoutRaw & 0xFFFF_FFFFUL;
var deadline = Environment.TickCount64 +
Math.Max(1L, (long)Math.Min(timeoutMicros / 1000, int.MaxValue));
lock (_eventQueueGate)
{
while (!HasPendingEvents(handle))
{
var remaining = deadline - Environment.TickCount64;
if (remaining <= 0)
{
break;
}
Monitor.Wait(_eventQueueGate, (int)Math.Min(remaining, 100));
}
}
deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (deliveredCount > 0)
{
TraceEventQueue(ctx, "wait-timed-deliver", handle);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
TraceEventQueue(ctx, "wait-timeout", handle);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
}
// Reached only on queue deletion or teardown; the guest sees zero events.
TraceEventQueue(ctx, "wait", handle);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -807,6 +798,32 @@ public static class KernelEventQueueCompatExports
return triggered;
}
private static int ResumeWaitEqueue(
CpuContext ctx,
ulong handle,
ulong eventsAddress,
int eventCapacity,
ulong outCountAddress)
{
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
return deliveredCount > 0
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
}
private static bool HasPendingEvents(ulong handle)
{
lock (_eventQueueGate)
{
return _pendingEvents.TryGetValue(handle, out var events) && events.Count != 0;
}
}
private static void QueueOrUpdateEvent(
KernelEventDeque queue,
KernelQueuedEvent queuedEvent)
@@ -824,16 +841,16 @@ public static class KernelEventQueueCompatExports
};
}
// Wake threads parked in-place on the queue gate; each re-checks for a
// matching pending event. The handle is unused (all queues share one gate)
// but kept in the signature so call sites read intent-fully.
// Wake keys are formatted once per handle: WakeEventQueue runs on every event
// enqueue (vblank/flip edges included), so formatting there is steady string churn.
private static readonly ConcurrentDictionary<ulong, string> _wakeKeys = new();
private static string GetEventQueueWakeKey(ulong handle) =>
_wakeKeys.GetOrAdd(handle, static h => $"sceKernelWaitEqueue:{h:X16}");
private static void WakeEventQueue(ulong handle)
{
_ = handle;
lock (_eventQueueGate)
{
Monitor.PulseAll(_eventQueueGate);
}
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventQueueWakeKey(handle));
}
private static int DequeueEvents(CpuContext ctx, ulong handle, ulong eventsAddress, int eventCapacity)
@@ -1819,15 +1819,6 @@ public static partial class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// POSIX alias; same Orbis result convention as the other posix-named
// file exports in this module (mkdir/rmdir/open).
[SysAbiExport(
Nid = "VAzswvTOCzI",
ExportName = "unlink",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixUnlink(CpuContext ctx) => KernelUnlink(ctx);
[SysAbiExport(
Nid = "AUXVxWeJU-A",
ExportName = "sceKernelUnlink",
@@ -6019,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))
{
@@ -6028,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
@@ -6068,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)
@@ -6382,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))
{
@@ -6391,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
@@ -6418,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;
@@ -37,35 +37,55 @@ public static class KernelPthreadCompatExports
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREAD_CONDS"), "1", StringComparison.Ordinal);
private static readonly HashSet<ulong>? _tracePthreadMutexFilter = ParseTraceAddressFilter(
Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREAD_MUTEX_FILTER"));
private static long _nextSynchronizationWaiterId;
// Blocking model: waiters block their own host thread in place via
// Monitor.Wait on the state object (mutexes) or SyncRoot (condvars).
// Block-and-wake is therefore atomic — no waiter queues, wake keys, or
// continuation hand-offs, and no lost-wakeup window between a thread
// deciding to block and registering as blocked.
private sealed class PthreadMutexState
{
public ulong OwnerThreadId { get; set; }
public int RecursionCount { get; set; }
public int Type { get; set; } = MutexTypeErrorCheck;
public int Protocol { get; set; }
// Threads currently blocked in PthreadMutexLockCore; destroy reports BUSY while nonzero.
public int WaiterCount { get; set; }
public LinkedList<PthreadMutexWaiter> Waiters { get; } = new();
}
private sealed class PthreadMutexWaiter
{
public required ulong ThreadId { get; init; }
public required string WakeKey { get; init; }
public required bool Cooperative { get; init; }
public LinkedListNode<PthreadMutexWaiter>? Node { get; set; }
public int Granted;
}
private sealed class PthreadCondState
{
public object SyncRoot { get; } = new();
public LinkedList<PthreadCondWaiter> WaiterQueue { get; } = new();
public ulong SignalEpoch { get; set; }
public int Waiters { get; set; }
// Signals produced but not yet consumed by a waiter. A signal only
// increments this when an unserved waiter exists (POSIX: signaling an
// empty condvar is a no-op), so stale signals cannot accumulate.
public int SignalsPending { get; set; }
}
private sealed class PthreadCondWaiter
{
public required ulong ThreadId { get; init; }
public required PthreadMutexState MutexState { get; init; }
public required string WakeKey { get; init; }
public required bool Cooperative { get; init; }
public bool PosixErrors { get; init; }
public LinkedListNode<PthreadCondWaiter>? Node { get; set; }
public PthreadMutexWaiter? MutexWaiter { get; set; }
public Timer? TimeoutTimer { get; set; }
// 0 = waiting, 1 = signaled, 2 = timed out.
public int CompletionState { get; set; }
}
private readonly record struct PthreadMutexAttrState(int Type, int Protocol);
static KernelPthreadCompatExports()
{
RunSynchronizationSelfChecks();
}
[SysAbiExport(
Nid = "aI+OeCz8xrQ",
ExportName = "scePthreadSelf",
@@ -119,13 +139,6 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "9vyP6Z7bqzc",
ExportName = "pthread_rename_np",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadRenameNp(CpuContext ctx) => PthreadRename(ctx);
[SysAbiExport(
Nid = "GBUY7ywdULE",
ExportName = "scePthreadRename",
@@ -610,7 +623,7 @@ public static class KernelPthreadCompatExports
lock (state)
{
if (state.OwnerThreadId != 0 || state.RecursionCount != 0 || state.WaiterCount != 0)
if (state.OwnerThreadId != 0 || state.RecursionCount != 0 || state.Waiters.Count != 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
}
@@ -640,6 +653,10 @@ public static class KernelPthreadCompatExports
}
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
var canCooperativelyBlock = !tryOnly &&
GuestThreadExecution.IsGuestThread &&
GuestThreadExecution.TryGetCurrentImportCallFrame(out _);
PthreadMutexWaiter? waiter = null;
lock (state)
{
if (state.OwnerThreadId == currentThreadId)
@@ -679,7 +696,14 @@ public static class KernelPthreadCompatExports
}
}
if (state.OwnerThreadId == 0)
// pthread_mutex_trylock succeeds whenever the mutex is not currently
// held; unlike the blocking lock it does not queue behind waiters
// (POSIX gives it no fairness obligation). Gating trylock on an empty
// wait queue is wrong and, worse, lets a single stale/undrainable
// 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.OwnerThreadId = currentThreadId;
state.RecursionCount = 1;
@@ -693,39 +717,24 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
}
// Contended: block this host thread in place until the owner
// releases. Monitor.Wait atomically releases the state lock and
// parks, so an unlock's PulseAll cannot be missed. Waits are
// sliced only so teardown can unwind parked threads.
waiter = EnqueueMutexWaiterLocked(state, currentThreadId, canCooperativelyBlock);
}
if (canCooperativelyBlock && waiter is not null &&
GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"pthread_mutex_lock",
waiter.WakeKey,
() => CompleteBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter),
() => TryGrantBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter)))
{
TracePthreadMutex(ctx, "lock-block", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
GuestThreadBlocking.NoteBlocked(currentThreadId, "pthread_mutex_lock");
state.WaiterCount++;
try
{
while (state.OwnerThreadId != 0)
{
if (GuestThreadBlocking.ShutdownRequested)
{
TracePthreadMutex(ctx, "lock-shutdown", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
}
GuestThreadBlocking.Checkpoint(currentThreadId, state);
_ = Monitor.Wait(state, GuestThreadBlocking.WaitSliceMilliseconds);
}
state.OwnerThreadId = currentThreadId;
state.RecursionCount = 1;
}
finally
{
state.WaiterCount--;
GuestThreadBlocking.NoteUnblocked(currentThreadId);
}
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
var hostResult = WaitForHostMutexLock(state, waiter!);
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, hostResult);
return hostResult;
}
private static int PthreadMutexUnlockCore(CpuContext ctx, ulong mutexAddress, bool requireOwner)
@@ -742,6 +751,7 @@ public static class KernelPthreadCompatExports
}
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
string? nextWakeKey = null;
lock (state)
{
if (state.RecursionCount <= 0)
@@ -760,13 +770,18 @@ public static class KernelPthreadCompatExports
if (state.RecursionCount == 0)
{
state.OwnerThreadId = 0;
if (state.WaiterCount != 0)
{
Monitor.PulseAll(state);
}
nextWakeKey = state.Waiters.First?.Value.Cooperative == true
? state.Waiters.First.Value.WakeKey
: null;
Monitor.PulseAll(state);
}
}
if (nextWakeKey is not null)
{
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(nextWakeKey, 1);
}
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -1183,7 +1198,7 @@ public static class KernelPthreadCompatExports
lock (state.SyncRoot)
{
if (state.Waiters != 0)
if (state.WaiterQueue.Count != 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
}
@@ -1248,80 +1263,94 @@ public static class KernelPthreadCompatExports
}
}
var signaled = false;
var cooperative = GuestThreadExecution.IsGuestThread &&
GuestThreadExecution.TryGetCurrentImportCallFrame(out _);
var waiter = new PthreadCondWaiter
{
ThreadId = currentThreadId,
MutexState = mutexState,
Cooperative = cooperative,
PosixErrors = posixErrors,
WakeKey = cooperative
? $"pthread_cond_waiter:{Interlocked.Increment(ref _nextSynchronizationWaiterId)}"
: string.Empty,
};
lock (state.SyncRoot)
{
waiter.Node = state.WaiterQueue.AddLast(waiter);
state.Waiters++;
TracePthreadCond("wait-enter", condAddress, mutexAddress, state, timed, (int)OrbisGen2Result.ORBIS_GEN2_OK);
// POSIX atomicity: we are registered as a waiter (Waiters++ under
// SyncRoot) before the mutex is released, so a signal issued the
// instant the mutex unlocks already counts us and lands in
// SignalsPending — checked before the first Monitor.Wait. No
// window exists where a wake can be lost.
var unlockResult = PthreadMutexUnlockCore(ctx, mutexAddress, requireOwner: true);
if (unlockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
state.Waiters--;
RemoveCondWaiterLocked(state, waiter);
TracePthreadCond("wait-unlock-fail", condAddress, mutexAddress, state, timed, unlockResult);
return unlockResult;
}
var deadline = timed
? GuestThreadExecution.ComputeDeadlineTimestamp(GetCondWaitTimeout(timeoutUsec))
: long.MaxValue;
GuestThreadBlocking.NoteBlocked(currentThreadId, timed ? "pthread_cond_timedwait" : "pthread_cond_wait");
try
if (cooperative && timed)
{
while (state.SignalsPending == 0 && !GuestThreadBlocking.ShutdownRequested)
{
var remaining = timed
? GetRemainingTimeout(deadline)
: TimeSpan.FromMilliseconds(GuestThreadBlocking.WaitSliceMilliseconds);
if (timed && remaining <= TimeSpan.Zero)
waiter.TimeoutTimer = new Timer(
static callbackState =>
{
break;
}
if (remaining > TimeSpan.FromMilliseconds(GuestThreadBlocking.WaitSliceMilliseconds))
{
remaining = TimeSpan.FromMilliseconds(GuestThreadBlocking.WaitSliceMilliseconds);
}
GuestThreadBlocking.Checkpoint(currentThreadId, state.SyncRoot);
_ = Monitor.Wait(state.SyncRoot, remaining);
}
}
finally
{
GuestThreadBlocking.NoteUnblocked(currentThreadId);
}
if (state.SignalsPending > 0)
{
state.SignalsPending--;
signaled = true;
}
state.Waiters--;
if (state.SignalsPending > state.Waiters)
{
// A timed-out waiter left a signal unconsumed with nobody
// remaining to take it; drop it so a future wait does not
// observe a phantom wake (signals on an empty condvar are
// no-ops on real hardware).
state.SignalsPending = state.Waiters;
var (condState, condWaiter) = ((PthreadCondState, PthreadCondWaiter))callbackState!;
CompleteCondWaiter(condState, condWaiter, timedOut: true);
},
(state, waiter),
GetCondWaitTimeout(timeoutUsec),
Timeout.InfiniteTimeSpan);
}
}
// POSIX guarantees the mutex is re-acquired on every return path,
// signaled or timed out. Blocks in place like any other locker.
_ = PthreadMutexLockCore(ctx, mutexAddress, tryOnly: false);
if (cooperative &&
GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
timed ? "pthread_cond_timedwait" : "pthread_cond_wait",
waiter.WakeKey,
() => CompleteBlockedCondWait(ctx, condAddress, mutexAddress, state, waiter),
() => TryGrantCondWaiterMutex(waiter)))
{
TracePthreadCond("wait-block", condAddress, mutexAddress, state, timed, (int)OrbisGen2Result.ORBIS_GEN2_OK);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
var waitResult = signaled || !timed
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: CondTimedOutResult(posixErrors);
TracePthreadCond(signaled ? "wait-exit" : "wait-exit-timeout", condAddress, mutexAddress, state, timed, waitResult);
// Non-guest callers have no resumable CPU continuation. Park only
// those host-side compatibility callers, preserving the same FIFO
// mutex reacquisition rules as cooperative guest waiters.
lock (state.SyncRoot)
{
var deadline = timed
? GuestThreadExecution.ComputeDeadlineTimestamp(GetCondWaitTimeout(timeoutUsec))
: long.MaxValue;
while (waiter.CompletionState == 0)
{
if (!timed)
{
Monitor.Wait(state.SyncRoot);
continue;
}
var remaining = GetRemainingTimeout(deadline);
if (remaining <= TimeSpan.Zero || !Monitor.Wait(state.SyncRoot, remaining))
{
CompleteCondWaiterLocked(state, waiter, timedOut: true);
break;
}
}
}
if (waiter.MutexWaiter is null)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
_ = WaitForHostMutexLock(mutexState, waiter.MutexWaiter);
var waitResult = waiter.CompletionState == 2
? CondTimedOutResult(waiter)
: (int)OrbisGen2Result.ORBIS_GEN2_OK;
TracePthreadCond(waiter.CompletionState == 2 ? "wait-exit-timeout" : "wait-exit", condAddress, mutexAddress, state, timed, waitResult);
return waitResult;
}
@@ -1337,36 +1366,305 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
List<PthreadCondWaiter>? completedWaiters = null;
lock (state.SyncRoot)
{
state.SignalEpoch++;
if (broadcast)
for (var node = state.WaiterQueue.First; node is not null;)
{
state.SignalsPending = state.Waiters;
}
else if (state.SignalsPending < state.Waiters)
{
// Only count a signal an unserved waiter can consume; signaling
// an empty condvar is a no-op per POSIX.
state.SignalsPending++;
}
var next = node.Next;
var waiter = node.Value;
if (waiter.CompletionState == 0 && CompleteCondWaiterLocked(state, waiter, timedOut: false))
{
(completedWaiters ??= new List<PthreadCondWaiter>()).Add(waiter);
if (!broadcast)
{
break;
}
}
if (state.Waiters != 0)
{
Monitor.PulseAll(state.SyncRoot);
node = next;
}
TracePthreadCond(broadcast ? "broadcast" : "signal", condAddress, mutexAddress: 0, state, timed: false, (int)OrbisGen2Result.ORBIS_GEN2_OK);
}
if (completedWaiters is not null)
{
foreach (var waiter in completedWaiters)
{
WakeCooperativeWaiter(waiter);
}
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static int CondTimedOutResult(bool posixErrors) =>
posixErrors
private static PthreadMutexWaiter EnqueueMutexWaiterLocked(
PthreadMutexState state,
ulong threadId,
bool cooperative,
string? wakeKey = null)
{
// A guest thread can have at most one pending acquisition on a mutex —
// it is either running or blocked on exactly one wait. If a waiter for
// this thread is still queued when it comes back for a fresh
// acquisition, that entry is a stale leftover the thread abandoned
// (most often a cond_timedwait timeout whose re-acquire hand-off was
// lost). Stale entries clog the FIFO head with waiters no thread is
// blocked on, so the unlock hand-off wakes a dead wake-key and the
// mutex wedges permanently (observed deadlocking Hades: several
// re-acquire waiters from one thread piled ahead of a live locker).
// Prune any prior entry for this thread before enqueueing the new one.
if (threadId != 0)
{
for (var node = state.Waiters.First; node is not null;)
{
var next = node.Next;
if (node.Value.ThreadId == threadId)
{
state.Waiters.Remove(node);
node.Value.Node = null;
}
node = next;
}
}
var waiter = new PthreadMutexWaiter
{
ThreadId = threadId,
Cooperative = cooperative,
WakeKey = cooperative
? wakeKey ?? $"pthread_mutex_waiter:{Interlocked.Increment(ref _nextSynchronizationWaiterId)}"
: string.Empty,
};
waiter.Node = state.Waiters.AddLast(waiter);
return waiter;
}
[Conditional("DEBUG")]
private static void RunSynchronizationSelfChecks()
{
var mutex = new PthreadMutexState();
PthreadMutexWaiter first;
PthreadMutexWaiter second;
lock (mutex)
{
first = EnqueueMutexWaiterLocked(mutex, 0x101, cooperative: false);
second = EnqueueMutexWaiterLocked(mutex, 0x202, cooperative: false);
Debug.Assert(!TryGrantMutexWaiterLocked(mutex, second), "A mutex waiter bypassed FIFO order.");
Debug.Assert(TryGrantMutexWaiterLocked(mutex, first), "The FIFO mutex head was not granted.");
Debug.Assert(mutex.OwnerThreadId == first.ThreadId && mutex.RecursionCount == 1, "Mutex ownership was not transferred atomically.");
mutex.OwnerThreadId = 0;
mutex.RecursionCount = 0;
Debug.Assert(TryGrantMutexWaiterLocked(mutex, second), "The second mutex waiter was not granted after release.");
}
var cond = new PthreadCondState();
var condMutex = new PthreadMutexState();
var condWaiter = new PthreadCondWaiter
{
ThreadId = 0x303,
MutexState = condMutex,
WakeKey = string.Empty,
Cooperative = false,
};
lock (cond.SyncRoot)
{
condWaiter.Node = cond.WaiterQueue.AddLast(condWaiter);
cond.Waiters++;
Debug.Assert(CompleteCondWaiterLocked(cond, condWaiter, timedOut: false), "A condition waiter was not completed.");
Debug.Assert(cond.WaiterQueue.Count == 0 && cond.Waiters == 0 && condWaiter.MutexWaiter is not null, "Condition completion did not atomically queue mutex reacquisition.");
}
}
private static bool TryGrantMutexWaiterLocked(PthreadMutexState state, PthreadMutexWaiter waiter)
{
if (Volatile.Read(ref waiter.Granted) != 0)
{
return true;
}
if (state.OwnerThreadId != 0 ||
waiter.Node is null ||
!ReferenceEquals(state.Waiters.First, waiter.Node))
{
return false;
}
state.Waiters.Remove(waiter.Node);
waiter.Node = null;
state.OwnerThreadId = waiter.ThreadId;
state.RecursionCount = 1;
Volatile.Write(ref waiter.Granted, 1);
Monitor.PulseAll(state);
return true;
}
private static int WaitForHostMutexLock(PthreadMutexState state, PthreadMutexWaiter waiter)
{
lock (state)
{
while (!TryGrantMutexWaiterLocked(state, waiter))
{
Monitor.Wait(state);
}
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool TryGrantBlockedMutexLock(
CpuContext ctx,
ulong mutexAddress,
ulong resolvedAddress,
PthreadMutexState state,
PthreadMutexWaiter waiter)
{
var granted = false;
lock (state)
{
granted = TryGrantMutexWaiterLocked(state, waiter);
}
TracePthreadMutex(
ctx,
granted ? "lock-reserve" : "lock-reserve-busy",
mutexAddress,
resolvedAddress,
state,
waiter.ThreadId,
granted ? (int)OrbisGen2Result.ORBIS_GEN2_OK : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
return granted;
}
private static int CompleteBlockedMutexLock(
CpuContext ctx,
ulong mutexAddress,
ulong resolvedAddress,
PthreadMutexState state,
PthreadMutexWaiter waiter)
{
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
if (Volatile.Read(ref waiter.Granted) == 1)
{
TracePthreadMutex(ctx, "lock-resume", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
TracePthreadMutex(ctx, "lock-resume-ungranted", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
}
private static bool CompleteCondWaiterLocked(
PthreadCondState state,
PthreadCondWaiter waiter,
bool timedOut)
{
if (waiter.CompletionState != 0)
{
return false;
}
waiter.CompletionState = timedOut ? 2 : 1;
RemoveCondWaiterLocked(state, waiter);
waiter.TimeoutTimer?.Dispose();
waiter.TimeoutTimer = null;
lock (waiter.MutexState)
{
waiter.MutexWaiter = EnqueueMutexWaiterLocked(
waiter.MutexState,
waiter.ThreadId,
waiter.Cooperative,
waiter.WakeKey);
}
Monitor.PulseAll(state.SyncRoot);
return true;
}
private static void CompleteCondWaiter(
PthreadCondState state,
PthreadCondWaiter waiter,
bool timedOut)
{
var completed = false;
lock (state.SyncRoot)
{
completed = CompleteCondWaiterLocked(state, waiter, timedOut);
}
if (completed)
{
WakeCooperativeWaiter(waiter);
}
}
private static void RemoveCondWaiterLocked(PthreadCondState state, PthreadCondWaiter waiter)
{
if (waiter.Node is not null)
{
state.WaiterQueue.Remove(waiter.Node);
waiter.Node = null;
state.Waiters = Math.Max(0, state.Waiters - 1);
}
}
private static bool TryGrantCondWaiterMutex(PthreadCondWaiter waiter)
{
var mutexWaiter = waiter.MutexWaiter;
if (waiter.CompletionState == 0 || mutexWaiter is null)
{
return false;
}
lock (waiter.MutexState)
{
return TryGrantMutexWaiterLocked(waiter.MutexState, mutexWaiter);
}
}
private static int CompleteBlockedCondWait(
CpuContext ctx,
ulong condAddress,
ulong mutexAddress,
PthreadCondState state,
PthreadCondWaiter waiter)
{
waiter.TimeoutTimer?.Dispose();
waiter.TimeoutTimer = null;
var result = waiter.MutexWaiter is not null &&
Volatile.Read(ref waiter.MutexWaiter.Granted) == 1
? (waiter.CompletionState == 2
? CondTimedOutResult(waiter)
: (int)OrbisGen2Result.ORBIS_GEN2_OK)
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
TracePthreadCond(
waiter.CompletionState == 2 ? "wait-resume-timeout" : "wait-resume",
condAddress,
mutexAddress,
state,
waiter.CompletionState == 2,
result);
_ = ctx;
return result;
}
private static int CondTimedOutResult(PthreadCondWaiter waiter) =>
waiter.PosixErrors
? 60 // ETIMEDOUT on Orbis/FreeBSD; pthread APIs return errno directly.
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
private static void WakeCooperativeWaiter(PthreadCondWaiter waiter)
{
if (waiter.Cooperative)
{
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(waiter.WakeKey, 1);
}
}
private static TimeSpan GetCondWaitTimeout(uint timeoutUsec)
{
if (timeoutUsec == 0)
@@ -91,6 +91,10 @@ public static class KernelPthreadExtendedCompatExports
public PthreadAttrState Attributes { get; set; } = PthreadAttrState.Default;
}
// On the outer class deliberately: a static on the nested state class gives it a type
// initializer that first runs on a guest thread and fail-fasts the CLR.
private static long _nextRwlockWakeId;
private sealed class PthreadRwlockState
{
public object SyncRoot { get; } = new();
@@ -101,6 +105,8 @@ public static class KernelPthreadExtendedCompatExports
public ulong WriterThreadId { get; set; }
public int WaitingWriters { get; set; }
// See PthreadMutexState.WakeKey.
public string WakeKey { get; } = "pthread_rwlock#" + Interlocked.Increment(ref _nextRwlockWakeId).ToString("X");
public int GetReaderCount(ulong threadId)
{
@@ -162,6 +168,17 @@ public static class KernelPthreadExtendedCompatExports
}
}
private sealed class RwlockWaiter : IGuestThreadBlockWaiter
{
public required PthreadRwlockState Rwlock { get; init; }
public required ulong ThreadId { get; init; }
public required bool Write { get; init; }
public int Resume() => (int)OrbisGen2Result.ORBIS_GEN2_OK;
public bool TryWake() => TryAcquireBlockedRwlock(Rwlock, ThreadId, Write);
}
private readonly record struct TlsKeyState(ulong Destructor);
private readonly record struct PthreadAttrState(
@@ -1167,6 +1184,7 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
}
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(rwlock.WakeKey);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -1461,30 +1479,35 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// In-place block: Monitor.Wait releases SyncRoot and parks
// atomically, so an unlock's PulseAll cannot be lost. Sliced
// only so teardown can unwind parked threads.
rwlock.WaitingWriters++;
GuestThreadBlocking.NoteBlocked(currentThreadId, "pthread_rwlock_wrlock");
var transferredToScheduler = false;
try
{
if (GuestThreadExecution.IsGuestThread &&
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"pthread_rwlock_wrlock",
rwlock.WakeKey,
new RwlockWaiter { Rwlock = rwlock, ThreadId = currentThreadId, Write = true }))
{
transferredToScheduler = true;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
while (rwlock.WriterThreadId != 0 || rwlock.ReaderTotalCount != 0 || rwlock.CompatWriterTotalCount != 0)
{
if (GuestThreadBlocking.ShutdownRequested)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
}
GuestThreadBlocking.Checkpoint(currentThreadId, rwlock.SyncRoot);
_ = Monitor.Wait(rwlock.SyncRoot, GuestThreadBlocking.WaitSliceMilliseconds);
Monitor.Wait(rwlock.SyncRoot);
}
rwlock.WriterThreadId = currentThreadId;
}
finally
{
rwlock.WaitingWriters = Math.Max(0, rwlock.WaitingWriters - 1);
GuestThreadBlocking.NoteUnblocked(currentThreadId);
if (!transferredToScheduler)
{
rwlock.WaitingWriters = Math.Max(0, rwlock.WaitingWriters - 1);
}
}
}
else
@@ -1494,26 +1517,20 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
}
if (ReaderMustWaitForRwlock(rwlock, currentThreadId))
while (ReaderMustWaitForRwlock(rwlock, currentThreadId))
{
GuestThreadBlocking.NoteBlocked(currentThreadId, "pthread_rwlock_rdlock");
try
if (GuestThreadExecution.IsGuestThread &&
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"pthread_rwlock_rdlock",
rwlock.WakeKey,
new RwlockWaiter { Rwlock = rwlock, ThreadId = currentThreadId, Write = false }))
{
while (ReaderMustWaitForRwlock(rwlock, currentThreadId))
{
if (GuestThreadBlocking.ShutdownRequested)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
GuestThreadBlocking.Checkpoint(currentThreadId, rwlock.SyncRoot);
_ = Monitor.Wait(rwlock.SyncRoot, GuestThreadBlocking.WaitSliceMilliseconds);
}
}
finally
{
GuestThreadBlocking.NoteUnblocked(currentThreadId);
}
Monitor.Wait(rwlock.SyncRoot);
}
if (rwlock.WriterThreadId != 0 ||
@@ -1530,6 +1547,33 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool TryAcquireBlockedRwlock(PthreadRwlockState rwlock, ulong currentThreadId, bool write)
{
lock (rwlock.SyncRoot)
{
if (write)
{
if (rwlock.WriterThreadId != 0 || rwlock.ReaderTotalCount != 0 || rwlock.CompatWriterTotalCount != 0)
{
return false;
}
DetectRwlockWriterConflict(0, rwlock, currentThreadId, "wrlock-resume");
rwlock.WriterThreadId = currentThreadId;
rwlock.WaitingWriters = Math.Max(0, rwlock.WaitingWriters - 1);
return true;
}
if (ReaderMustWaitForRwlock(rwlock, currentThreadId))
{
return false;
}
rwlock.AddReader(currentThreadId);
return true;
}
}
// Call while holding lock(rwlock.SyncRoot): an existing reader/writer here means a
// writer would share the rwlock with another holder — a data race.
private static void DetectRwlockWriterConflict(ulong resolvedAddress, PthreadRwlockState rwlock, ulong currentThreadId, string site)
@@ -1560,6 +1604,8 @@ public static class KernelPthreadExtendedCompatExports
rwlock.GetReaderCount(currentThreadId) == 0;
}
private static string GetRwlockWakeKey(ulong rwlockAddress) => $"pthread_rwlock:0x{rwlockAddress:X16}";
public static string? DumpRwlockStateForStall(ulong rwlockAddress)
{
PthreadRwlockState? rwlock;
@@ -97,6 +97,8 @@ public static class KernelRuntimeCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
GuestThreadExecution.Scheduler?.Pump(ctx, "sceKernelUsleep");
if (micros < 1000)
{
// Guest worker pools use usleep(1) as a polling backoff. Do not turn
@@ -1302,30 +1304,6 @@ public static class KernelRuntimeCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// Same (pc, flags, out-info) contract as sceKernelGetModuleInfoForUnwind,
// surfaced through libSceSysmodule on Gen5; the unwinder threads whichever
// one the module's libc was linked against.
[SysAbiExport(
Nid = "4fU5yvOkVG4",
ExportName = "sceSysmoduleGetModuleInfoForUnwind",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSysmodule")]
public static int SysmoduleGetModuleInfoForUnwind(CpuContext ctx) => KernelGetModuleInfoForUnwind(ctx);
// libc unwinder predicate: is this PC the kernel signal-return trampoline?
// Guest signal returns do not run through a guest-visible trampoline here,
// so no PC is ever one — report false and let the frame unwind normally.
[SysAbiExport(
Nid = "crb5j7mkk1c",
ExportName = "_is_signal_return",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int IsSignalReturn(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "nu4a0-arQis",
ExportName = "sceKernelAioInitializeParam",
@@ -2119,6 +2097,7 @@ public static class KernelRuntimeCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
GuestThreadExecution.Scheduler?.Pump(ctx, posix ? "nanosleep" : "sceKernelNanosleep");
var totalTicks = tvSec * TimeSpan.TicksPerSecond + Math.Max(tvNsec / 100L, 1L);
try
{
@@ -17,6 +17,8 @@ public static class KernelSemaphoreCompatExports
private sealed class KernelSemaphoreState
{
public required string Name { get; init; }
// Formatted once at creation; signal/wait/cancel/delete all wake through this key.
public required string WakeKey { get; init; }
public required int InitialCount { get; init; }
public required int MaxCount { get; init; }
public int Count { get; set; }
@@ -63,6 +65,7 @@ public static class KernelSemaphoreCompatExports
_semaphores[handle] = new KernelSemaphoreState
{
Name = name,
WakeKey = GetSemaphoreWakeKey(handle),
InitialCount = initialCount,
MaxCount = maxCount,
Count = initialCount,
@@ -108,79 +111,148 @@ public static class KernelSemaphoreCompatExports
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
// In-place block on the semaphore gate. Monitor.Wait releases the gate
// and parks atomically, so a concurrent SignalSema's PulseAll cannot be
// lost between the count check and the park. Semantics mirror FreeBSD
// ksem / sem_wait: acquire when count>=need, else sleep until posted or
// the deadline lapses. Waits are sliced only so teardown can unwind.
var deadlineMs = timeoutAddress != 0
? Environment.TickCount64 + Math.Max(1L, timeoutUsec / 1000L)
: long.MaxValue;
lock (semaphore.Gate)
{
if (semaphore.Count < needCount)
if (semaphore.Count >= needCount)
{
semaphore.WaitingThreads++;
semaphore.Count -= needCount;
if (timeoutAddress != 0)
{
_ = TryWriteUInt32(ctx, timeoutAddress, timeoutUsec);
}
if (_traceSema)
{
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)} waiters={semaphore.WaitingThreads} {FormatCallSite(ctx)}");
}
var guestThreadHandle = GuestThreadExecution.CurrentGuestThreadHandle;
GuestThreadBlocking.NoteBlocked(guestThreadHandle, "sceKernelWaitSema");
try
{
while (semaphore.Count < needCount)
{
if (GuestThreadBlocking.ShutdownRequested)
{
if (timeoutAddress != 0)
{
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
}
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
var remaining = deadlineMs - Environment.TickCount64;
if (timeoutAddress != 0 && remaining <= 0)
{
if (_traceSema)
{
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
}
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
GuestThreadBlocking.Checkpoint(guestThreadHandle, semaphore.Gate);
_ = Monitor.Wait(semaphore.Gate, (int)Math.Min(remaining, GuestThreadBlocking.WaitSliceMilliseconds));
}
}
finally
{
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
GuestThreadBlocking.NoteUnblocked(guestThreadHandle);
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)}");
}
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
semaphore.Count -= needCount;
semaphore.WaitingThreads++;
}
// Block cooperatively: the wake predicate atomically acquires the
// tokens (so a wake commits the acquisition), while the resume
// handler distinguishes a real acquisition from a deadline expiry.
var acquired = false;
var deadline = timeoutAddress != 0
? GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromMicroseconds(timeoutUsec))
: 0;
bool WakePredicate()
{
lock (semaphore.Gate)
{
if (semaphore.Count >= needCount)
{
semaphore.Count -= needCount;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
acquired = true;
return true;
}
return false;
}
}
int ResumeWait()
{
if (timeoutAddress != 0)
{
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
}
if (acquired)
{
if (_traceSema)
{
TraceSemaphore($"wait-wake handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
lock (semaphore.Gate)
{
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
}
if (_traceSema)
{
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)}");
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
}
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
}
if (GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitSema",
GetSemaphoreWakeKey(handle),
ResumeWait,
WakePredicate,
deadline))
{
if (_traceSema)
{
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)} waiters={semaphore.WaitingThreads} {FormatCallSite(ctx)}");
}
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
// Not a guest thread (or no scheduler): fall back to a host-thread
// wait so the semantics still hold on non-cooperative callers.
return WaitSemaphoreOnHostThread(ctx, semaphore, handle, needCount, timeoutAddress, timeoutUsec);
}
private static int WaitSemaphoreOnHostThread(
CpuContext ctx,
KernelSemaphoreState semaphore,
uint handle,
int needCount,
ulong timeoutAddress,
uint timeoutUsec)
{
var deadlineMs = timeoutAddress != 0
? Environment.TickCount64 + Math.Max(1L, timeoutUsec / 1000L)
: long.MaxValue;
lock (semaphore.Gate)
{
if (_traceSema)
{
TraceSemaphore(
$"wait-host-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} " +
$"count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)} {FormatCallSite(ctx)}");
}
while (semaphore.Count < needCount)
{
var remaining = deadlineMs - Environment.TickCount64;
if (timeoutAddress != 0 && remaining <= 0)
{
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
Monitor.Wait(semaphore.Gate, (int)Math.Min(remaining, 100));
}
semaphore.Count -= needCount;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
if (_traceSema)
{
TraceSemaphore(
$"wait-host-wake handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} {FormatCallSite(ctx)}");
}
if (timeoutAddress != 0)
{
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
}
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
}
private static string GetSemaphoreWakeKey(uint handle) => $"sceKernelWaitSema:{handle:X8}";
[SysAbiExport(
Nid = "12wOHk8ywb0",
ExportName = "sceKernelPollSema",
@@ -243,7 +315,7 @@ public static class KernelSemaphoreCompatExports
}
semaphore.Count += signalCount;
// Wake threads parked in-place on the gate; each re-checks the count.
// Wake host-thread waiters parked in the fallback path.
Monitor.PulseAll(semaphore.Gate);
if (_traceSema)
{
@@ -251,6 +323,9 @@ public static class KernelSemaphoreCompatExports
}
}
// Wake cooperatively-blocked guest threads; their wake predicate
// acquires the tokens atomically, so this respects the new count.
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
@@ -279,6 +354,7 @@ public static class KernelSemaphoreCompatExports
}
semaphore.Count = setCount < 0 ? semaphore.InitialCount : setCount;
semaphore.WaitingThreads = 0;
Monitor.PulseAll(semaphore.Gate);
if (_traceSema)
{
@@ -286,6 +362,7 @@ public static class KernelSemaphoreCompatExports
}
}
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
@@ -333,6 +410,7 @@ public static class KernelSemaphoreCompatExports
_semaphores[handle] = new KernelSemaphoreState
{
Name = $"posix@0x{semaphoreAddress:X16}",
WakeKey = GetSemaphoreWakeKey(handle),
InitialCount = initialCount,
MaxCount = int.MaxValue,
Count = initialCount,
@@ -350,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",
@@ -384,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",
@@ -408,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",
@@ -457,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",
@@ -514,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,127 +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).
//
// Waits block in place on a per-address gate (see GuestThreadBlocking):
// Monitor.Wait releases the gate and parks atomically, so a wake's generation
// bump + PulseAll cannot be lost between the generation check and the park.
// 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 bounded by a self-heal deadline and treated as a
// spurious-wakeup-tolerant park: the guest re-checks its own condition after
// resuming, which futex callers already tolerate. Wake-one degrades to
// wake-all for the same reason (each resumed waiter re-evaluates).
public static class KernelSyncOnAddressCompatExports
{
// Safety-net bound. Real releases come from the wake side; this only limits
// how long a wait that genuinely raced/missed its wake stays parked before
// the guest re-evaluates. Kept large: a short bound turns every parked
// waiter into a hot re-poll that steals CPU from the threads 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);
private static readonly ConcurrentDictionary<ulong, object> _addressGates = new();
// Per-address wake generation. A wait captures the current generation and
// stays parked while it is unchanged; a wake bumps it first, then pulses
// the gate, so a wait between its generation check and its park still
// observes the bump (the check happens under the gate).
private static readonly ConcurrentDictionary<ulong, long> _wakeGenerations = new();
private static long CurrentGeneration(ulong address) =>
_wakeGenerations.TryGetValue(address, out var generation) ? generation : 0;
[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 gate = _addressGates.GetOrAdd(address, static _ => new object());
var deadlineMs = Environment.TickCount64 + (long)WaitSelfHealTimeout.TotalMilliseconds;
var guestThreadHandle = GuestThreadExecution.CurrentGuestThreadHandle;
GuestThreadBlocking.NoteBlocked(guestThreadHandle, "sceKernelSyncOnAddressWait");
try
{
lock (gate)
{
while (CurrentGeneration(address) == observedGeneration &&
!GuestThreadBlocking.ShutdownRequested)
{
var remaining = deadlineMs - Environment.TickCount64;
if (remaining <= 0)
{
// Self-heal: resume and let the guest re-check its condition.
break;
}
GuestThreadBlocking.Checkpoint(guestThreadHandle, gate);
_ = Monitor.Wait(gate, (int)Math.Min(remaining, GuestThreadBlocking.WaitSliceMilliseconds));
}
}
}
finally
{
GuestThreadBlocking.NoteUnblocked(guestThreadHandle);
}
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);
}
// Bump the generation first so a wait that has checked but not yet
// parked (it holds the gate for both) observes the change; then pulse
// parked waiters. rsi's wake count degrades to wake-all — resumed
// waiters re-evaluate their own condition, which futex callers tolerate.
_wakeGenerations.AddOrUpdate(address, 1, static (_, current) => current + 1);
if (_addressGates.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;
}
}
+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}";
}
}
+150 -346
View File
@@ -2412,9 +2412,7 @@ internal static unsafe class VulkanVideoPresenter
}
}
private static bool TryTakeGuestWork(
out PendingGuestWork work,
HashSet<string>? excludedQueues = null)
private static bool TryTakeGuestWork(out PendingGuestWork work)
{
lock (_gate)
{
@@ -2427,14 +2425,6 @@ internal static unsafe class VulkanVideoPresenter
}
var queueName = _pendingGuestQueueSchedule[_pendingGuestQueueCursor];
if (excludedQueues?.Contains(queueName) == true)
{
_pendingGuestQueueCursor =
(_pendingGuestQueueCursor + 1) % _pendingGuestQueueSchedule.Count;
queuesToProbe--;
continue;
}
if (!_pendingGuestWorkByQueue.TryGetValue(queueName, out var queue) ||
queue.First is not { } first)
{
@@ -2476,32 +2466,6 @@ internal static unsafe class VulkanVideoPresenter
}
}
private static bool RequeueGuestWorkFront(in PendingGuestWork work)
{
lock (_gate)
{
if (_closed)
{
return false;
}
if (!_pendingGuestWorkByQueue.TryGetValue(work.Queue.Name, out var queue))
{
queue = new LinkedList<PendingGuestWork>();
_pendingGuestWorkByQueue.Add(work.Queue.Name, queue);
_pendingGuestQueueSchedule.Add(work.Queue.Name);
}
// TryTakeGuestWork removes only the item count. Payload ownership
// remains live until CompleteGuestWork, so requeueing must not add
// the retained-byte total a second time.
queue.AddFirst(work);
_pendingGuestWorkCount++;
System.Threading.Monitor.PulseAll(_gate);
return true;
}
}
private static void CompleteGuestWork(in PendingGuestWork pending)
{
SharpEmu.HLE.GuestImageWriteTracker.FlushPendingDiagnostics();
@@ -2911,14 +2875,11 @@ internal static unsafe class VulkanVideoPresenter
DescriptorSetLayout DescriptorSetLayout,
PipelineLayout PipelineLayout);
private readonly record struct DirtyGuestBufferRange(
ulong Offset,
ulong Length,
string QueueName,
ulong Timeline);
private readonly record struct DirtyGuestBufferRange(ulong Offset, ulong Length);
private sealed class GuestBufferAllocation
{
public string QueueName = VulkanGuestQueueIdentity.Default.Name;
public ulong BaseAddress;
public ulong Size;
public VkBuffer Buffer;
@@ -2929,6 +2890,8 @@ internal static unsafe class VulkanVideoPresenter
public List<DirtyGuestBufferRange> DirtyRanges { get; } = [];
}
private const string SharedReadOnlyGuestBufferQueue = "shared.readonly";
private sealed class TranslatedDrawResources
{
public string DebugName = "SharpEmu translated";
@@ -4832,9 +4795,7 @@ internal static unsafe class VulkanVideoPresenter
MarkGuestBufferDirty(
allocation,
globalBuffer.GuestOffset,
globalBuffer.GuestSize,
_activeGuestQueue.Name,
_submitTimeline);
globalBuffer.GuestSize);
}
}
}
@@ -5030,7 +4991,7 @@ internal static unsafe class VulkanVideoPresenter
}
}
private bool TryMakeActiveGuestQueueSubmissionsCpuVisible()
private void WaitForActiveGuestQueueSubmissionsForCpuVisibility()
{
FlushBatchedGuestCommands();
if (!_lastSubmittedTimelineByGuestQueue.TryGetValue(
@@ -5038,7 +4999,7 @@ internal static unsafe class VulkanVideoPresenter
out var targetTimeline) ||
targetTimeline <= _completedTimeline)
{
return true;
return;
}
PendingGuestSubmission? target = null;
@@ -5058,77 +5019,27 @@ internal static unsafe class VulkanVideoPresenter
$"{targetTimeline} (completed={_completedTimeline}).");
}
var waitStart = System.Diagnostics.Stopwatch.GetTimestamp();
var fence = target.Fence;
var status = _vk.GetFenceStatus(_device, fence);
if (status == Result.NotReady)
{
return false;
}
if (status == Result.ErrorDeviceLost)
{
_deviceLost = true;
return true;
}
Check(status, $"vkGetFenceStatus(queue visibility: {_activeGuestQueue.Name})");
Check(
_vk.WaitForFences(_device, 1, &fence, true, ulong.MaxValue),
$"vkWaitForFences(queue visibility: {_activeGuestQueue.Name})");
CollectCompletedGuestSubmissions(waitForOldest: false);
var waitedMs = (System.Diagnostics.Stopwatch.GetTimestamp() - waitStart) *
1000.0 / System.Diagnostics.Stopwatch.Frequency;
if (_traceVulkanShaderEnabled)
{
TraceVulkanShader(
$"vk.queue_visibility queue={_activeGuestQueue.Name} " +
$"submission={_activeGuestQueue.SubmissionId} " +
$"target_timeline={targetTimeline} completed_timeline={_completedTimeline}");
$"target_timeline={targetTimeline} completed_timeline={_completedTimeline} " +
$"waited_ms={waitedMs:F3}");
}
return true;
}
private void WaitForGuestBufferAllocationForCpuVisibility(
GuestBufferAllocation allocation)
private void ExecuteOrderedGuestAction(VulkanOrderedGuestAction work)
{
if (IsGuestBufferAllocationReferencedByOpenBatch(allocation))
{
FlushBatchedGuestCommands();
}
var targetTimeline = allocation.LastUseTimeline;
if (targetTimeline <= _completedTimeline)
{
return;
}
PendingGuestSubmission? target = null;
foreach (var submission in _pendingGuestSubmissions)
{
if (submission.Timeline == targetTimeline)
{
target = submission;
break;
}
}
if (target is null)
{
throw new InvalidOperationException(
$"Guest buffer 0x{allocation.BaseAddress:X16} lost pending timeline " +
$"{targetTimeline} (completed={_completedTimeline}).");
}
var fence = target.Fence;
Check(
_vk.WaitForFences(_device, 1, &fence, true, ulong.MaxValue),
$"vkWaitForFences(buffer visibility: 0x{allocation.BaseAddress:X16})");
CollectCompletedGuestSubmissions(waitForOldest: false);
}
private bool TryExecuteOrderedGuestAction(VulkanOrderedGuestAction work)
{
if (!TryMakeActiveGuestQueueSubmissionsCpuVisible())
{
return false;
}
WaitForActiveGuestQueueSubmissionsForCpuVisibility();
WriteBackAllDirtyGuestBuffers(_activeGuestQueue.Name);
work.Action();
if (_traceVulkanShaderEnabled)
@@ -5138,8 +5049,6 @@ internal static unsafe class VulkanVideoPresenter
$"submission={_activeGuestQueue.SubmissionId} " +
$"work_sequence={_activeGuestWorkSequence} name='{work.DebugName}'");
}
return true;
}
private void ExecuteOrderedGuestFlip(VulkanOrderedGuestFlip work)
@@ -8277,6 +8186,9 @@ internal static unsafe class VulkanVideoPresenter
var size = (ulong)Math.Max(guestBuffer.Length, sizeof(uint));
var endAddress = checked(guestBuffer.BaseAddress + size);
GuestBufferAllocation? allocation = null;
var allocationPriority = -1;
// This runs for every bound global buffer. Preserve the previous
// stable queue preference without allocating LINQ sort state.
foreach (var candidate in _guestBufferAllocations)
{
if (candidate.BaseAddress > guestBuffer.BaseAddress ||
@@ -8285,8 +8197,22 @@ internal static unsafe class VulkanVideoPresenter
continue;
}
allocation = candidate;
break;
var candidatePriority = string.Equals(
candidate.QueueName,
_activeGuestQueue.Name,
StringComparison.Ordinal)
? 2
: string.Equals(
candidate.QueueName,
SharedReadOnlyGuestBufferQueue,
StringComparison.Ordinal)
? 1
: 0;
if (candidatePriority > allocationPriority)
{
allocation = candidate;
allocationPriority = candidatePriority;
}
}
if (allocation is null)
@@ -8321,7 +8247,11 @@ internal static unsafe class VulkanVideoPresenter
var shadow = allocation.Shadow.AsSpan(checked((int)guestOffset), guestBuffer.Length);
if (!source.SequenceEqual(shadow))
{
if (!guestBuffer.Writable &&
var sharedReadOnly = string.Equals(
allocation.QueueName,
SharedReadOnlyGuestBufferQueue,
StringComparison.Ordinal);
if (sharedReadOnly &&
(allocation.LastUseTimeline > _completedTimeline ||
IsGuestBufferAllocationReferencedByOpenBatch(allocation)))
{
@@ -8335,8 +8265,14 @@ internal static unsafe class VulkanVideoPresenter
// an in-flight shader access. Retire prior users, publish their
// dirty ranges to guest memory, then upload the current guest
// bytes (which may be newer than the parser's captured array).
WaitForGuestBufferAllocationForCpuVisibility(allocation);
WriteBackAllDirtyGuestBuffers();
if (!sharedReadOnly)
{
// Writable aliases are private to one logical guest queue.
// Retiring unrelated queues here recreates the global FIFO
// and turns routine buffer refreshes into queue-wide stalls.
WaitForActiveGuestQueueSubmissionsForCpuVisibility();
WriteBackAllDirtyGuestBuffers(_activeGuestQueue.Name);
}
// Populate the cached shadow copy first and write it out to the
// mapped allocation in one pass. The mapped memory is
// HOST_VISIBLE|HOST_COHERENT (write-combined on most drivers),
@@ -8482,7 +8418,7 @@ internal static unsafe class VulkanVideoPresenter
return;
}
var ranges = new List<(ulong Start, ulong End)>(buffers.Count);
var ranges = new List<(ulong Start, ulong End, bool Writable)>(buffers.Count);
foreach (var buffer in buffers)
{
if (buffer.BaseAddress == 0)
@@ -8496,7 +8432,8 @@ internal static unsafe class VulkanVideoPresenter
var paddedEnd = checked(buffer.BaseAddress + size + 3) & ~3UL;
ranges.Add((
alignedStart,
paddedEnd));
paddedEnd,
buffer.Writable && buffer.WriteBackToGuest));
}
if (ranges.Count == 0)
@@ -8505,7 +8442,7 @@ internal static unsafe class VulkanVideoPresenter
}
ranges.Sort(static (left, right) => left.Start.CompareTo(right.Start));
var merged = new List<(ulong Start, ulong End)>(ranges.Count);
var merged = new List<(ulong Start, ulong End, bool Writable)>(ranges.Count);
foreach (var range in ranges)
{
if (merged.Count == 0 || range.Start > merged[^1].End)
@@ -8517,18 +8454,25 @@ internal static unsafe class VulkanVideoPresenter
var previous = merged[^1];
merged[^1] = (
previous.Start,
Math.Max(previous.End, range.End));
Math.Max(previous.End, range.End),
previous.Writable || range.Writable);
}
foreach (var range in merged)
{
EnsureGuestBufferAllocation(range.Start, range.End);
EnsureGuestBufferAllocation(
range.Start,
range.End,
range.Writable
? _activeGuestQueue.Name
: SharedReadOnlyGuestBufferQueue);
}
}
private void EnsureGuestBufferAllocation(
ulong requestedStart,
ulong requestedEnd)
ulong requestedEnd,
string queueName)
{
var start = requestedStart;
var end = requestedEnd;
@@ -8537,6 +8481,10 @@ internal static unsafe class VulkanVideoPresenter
{
overlaps = _guestBufferAllocations
.Where(allocation =>
string.Equals(
allocation.QueueName,
queueName,
StringComparison.Ordinal) &&
allocation.BaseAddress < end &&
start < allocation.BaseAddress + allocation.Size)
.ToList();
@@ -8573,7 +8521,7 @@ internal static unsafe class VulkanVideoPresenter
WriteBackAllDirtyGuestBuffers();
}
var replacement = CreateGuestBufferAllocation(start, end);
var replacement = CreateGuestBufferAllocation(start, end, queueName);
foreach (var overlap in overlaps)
{
_guestBufferAllocations.Remove(overlap);
@@ -8582,27 +8530,22 @@ internal static unsafe class VulkanVideoPresenter
_guestBufferAllocations.Add(replacement);
_guestBufferAllocations.Sort(static (left, right) =>
left.BaseAddress.CompareTo(right.BaseAddress));
UpdateGuestBufferCacheMetric();
TraceVulkanShader(
$"vk.guest_buffer_allocation base=0x{start:X16} bytes={replacement.Size} " +
$"merged={overlaps.Count}");
}
private void UpdateGuestBufferCacheMetric()
{
var bytes = 0UL;
foreach (var allocation in _guestBufferAllocations)
{
bytes = checked(bytes + allocation.Size);
}
PerfOverlay.SetGuestBufferCacheBytes(bytes);
var queueOrder = string.CompareOrdinal(left.QueueName, right.QueueName);
return queueOrder != 0
? queueOrder
: left.BaseAddress.CompareTo(right.BaseAddress);
});
TraceVulkanShader(
$"vk.guest_buffer_allocation queue={queueName} " +
$"base=0x{start:X16} bytes={replacement.Size} " +
$"merged={overlaps.Count}");
}
private GuestBufferAllocation CreateGuestBufferAllocation(
ulong start,
ulong end)
ulong end,
string queueName)
{
var size = checked(end - start);
if (size == 0 || size > int.MaxValue)
@@ -8628,6 +8571,7 @@ internal static unsafe class VulkanVideoPresenter
$"SharpEmu guest VA 0x{start:X16}-0x{end:X16}");
return new GuestBufferAllocation
{
QueueName = queueName,
BaseAddress = start,
Size = size,
Buffer = buffer,
@@ -9633,6 +9577,15 @@ internal static unsafe class VulkanVideoPresenter
MarkSampledImagesInitialized(resources);
MarkStorageImagesInitialized(resources, traceContents: false);
if (work.WritesGlobalMemory)
{
// The CPU submit thread may immediately consume an indirect
// argument written by this dispatch. Wait for the specific
// guest fences and publish only dirty ranges; a queue-wide
// idle unnecessarily serialized presentation work too.
WaitForActiveGuestQueueSubmissionsForCpuVisibility();
WriteBackAllDirtyGuestBuffers(_activeGuestQueue.Name);
}
TraceVulkanShader(
$"vk.compute_dispatch groups={work.GroupCountX}x" +
$"{work.GroupCountY}x{work.GroupCountZ} " +
@@ -9862,9 +9815,7 @@ internal static unsafe class VulkanVideoPresenter
private static void MarkGuestBufferDirty(
GuestBufferAllocation allocation,
ulong offset,
ulong length,
string queueName,
ulong timeline)
ulong length)
{
if (length == 0)
{
@@ -9876,11 +9827,6 @@ internal static unsafe class VulkanVideoPresenter
for (var index = allocation.DirtyRanges.Count - 1; index >= 0; index--)
{
var existing = allocation.DirtyRanges[index];
if (!string.Equals(existing.QueueName, queueName, StringComparison.Ordinal))
{
continue;
}
var existingEnd = existing.Offset + existing.Length;
if (end < existing.Offset || existingEnd < start)
{
@@ -9889,12 +9835,10 @@ internal static unsafe class VulkanVideoPresenter
start = Math.Min(start, existing.Offset);
end = Math.Max(end, existingEnd);
timeline = Math.Max(timeline, existing.Timeline);
allocation.DirtyRanges.RemoveAt(index);
}
allocation.DirtyRanges.Add(
new DirtyGuestBufferRange(start, end - start, queueName, timeline));
allocation.DirtyRanges.Add(new DirtyGuestBufferRange(start, end - start));
}
private void WriteBackAllDirtyGuestBuffers(string? queueName = null)
@@ -9907,51 +9851,33 @@ internal static unsafe class VulkanVideoPresenter
foreach (var allocation in _guestBufferAllocations)
{
if (queueName is not null &&
!string.Equals(allocation.QueueName, queueName, StringComparison.Ordinal))
{
continue;
}
if (allocation.DirtyRanges.Count != 0 &&
allocation.LastUseTimeline > _completedTimeline)
{
// A mapped HOST_COHERENT allocation still cannot be read
// by the CPU while a shader may be writing it. Callers
// normally retire the relevant fences first; keep this
// helper fail-closed if a future path forgets to do so.
TraceVulkanShader(
$"vk.global_writeback_deferred base=0x{allocation.BaseAddress:X16} " +
$"last_use={allocation.LastUseTimeline} completed={_completedTimeline}");
continue;
}
for (var index = allocation.DirtyRanges.Count - 1; index >= 0; index--)
{
var range = allocation.DirtyRanges[index];
if ((queueName is not null &&
!string.Equals(range.QueueName, queueName, StringComparison.Ordinal)) ||
range.Timeline > _completedTimeline)
{
continue;
}
if (range.Length == 0 || range.Length > int.MaxValue)
{
continue;
}
var rangeEnd = checked(range.Offset + range.Length);
var overlapsInFlightWrite = false;
for (var otherIndex = 0;
otherIndex < allocation.DirtyRanges.Count;
otherIndex++)
{
if (otherIndex == index)
{
continue;
}
var other = allocation.DirtyRanges[otherIndex];
if (other.Timeline <= _completedTimeline)
{
continue;
}
var otherEnd = checked(other.Offset + other.Length);
if (range.Offset < otherEnd && other.Offset < rangeEnd)
{
overlapsInFlightWrite = true;
break;
}
}
if (overlapsInFlightWrite)
{
continue;
}
var mappedBytes = new ReadOnlySpan<byte>(
(void*)(allocation.Mapped + checked((nint)range.Offset)),
checked((int)range.Length));
@@ -9981,7 +9907,6 @@ internal static unsafe class VulkanVideoPresenter
const int pageSize = 4096;
const int unreadableMergeGap = 16;
var livePageBuffer = GuestDataPool.Shared.Rent(pageSize);
var mappedPageBuffer = GuestDataPool.Shared.Rent(pageSize);
var pageRuns = new List<(int Start, int Length)>(64);
try
{
@@ -9990,49 +9915,35 @@ internal static unsafe class VulkanVideoPresenter
pageStart += pageSize)
{
var pageEnd = Math.Min(pageStart + pageSize, mappedBytes.Length);
var pageLength = pageEnd - pageStart;
var mappedPageSource = mappedBytes.Slice(pageStart, pageLength);
var shadowPage = shadowBytes.Slice(pageStart, pageLength);
if (mappedPageSource.SequenceEqual(shadowPage))
{
continue;
}
// HOST_COHERENT mappings are commonly uncached or
// write-combined on the CPU. Read each changed page
// once with a bulk copy, then perform the byte-level
// merge against ordinary cached memory.
var mappedPage = mappedPageBuffer.AsSpan(0, pageLength);
mappedPageSource.CopyTo(mappedPage);
pageRuns.Clear();
var cursor = 0;
while (cursor < pageLength)
var cursor = pageStart;
while (cursor < pageEnd)
{
while (cursor < pageLength &&
mappedPage[cursor] == shadowPage[cursor])
while (cursor < pageEnd &&
mappedBytes[cursor] == shadowBytes[cursor])
{
cursor++;
}
if (cursor == pageLength)
if (cursor == pageEnd)
{
break;
}
var runStart = cursor;
while (cursor < pageLength &&
mappedPage[cursor] != shadowPage[cursor])
while (cursor < pageEnd &&
mappedBytes[cursor] != shadowBytes[cursor])
{
cursor++;
}
var runLength = cursor - runStart;
pageRuns.Add((pageStart + runStart, runLength));
pageRuns.Add((runStart, runLength));
changedRuns++;
changedBytes += (ulong)runLength;
if (firstChangedOffset < 0)
{
firstChangedOffset = pageStart + runStart;
firstChangedOffset = runStart;
}
}
@@ -10042,12 +9953,13 @@ internal static unsafe class VulkanVideoPresenter
}
changedPages++;
var pageLength = pageEnd - pageStart;
var livePage = livePageBuffer.AsSpan(0, pageLength);
if (memory.TryRead(guestAddress + (ulong)pageStart, livePage))
{
foreach (var run in pageRuns)
{
mappedPage.Slice(run.Start - pageStart, run.Length).CopyTo(
mappedBytes.Slice(run.Start, run.Length).CopyTo(
livePage.Slice(run.Start - pageStart, run.Length));
}
@@ -10055,7 +9967,7 @@ internal static unsafe class VulkanVideoPresenter
{
foreach (var run in pageRuns)
{
mappedPage.Slice(run.Start - pageStart, run.Length).CopyTo(
mappedBytes.Slice(run.Start, run.Length).CopyTo(
shadowBytes.Slice(run.Start, run.Length));
}
@@ -10070,9 +9982,7 @@ internal static unsafe class VulkanVideoPresenter
MarkGuestBufferDirty(
allocation,
range.Offset + (ulong)run.Start,
(ulong)run.Length,
range.QueueName,
range.Timeline);
(ulong)run.Length);
}
continue;
@@ -10108,7 +10018,7 @@ internal static unsafe class VulkanVideoPresenter
overlayIndex++)
{
var run = pageRuns[overlayIndex];
mappedPage.Slice(run.Start - pageStart, run.Length).CopyTo(
mappedBytes.Slice(run.Start, run.Length).CopyTo(
mergedLive.Slice(
run.Start - mergedStart,
run.Length));
@@ -10124,7 +10034,7 @@ internal static unsafe class VulkanVideoPresenter
overlayIndex++)
{
var run = pageRuns[overlayIndex];
mappedPage.Slice(run.Start - pageStart, run.Length).CopyTo(
mappedBytes.Slice(run.Start, run.Length).CopyTo(
shadowBytes.Slice(run.Start, run.Length));
}
@@ -10141,9 +10051,7 @@ internal static unsafe class VulkanVideoPresenter
exactIndex++)
{
var run = pageRuns[exactIndex];
var changed = mappedPage.Slice(
run.Start - pageStart,
run.Length);
var changed = mappedBytes.Slice(run.Start, run.Length);
fallbackWrites++;
if (memory.TryWrite(
guestAddress + (ulong)run.Start,
@@ -10160,9 +10068,7 @@ internal static unsafe class VulkanVideoPresenter
MarkGuestBufferDirty(
allocation,
range.Offset + (ulong)run.Start,
(ulong)run.Length,
range.QueueName,
range.Timeline);
(ulong)run.Length);
}
}
}
@@ -10171,7 +10077,6 @@ internal static unsafe class VulkanVideoPresenter
finally
{
GuestDataPool.Shared.Return(livePageBuffer);
GuestDataPool.Shared.Return(mappedPageBuffer);
}
var probe = mappedBytes[..Math.Min(mappedBytes.Length, 256)];
@@ -10441,7 +10346,6 @@ internal static unsafe class VulkanVideoPresenter
}
GuestDepthResource? depth = null;
DepthFramebufferResource? depthFramebuffer = null;
var clearDepthSeparately = false;
if (ShouldAttachGuestDepth(
work.DepthTarget,
draw.RenderState.Depth) &&
@@ -10469,26 +10373,12 @@ internal static unsafe class VulkanVideoPresenter
depth.GuestClearDepth = effectiveDepthTarget.ClearDepth;
depth.ClearDepth = effectiveDepthTarget.ClearDepth;
}
clearDepthSeparately = clearDepthForDraw &&
(depth.Width < firstTarget.Width ||
depth.Height < firstTarget.Height);
if (targets.Length == 1 && !clearDepthSeparately)
if (targets.Length == 1)
{
depthFramebuffer = GetOrCreateDepthFramebuffer(firstTarget, depth);
}
}
if (depth is not null && !clearDepthSeparately)
{
// Guest color images may be allocated at their maximum
// resolution while the active viewport and DB surface use
// a smaller dynamic-rendering extent. Vulkan requires the
// framebuffer extent to fit every attachment.
extent = new Extent2D(
Math.Min(firstTarget.Width, depth.Width),
Math.Min(firstTarget.Height, depth.Height));
}
if (clearDepthForDraw)
{
// DB_RENDER_CONTROL.DEPTH_CLEAR_ENABLE makes this a DB
@@ -10522,18 +10412,17 @@ internal static unsafe class VulkanVideoPresenter
var framebuffer = depthFramebuffer?.Framebuffer ?? firstTarget.Framebuffer;
if (targets.Length > 1)
{
var attachedDepth = clearDepthSeparately ? null : depth;
(renderPass, framebuffer) = CreateRenderPassAndFramebuffer(
formats,
targets.Select(target => target.MipViews.Length > 0
? target.MipViews[0]
: target.View).ToArray(),
extent.Width,
extent.Height,
firstTarget.Width,
firstTarget.Height,
targets.Select(target =>
target.Initialized || target.InitialUploadPending).ToArray(),
attachedDepth,
attachedDepth?.Initialized == true && !clearDepthForDraw);
depth,
depth?.Initialized == true && !clearDepthForDraw);
transientRenderPass = renderPass;
transientFramebuffer = framebuffer;
}
@@ -10544,8 +10433,8 @@ internal static unsafe class VulkanVideoPresenter
formats,
extent,
targets,
hasDepthAttachment: depth is not null && !clearDepthSeparately,
feedbackDepth: clearDepthSeparately ? null : depth);
hasDepthAttachment: depth is not null,
feedbackDepth: depth);
resources.TransientRenderPass = transientRenderPass;
resources.TransientFramebuffer = transientFramebuffer;
transientRenderPass = default;
@@ -10565,10 +10454,6 @@ internal static unsafe class VulkanVideoPresenter
submitted = true;
BeginDebugLabel(_commandBuffer, resources.DebugName);
if (clearDepthSeparately && depth is not null)
{
RecordStandaloneGuestDepthClear(depth);
}
var hasStorageImages = false;
foreach (var texture in resources.Textures)
{
@@ -10632,7 +10517,6 @@ internal static unsafe class VulkanVideoPresenter
toColorAttachments);
if (depth is not null &&
!clearDepthSeparately &&
depth.Layout == ImageLayout.ShaderReadOnlyOptimal)
{
var toDepthAttachment = new ImageMemoryBarrier
@@ -10670,7 +10554,7 @@ internal static unsafe class VulkanVideoPresenter
framebuffer,
extent,
colorAttachmentCount: targets.Length,
hasDepthAttachment: depth is not null && !clearDepthSeparately,
hasDepthAttachment: depth is not null,
clearDepth: depth?.ClearDepth ?? 1f);
RecordTranslatedDrawInPass(resources, extent);
_vk.CmdEndRenderPass(_commandBuffer);
@@ -10726,10 +10610,7 @@ internal static unsafe class VulkanVideoPresenter
if (depth is not null)
{
depth.Initialized = true;
if (!clearDepthSeparately)
{
depth.Layout = ImageLayout.DepthStencilAttachmentOptimal;
}
depth.Layout = ImageLayout.DepthStencilAttachmentOptimal;
if (clearDepthForDraw)
{
depth.InitializationSource = "guest-depth-clear";
@@ -11743,6 +11624,14 @@ internal static unsafe class VulkanVideoPresenter
return existing;
}
if (depth.Width < color.Width || depth.Height < color.Height)
{
throw new InvalidOperationException(
$"guest depth 0x{depth.Address:X16} extent {depth.Width}x{depth.Height} " +
$"is smaller than color target 0x{color.Address:X16} " +
$"{color.Width}x{color.Height}");
}
var attachmentView = color.MipViews.Length > 0 ? color.MipViews[0] : color.View;
var loadRenderPass = CreateDepthRenderPass(
color.Format,
@@ -11769,8 +11658,8 @@ internal static unsafe class VulkanVideoPresenter
RenderPass = loadRenderPass,
AttachmentCount = 2,
PAttachments = attachments,
Width = Math.Min(color.Width, depth.Width),
Height = Math.Min(color.Height, depth.Height),
Width = color.Width,
Height = color.Height,
Layers = 1,
};
Check(
@@ -12266,7 +12155,6 @@ internal static unsafe class VulkanVideoPresenter
EvictDirtyCachedTextures();
var completedWork = 0;
HashSet<string>? deferredOrderedQueues = null;
var renderWorkDeadline = _renderWorkBudgetTicks > 0
? System.Diagnostics.Stopwatch.GetTimestamp() + _renderWorkBudgetTicks
: long.MaxValue;
@@ -12284,7 +12172,7 @@ internal static unsafe class VulkanVideoPresenter
break;
}
if (!TryTakeGuestWork(out var pendingGuestWork, deferredOrderedQueues))
if (!TryTakeGuestWork(out var pendingGuestWork))
{
break;
}
@@ -12311,7 +12199,6 @@ internal static unsafe class VulkanVideoPresenter
_enqueueAsImmediateQueueFollowup = true;
_immediateFollowupTail = null;
var work = pendingGuestWork.Work;
var deferGuestWork = false;
var traceWork = ShouldTracePresentedGuestImageContentsForDiagnostics();
var workStart = traceWork ? System.Diagnostics.Stopwatch.GetTimestamp() : 0L;
@@ -12339,7 +12226,7 @@ internal static unsafe class VulkanVideoPresenter
ExecuteGuestImageWrite(guestImageWrite);
break;
case VulkanOrderedGuestAction orderedAction:
deferGuestWork = !TryExecuteOrderedGuestAction(orderedAction);
ExecuteOrderedGuestAction(orderedAction);
break;
case VulkanOrderedGuestFlip orderedFlip:
ExecuteOrderedGuestFlip(orderedFlip);
@@ -12351,21 +12238,12 @@ internal static unsafe class VulkanVideoPresenter
}
finally
{
if (!deferGuestWork || !RequeueGuestWorkFront(pendingGuestWork))
{
CompleteGuestWork(pendingGuestWork);
}
CompleteGuestWork(pendingGuestWork);
_enqueueAsImmediateQueueFollowup = false;
_immediateFollowupTail = null;
Volatile.Write(ref _executingGuestWorkSequence, 0);
}
if (deferGuestWork)
{
deferredOrderedQueues ??= new HashSet<string>(StringComparer.Ordinal);
deferredOrderedQueues.Add(pendingGuestWork.Queue.Name);
}
if (workStart != 0)
{
var elapsedMs = (System.Diagnostics.Stopwatch.GetTimestamp() - workStart)
@@ -13322,79 +13200,6 @@ internal static unsafe class VulkanVideoPresenter
depth.Layout = ImageLayout.ShaderReadOnlyOptimal;
}
private void RecordStandaloneGuestDepthClear(GuestDepthResource depth)
{
var depthRange = new ImageSubresourceRange(
ImageAspectFlags.DepthBit,
0,
1,
0,
1);
var sourceStage = PipelineStageFlags.TopOfPipeBit;
var sourceAccess = AccessFlags.None;
switch (depth.Layout)
{
case ImageLayout.ShaderReadOnlyOptimal:
sourceStage =
PipelineStageFlags.VertexShaderBit |
PipelineStageFlags.FragmentShaderBit |
PipelineStageFlags.ComputeShaderBit;
sourceAccess = AccessFlags.ShaderReadBit;
break;
case ImageLayout.DepthStencilAttachmentOptimal:
sourceStage =
PipelineStageFlags.EarlyFragmentTestsBit |
PipelineStageFlags.LateFragmentTestsBit;
sourceAccess =
AccessFlags.DepthStencilAttachmentReadBit |
AccessFlags.DepthStencilAttachmentWriteBit;
break;
case ImageLayout.TransferDstOptimal:
sourceStage = PipelineStageFlags.TransferBit;
sourceAccess = AccessFlags.TransferWriteBit;
break;
}
if (depth.Layout != ImageLayout.TransferDstOptimal)
{
var toTransfer = new ImageMemoryBarrier
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = sourceAccess,
DstAccessMask = AccessFlags.TransferWriteBit,
OldLayout = depth.Layout,
NewLayout = ImageLayout.TransferDstOptimal,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = depth.Image,
SubresourceRange = depthRange,
};
_vk.CmdPipelineBarrier(
_commandBuffer,
sourceStage,
PipelineStageFlags.TransferBit,
0,
0,
null,
0,
null,
1,
&toTransfer);
}
var clearValue = new ClearDepthStencilValue(depth.ClearDepth, 0);
_vk.CmdClearDepthStencilImage(
_commandBuffer,
depth.Image,
ImageLayout.TransferDstOptimal,
&clearValue,
1,
&depthRange);
depth.Initialized = true;
depth.Layout = ImageLayout.TransferDstOptimal;
depth.InitializationSource = "guest-depth-clear";
}
private void RecordRenderTargetFeedbackSnapshots(
TranslatedDrawResources resources,
PipelineStageFlags shaderStage)
@@ -15160,7 +14965,6 @@ internal static unsafe class VulkanVideoPresenter
DestroyGuestBufferAllocation(allocation);
}
_guestBufferAllocations.Clear();
PerfOverlay.SetGuestBufferCacheBytes(0);
_hostBufferPool.Dispose();
foreach (var guestImage in _guestImages.Values)
{
@@ -953,13 +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 "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;
@@ -999,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,
@@ -1021,8 +1029,7 @@ public static partial class Gen5SpirvTranslator
return false;
}
var sourceCount = instruction.Opcode == "VPkFmaF16" ? 3 : 2;
for (var index = 0; index < sourceCount; index++)
for (var index = 0; index < 2; index++)
{
var source = instruction.Sources[index];
if (source.Kind is not (Gen5OperandKind.VectorRegister or Gen5OperandKind.ScalarRegister))
@@ -1047,12 +1054,6 @@ public static partial class Gen5SpirvTranslator
{
var left = EmitPackedF16Operand(instruction, control, 0, highLane);
var right = EmitPackedF16Operand(instruction, control, 1, highLane);
if (instruction.Opcode == "VPkFmaF16")
{
var addend = EmitPackedF16Operand(instruction, control, 2, highLane);
return EmitFloatToHalf(EmitPackedF16FusedMultiplyAdd(left, right, addend));
}
var value = instruction.Opcode switch
{
"VPkAddF16" => _module.AddInstruction(SpirvOp.FAdd, _floatType, left, right),
@@ -1064,75 +1065,6 @@ public static partial class Gen5SpirvTranslator
return EmitFloatToHalf(Bitcast(_uintType, value));
}
// 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;
}
// Reads source `index`, selects the half feeding this lane (op_sel / op_sel_hi),
// widens it exactly to f32 and applies the lane's negate modifier (neg_lo / neg_hi).
private uint EmitPackedF16Operand(
@@ -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;
@@ -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;
}
}
}
@@ -0,0 +1,112 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Reflection;
using SharpEmu.Core.Cpu.Native;
using SharpEmu.HLE;
using Xunit;
namespace SharpEmu.Libs.Tests.Cpu;
public sealed class GuestThreadBlockWaiterRepresentationTests
{
[Fact]
public void SchedulerStoresOnlyTheWaiterObjectRepresentation()
{
var stateType = typeof(DirectExecutionBackend).GetNestedType(
"GuestThreadState",
BindingFlags.NonPublic);
Assert.NotNull(stateType);
var waiterProperty = stateType.GetProperty(
"BlockWaiter",
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
Assert.NotNull(waiterProperty);
Assert.Equal(typeof(IGuestThreadBlockWaiter), waiterProperty.PropertyType);
Assert.Null(stateType.GetProperty(
"BlockResumeHandler",
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic));
Assert.Null(stateType.GetProperty(
"BlockWakeHandler",
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic));
var registerMethods = typeof(DirectExecutionBackend)
.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic)
.Where(method => method.Name == "RegisterBlockedGuestThreadContinuation")
.ToArray();
var registerMethod = Assert.Single(registerMethods);
Assert.Contains(
registerMethod.GetParameters(),
parameter => parameter.ParameterType == typeof(IGuestThreadBlockWaiter));
Assert.DoesNotContain(
registerMethod.GetParameters(),
parameter => IsFuncParameter(parameter.ParameterType));
var consumeMethods = typeof(GuestThreadExecution)
.GetMethods(BindingFlags.Static | BindingFlags.Public)
.Where(method => method.Name == nameof(GuestThreadExecution.TryConsumeCurrentThreadBlock));
Assert.DoesNotContain(
consumeMethods.SelectMany(method => method.GetParameters()),
parameter => IsFuncParameter(parameter.ParameterType));
}
[Fact]
public void DelegateCompatibilityBridgeIsConsumedAsOneWaiterObject()
{
var previousThread = GuestThreadExecution.EnterGuestThread(0x1234);
try
{
var canWake = false;
var wakeCalls = 0;
var resumeCalls = 0;
Assert.True(GuestThreadExecution.RequestCurrentThreadBlock(
context: null,
reason: "test_wait",
wakeKey: "test_waiter:1",
resumeHandler: () =>
{
resumeCalls++;
return 42;
},
wakeHandler: () =>
{
wakeCalls++;
return canWake;
}));
Assert.True(GuestThreadExecution.TryConsumeCurrentThreadBlock(
out var reason,
out _,
out var hasContinuation,
out var wakeKey,
out IGuestThreadBlockWaiter? waiter,
out var deadline));
Assert.Equal("test_wait", reason);
Assert.Equal("test_waiter:1", wakeKey);
Assert.False(hasContinuation);
Assert.Equal(0, deadline);
Assert.NotNull(waiter);
Assert.False(waiter.TryWake());
canWake = true;
Assert.True(waiter.TryWake());
Assert.Equal(42, waiter.Resume());
Assert.Equal(2, wakeCalls);
Assert.Equal(1, resumeCalls);
}
finally
{
GuestThreadExecution.RestoreGuestThread(previousThread);
}
}
private static bool IsFuncParameter(Type parameterType)
{
var type = parameterType.IsByRef
? parameterType.GetElementType()
: parameterType;
return type is not null &&
type.IsGenericType &&
type.GetGenericTypeDefinition() == typeof(Func<>);
}
}
@@ -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,56 +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;
public sealed class Sha1InstructionEmulatorTests
{
private static readonly Sha1Vector Destination = new(
0x0123_4567u, 0x89AB_CDEFu, 0x0F1E_2D3Cu, 0x4B5A_6978u);
private static readonly Sha1Vector Source = new(
0xFEDC_BA98u, 0x7654_3210u, 0xF0E1_D2C3u, 0xB4A5_9687u);
[Fact]
public void MessageSchedule1_MatchesIntelLaneSemantics()
{
Assert.Equal(
new Sha1Vector(0xF1C2_97A4u, 0x3D0E_5B68u, 0x0E3D_685Bu, 0xC2F1_A497u),
Sha1InstructionEmulator.MessageSchedule1(Destination, Source));
}
[Fact]
public void MessageSchedule2_MatchesIntelLaneSemantics()
{
Assert.Equal(
new Sha1Vector(0xECA8_6420u, 0xEEEE_EEEEu, 0xF294_3E58u, 0x7777_7777u),
Sha1InstructionEmulator.MessageSchedule2(Destination, Source));
}
[Fact]
public void NextE_MatchesIntelLaneSemantics()
{
Assert.Equal(
new Sha1Vector(0xFEDC_BA98u, 0x7654_3210u, 0xF0E1_D2C3u, 0xC77C_30E5u),
Sha1InstructionEmulator.NextE(Destination, Source));
}
[Theory]
[InlineData(0, 0x20E8_2326u, 0x911F_2CA8u, 0xECE0_593Fu, 0x0C1C_11FBu)]
[InlineData(1, 0x4598_D5B9u, 0x7BA0_0411u, 0x464E_3C51u, 0xF514_1B52u)]
[InlineData(2, 0xEE0E_73F6u, 0x2509_A67Bu, 0x26C6_85CCu, 0x0097_5845u)]
[InlineData(3, 0x9C7B_0B46u, 0xAEC8_EB49u, 0x8FD5_A2B7u, 0xFD49_9AECu)]
public void FourRounds_MatchesAllFourSha1Functions(
byte function,
uint lane0,
uint lane1,
uint lane2,
uint lane3)
{
Assert.Equal(
new Sha1Vector(lane0, lane1, lane2, lane3),
Sha1InstructionEmulator.FourRounds(Destination, Source, function));
}
}
@@ -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,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));
}
}
@@ -12,11 +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
// Every other word of the buffer must still hold the sentinel afterwards.
//
// Creating the compute pipeline doubles as a driver-acceptance check for the
@@ -40,12 +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;
unsafe
{
var spvPath = args.Length > 0
@@ -363,8 +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),
};
var failures = 0;
foreach (var (name, actual, expected) in results)
+1 -28
View File
@@ -42,23 +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
0xBF810000, // s_endpgm
]),
("mrt", true, [
0x7E0002FF, 0x3F800000, // v_mov_b32 v0, 1.0f
0x7E0202FF, 0x00000000, // v_mov_b32 v1, 0.0f
@@ -132,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
@@ -153,13 +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
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
0xBF810000, // s_endpgm
]),
];