mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-28 13:40:58 +08:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 487bda6a32 | |||
| 09812600a0 | |||
| 3005babab8 | |||
| 09bd4f028b | |||
| 2bda253927 | |||
| 71e5912c75 | |||
| a030cb5a5d | |||
| 336286e588 | |||
| cab001f265 | |||
| bab965e394 |
@@ -9,7 +9,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<SharpEmuVersion>0.0.2-beta.3</SharpEmuVersion>
|
<SharpEmuVersion>0.0.2-beta.4</SharpEmuVersion>
|
||||||
<Version>$(SharpEmuVersion)</Version>
|
<Version>$(SharpEmuVersion)</Version>
|
||||||
|
|
||||||
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
|
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<!--
|
||||||
|
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
|
||||||
|
```
|
||||||
@@ -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
|
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.
|
the movie without trying to execute the PS5-specific Bink GPU decode path.
|
||||||
|
|
||||||
Without an adapter, Bink movies are skipped by default: their open call returns
|
Without an adapter, Bink files remain visible to the guest and the game's
|
||||||
not-found so games that mark cinematics as optional progress to their next
|
statically linked decoder runs normally. Set SHARPEMU_BINK_MODE=skip only when
|
||||||
state instead of waiting on an empty Bink GPU texture.
|
explicitly testing a title whose cinematics are optional.
|
||||||
|
|
||||||
Set SHARPEMU_BINK_MODE=dummy to retain the open and show a built-in,
|
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
|
non-decoded placeholder frame. This requires no SDK, but is a visual diagnostic
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"sdk": {
|
"sdk": {
|
||||||
"version": "10.0.103",
|
"version": "10.0.103",
|
||||||
"rollForward": "disable"
|
"rollForward": "latestFeature"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
#!/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())
|
||||||
@@ -45,11 +45,6 @@ internal static partial class Program
|
|||||||
[STAThread]
|
[STAThread]
|
||||||
private static int Main(string[] args)
|
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
|
try
|
||||||
{
|
{
|
||||||
return Run(args);
|
return Run(args);
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||||
<DebugType>none</DebugType>
|
<DebugType>none</DebugType>
|
||||||
<DebugSymbols>false</DebugSymbols>
|
<DebugSymbols>false</DebugSymbols>
|
||||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'win-x64' Or '$(RuntimeIdentifier)' == ''">
|
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'win-x64' Or '$(RuntimeIdentifier)' == ''">
|
||||||
|
|||||||
@@ -1,603 +0,0 @@
|
|||||||
{
|
|
||||||
"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=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1410,6 +1410,8 @@ public sealed partial class DirectExecutionBackend
|
|||||||
"eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
|
"eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
|
||||||
"qvMUCyyaCSI" or // sceKernelAprSubmitCommandBufferAndGetId
|
"qvMUCyyaCSI" or // sceKernelAprSubmitCommandBufferAndGetId
|
||||||
"Q2V+iqvjgC0" or // vsnprintf
|
"Q2V+iqvjgC0" or // vsnprintf
|
||||||
|
"AV6ipCNa4Rw" or // strcasecmp
|
||||||
|
"viiwFMaNamA" or // strstr
|
||||||
"q1cHNfGycLI" or // scePadRead
|
"q1cHNfGycLI" or // scePadRead
|
||||||
"xk0AcarP3V4" or // scePadOpen
|
"xk0AcarP3V4" or // scePadOpen
|
||||||
"yH17Q6NWtVg" or // sceUserServiceGetEvent
|
"yH17Q6NWtVg" or // sceUserServiceGetEvent
|
||||||
@@ -1571,6 +1573,8 @@ public sealed partial class DirectExecutionBackend
|
|||||||
"WkkeywLJcgU" or // wcslen
|
"WkkeywLJcgU" or // wcslen
|
||||||
"Ovb2dSJOAuE" or // strcmp
|
"Ovb2dSJOAuE" or // strcmp
|
||||||
"aesyjrHVWy4" or // strncmp
|
"aesyjrHVWy4" or // strncmp
|
||||||
|
"AV6ipCNa4Rw" or // strcasecmp
|
||||||
|
"viiwFMaNamA" or // strstr
|
||||||
"pNtJdE3x49E" or // wcscmp
|
"pNtJdE3x49E" or // wcscmp
|
||||||
"fV2xHER+bKE" or // wcscoll
|
"fV2xHER+bKE" or // wcscoll
|
||||||
"E8wCoUEbfzk" or // wcsncmp
|
"E8wCoUEbfzk" or // wcsncmp
|
||||||
|
|||||||
@@ -1408,6 +1408,54 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
0x75, 0xE7,
|
0x75, 0xE7,
|
||||||
0xC3,
|
0xC3,
|
||||||
],
|
],
|
||||||
|
"AV6ipCNa4Rw" =>
|
||||||
|
[
|
||||||
|
0x0F, 0xB6, 0x07,
|
||||||
|
0x0F, 0xB6, 0x16,
|
||||||
|
0x8D, 0x48, 0xBF,
|
||||||
|
0x83, 0xF9, 0x19,
|
||||||
|
0x77, 0x03,
|
||||||
|
0x83, 0xC0, 0x20,
|
||||||
|
0x8D, 0x4A, 0xBF,
|
||||||
|
0x83, 0xF9, 0x19,
|
||||||
|
0x77, 0x03,
|
||||||
|
0x83, 0xC2, 0x20,
|
||||||
|
0x29, 0xD0,
|
||||||
|
0x75, 0x0C,
|
||||||
|
0x85, 0xD2,
|
||||||
|
0x74, 0x08,
|
||||||
|
0x48, 0xFF, 0xC7,
|
||||||
|
0x48, 0xFF, 0xC6,
|
||||||
|
0xEB, 0xD4,
|
||||||
|
0xC3,
|
||||||
|
],
|
||||||
|
"viiwFMaNamA" =>
|
||||||
|
[
|
||||||
|
0x0F, 0xB6, 0x16,
|
||||||
|
0x84, 0xD2,
|
||||||
|
0x74, 0x2D,
|
||||||
|
0x0F, 0xB6, 0x07,
|
||||||
|
0x84, 0xC0,
|
||||||
|
0x74, 0x2A,
|
||||||
|
0x38, 0xD0,
|
||||||
|
0x75, 0x1D,
|
||||||
|
0x4C, 0x8D, 0x47, 0x01,
|
||||||
|
0x4C, 0x8D, 0x4E, 0x01,
|
||||||
|
0x41, 0x0F, 0xB6, 0x09,
|
||||||
|
0x84, 0xC9,
|
||||||
|
0x74, 0x12,
|
||||||
|
0x41, 0x38, 0x08,
|
||||||
|
0x75, 0x08,
|
||||||
|
0x49, 0xFF, 0xC0,
|
||||||
|
0x49, 0xFF, 0xC1,
|
||||||
|
0xEB, 0xEB,
|
||||||
|
0x48, 0xFF, 0xC7,
|
||||||
|
0xEB, 0xD3,
|
||||||
|
0x48, 0x89, 0xF8,
|
||||||
|
0xC3,
|
||||||
|
0x31, 0xC0,
|
||||||
|
0xC3,
|
||||||
|
],
|
||||||
"pNtJdE3x49E" or "fV2xHER+bKE" =>
|
"pNtJdE3x49E" or "fV2xHER+bKE" =>
|
||||||
[
|
[
|
||||||
0x0F, 0xB7, 0x07,
|
0x0F, 0xB7, 0x07,
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ public static unsafe class JitStubs
|
|||||||
var pattern = TlsAccessPattern;
|
var pattern = TlsAccessPattern;
|
||||||
var end = start + length - pattern.Length;
|
var end = start + length - pattern.Length;
|
||||||
|
|
||||||
for (var ptr = start; ptr < end; ptr++)
|
for (var ptr = start; ptr <= end; ptr++)
|
||||||
{
|
{
|
||||||
if (MatchesPattern(ptr, pattern))
|
if (MatchesPattern(ptr, pattern))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -92,6 +92,11 @@ public partial class MainWindow : Window
|
|||||||
// plain window color remains the fallback when the asset fails to load.
|
// plain window color remains the fallback when the asset fails to load.
|
||||||
private Bitmap? _defaultBackdrop;
|
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.
|
// Controller navigation state.
|
||||||
private readonly DispatcherTimer _gamepadTimer;
|
private readonly DispatcherTimer _gamepadTimer;
|
||||||
private HostGamepadButtons _previousPadButtons;
|
private HostGamepadButtons _previousPadButtons;
|
||||||
@@ -150,8 +155,18 @@ public partial class MainWindow : Window
|
|||||||
};
|
};
|
||||||
_libraryBlurTimer.Tick += (_, _) => AdvanceLibraryBlur();
|
_libraryBlurTimer.Tick += (_, _) => AdvanceLibraryBlur();
|
||||||
|
|
||||||
Activated += (_, _) => UpdateSessionBarVisibility();
|
// Native popups float above every window on the desktop; they must
|
||||||
Deactivated += (_, _) => SessionBarPopup.IsOpen = false;
|
// follow the launcher into the background or a minimized state.
|
||||||
|
Activated += (_, _) =>
|
||||||
|
{
|
||||||
|
UpdateSessionBarVisibility();
|
||||||
|
SessionLoadingPopup.IsOpen = _sessionLoadingActive;
|
||||||
|
};
|
||||||
|
Deactivated += (_, _) =>
|
||||||
|
{
|
||||||
|
SessionBarPopup.IsOpen = false;
|
||||||
|
SessionLoadingPopup.IsOpen = false;
|
||||||
|
};
|
||||||
|
|
||||||
TitleBar.PointerPressed += OnTitleBarPointerPressed;
|
TitleBar.PointerPressed += OnTitleBarPointerPressed;
|
||||||
GameList.SelectionChanged += (_, _) => UpdateSelectedGame();
|
GameList.SelectionChanged += (_, _) => UpdateSelectedGame();
|
||||||
@@ -414,6 +429,15 @@ public partial class MainWindow : Window
|
|||||||
return;
|
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;
|
var shoulderPressed = pad.Buttons & ~_previousPadButtons;
|
||||||
if ((shoulderPressed & HostGamepadButtons.L1) != 0)
|
if ((shoulderPressed & HostGamepadButtons.L1) != 0)
|
||||||
{
|
{
|
||||||
@@ -463,11 +487,6 @@ public partial class MainWindow : Window
|
|||||||
LaunchSelected();
|
LaunchSelected();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((pressed & HostGamepadButtons.Circle) != 0)
|
|
||||||
{
|
|
||||||
StopEmulator();
|
|
||||||
}
|
|
||||||
|
|
||||||
_previousPadButtons = pad.Buttons;
|
_previousPadButtons = pad.Buttons;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1626,13 +1645,23 @@ public partial class MainWindow : Window
|
|||||||
base.OnPropertyChanged(change);
|
base.OnPropertyChanged(change);
|
||||||
if (change.Property == WindowStateProperty)
|
if (change.Property == WindowStateProperty)
|
||||||
{
|
{
|
||||||
|
// The XAML WindowState="Maximized" assignment raises this change
|
||||||
|
// during InitializeComponent, before named controls are wired up.
|
||||||
if (WindowState == WindowState.Minimized)
|
if (WindowState == WindowState.Minimized)
|
||||||
{
|
{
|
||||||
_sndPreview.Pause();
|
_sndPreview.Pause();
|
||||||
|
if (SessionLoadingPopup is { } popup)
|
||||||
|
{
|
||||||
|
popup.IsOpen = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_sndPreview.Resume();
|
_sndPreview.Resume();
|
||||||
|
if (SessionLoadingPopup is { } popup)
|
||||||
|
{
|
||||||
|
popup.IsOpen = _sessionLoadingActive;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2009,7 +2038,7 @@ public partial class MainWindow : Window
|
|||||||
ContentToolbar.IsVisible = false;
|
ContentToolbar.IsVisible = false;
|
||||||
ConsolePanel.IsVisible = false;
|
ConsolePanel.IsVisible = false;
|
||||||
LaunchBar.IsVisible = false;
|
LaunchBar.IsVisible = false;
|
||||||
SessionLoadingPopup.IsOpen = false;
|
HideSessionLoading();
|
||||||
UpdateSessionBarVisibility();
|
UpdateSessionBarVisibility();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -2109,7 +2138,7 @@ public partial class MainWindow : Window
|
|||||||
GameView.IsVisible = false;
|
GameView.IsVisible = false;
|
||||||
GameView.IsHitTestVisible = true;
|
GameView.IsHitTestVisible = true;
|
||||||
SessionBarPopup.IsOpen = false;
|
SessionBarPopup.IsOpen = false;
|
||||||
SessionLoadingPopup.IsOpen = false;
|
HideSessionLoading();
|
||||||
AnimateLibraryBlur(0, clearWhenComplete: true);
|
AnimateLibraryBlur(0, clearWhenComplete: true);
|
||||||
MainContent.Margin = new Thickness(32, 24, 32, 20);
|
MainContent.Margin = new Thickness(32, 24, 32, 20);
|
||||||
ContentToolbar.IsVisible = true;
|
ContentToolbar.IsVisible = true;
|
||||||
@@ -2193,7 +2222,14 @@ public partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
SessionLoadingTitle.Text = title;
|
SessionLoadingTitle.Text = title;
|
||||||
SessionLoadingDetail.Text = detail;
|
SessionLoadingDetail.Text = detail;
|
||||||
SessionLoadingPopup.IsOpen = true;
|
_sessionLoadingActive = true;
|
||||||
|
SessionLoadingPopup.IsOpen = IsActive && WindowState != WindowState.Minimized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HideSessionLoading()
|
||||||
|
{
|
||||||
|
_sessionLoadingActive = false;
|
||||||
|
SessionLoadingPopup.IsOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ReturnToLibraryWhileStopping()
|
private void ReturnToLibraryWhileStopping()
|
||||||
|
|||||||
@@ -568,6 +568,8 @@ public static partial class AgcExports
|
|||||||
public ulong WorkSequence { get; set; }
|
public ulong WorkSequence { get; set; }
|
||||||
public ulong SubmissionSequence { get; set; }
|
public ulong SubmissionSequence { get; set; }
|
||||||
public bool WaitMonitorRunning { get; set; }
|
public bool WaitMonitorRunning { get; set; }
|
||||||
|
public object WaitMonitorSignalGate { get; } = new();
|
||||||
|
public long WaitMonitorSignalVersion { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly record struct RegisteredAgcResource(
|
private readonly record struct RegisteredAgcResource(
|
||||||
@@ -3313,24 +3315,11 @@ public static partial class AgcExports
|
|||||||
length,
|
length,
|
||||||
op,
|
op,
|
||||||
out var dispatch,
|
out var dispatch,
|
||||||
out var indirectDimsRetryAddress))
|
out _))
|
||||||
{
|
{
|
||||||
state.FrameDispatchCount++;
|
state.FrameDispatchCount++;
|
||||||
ObserveComputeDispatch(ctx, gpuState, state, dispatch);
|
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 &&
|
if (op == ItNop &&
|
||||||
@@ -3668,27 +3657,11 @@ public static partial class AgcExports
|
|||||||
void CompleteAndWake()
|
void CompleteAndWake()
|
||||||
{
|
{
|
||||||
CompleteLabelProducer(producer);
|
CompleteLabelProducer(producer);
|
||||||
if (GpuWaitRegistry.Count == 0)
|
lock (gpuState.WaitMonitorSignalGate)
|
||||||
{
|
{
|
||||||
return;
|
gpuState.WaitMonitorSignalVersion++;
|
||||||
|
Monitor.Pulse(gpuState.WaitMonitorSignalGate);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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()
|
void ApplyAndQueueCompletion()
|
||||||
@@ -4867,38 +4840,45 @@ public static partial class AgcExports
|
|||||||
SubmittedGpuState gpuState)
|
SubmittedGpuState gpuState)
|
||||||
{
|
{
|
||||||
var delayMilliseconds = 1;
|
var delayMilliseconds = 1;
|
||||||
|
long observedSignal;
|
||||||
|
lock (gpuState.WaitMonitorSignalGate)
|
||||||
|
{
|
||||||
|
observedSignal = gpuState.WaitMonitorSignalVersion;
|
||||||
|
}
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
var madeProgress = false;
|
int resumed;
|
||||||
|
int remaining;
|
||||||
lock (gpuState.Gate)
|
lock (gpuState.Gate)
|
||||||
{
|
{
|
||||||
var before = GpuWaitRegistry.CountForMemory(ctx.Memory);
|
resumed = DrainResumableDcbs(ctx, gpuState, tracePackets: _traceAgc);
|
||||||
if (before == 0)
|
remaining = GpuWaitRegistry.CountForMemory(ctx.Memory);
|
||||||
{
|
if (_traceAgc && resumed != 0)
|
||||||
gpuState.WaitMonitorRunning = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DrainResumableDcbs(ctx, gpuState, tracePackets: _traceAgc);
|
|
||||||
var after = GpuWaitRegistry.CountForMemory(ctx.Memory);
|
|
||||||
madeProgress = after < before;
|
|
||||||
if (madeProgress)
|
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][TRACE] agc.wait_monitor_resumed count={before - after} " +
|
$"[LOADER][TRACE] agc.wait_monitor_resumed count={resumed} " +
|
||||||
$"remaining={after}");
|
$"remaining={remaining}");
|
||||||
}
|
}
|
||||||
if (after == 0)
|
if (remaining == 0)
|
||||||
{
|
{
|
||||||
gpuState.WaitMonitorRunning = false;
|
gpuState.WaitMonitorRunning = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
delayMilliseconds = madeProgress
|
delayMilliseconds = resumed != 0
|
||||||
? 1
|
? 1
|
||||||
: Math.Min(delayMilliseconds * 2, 16);
|
: Math.Min(delayMilliseconds * 2, 16);
|
||||||
Thread.Sleep(delayMilliseconds);
|
lock (gpuState.WaitMonitorSignalGate)
|
||||||
|
{
|
||||||
|
if (gpuState.WaitMonitorSignalVersion == observedSignal)
|
||||||
|
{
|
||||||
|
Monitor.Wait(gpuState.WaitMonitorSignalGate, delayMilliseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
observedSignal = gpuState.WaitMonitorSignalVersion;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4952,16 +4932,17 @@ public static partial class AgcExports
|
|||||||
// guest memory (labels are advanced by ReleaseMem/WriteData/DmaData packets
|
// guest memory (labels are advanced by ReleaseMem/WriteData/DmaData packets
|
||||||
// or direct CPU writes) and resumes the ones now satisfied. A resumed DCB
|
// 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.
|
// can itself write labels that unblock others, so loop to a fixed point.
|
||||||
private static void DrainResumableDcbs(
|
private static int DrainResumableDcbs(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
SubmittedGpuState gpuState,
|
SubmittedGpuState gpuState,
|
||||||
bool tracePackets)
|
bool tracePackets)
|
||||||
{
|
{
|
||||||
if (!_gpuWaitSuspendEnabled)
|
if (!_gpuWaitSuspendEnabled)
|
||||||
{
|
{
|
||||||
return;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var resumedCount = 0;
|
||||||
for (var pass = 0; pass < 256; pass++)
|
for (var pass = 0; pass < 256; pass++)
|
||||||
{
|
{
|
||||||
var woken = GpuWaitRegistry.CollectSatisfied(ctx.Memory, (address, is64Bit) =>
|
var woken = GpuWaitRegistry.CollectSatisfied(ctx.Memory, (address, is64Bit) =>
|
||||||
@@ -5039,7 +5020,7 @@ public static partial class AgcExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return resumedCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (woken is not null)
|
if (woken is not null)
|
||||||
@@ -5047,9 +5028,12 @@ public static partial class AgcExports
|
|||||||
foreach (var waiter in woken)
|
foreach (var waiter in woken)
|
||||||
{
|
{
|
||||||
ResumeSuspendedDcb(ctx, gpuState, waiter, tracePackets);
|
ResumeSuspendedDcb(ctx, gpuState, waiter, tracePackets);
|
||||||
|
resumedCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return resumedCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ResumeSuspendedDcb(
|
private static void ResumeSuspendedDcb(
|
||||||
@@ -8894,7 +8878,7 @@ public static partial class AgcExports
|
|||||||
out _);
|
out _);
|
||||||
var globalMemoryBuffers =
|
var globalMemoryBuffers =
|
||||||
CreateTranslatedComputeGlobalBuffers(evaluation);
|
CreateTranslatedComputeGlobalBuffers(evaluation);
|
||||||
var workSequence = GuestGpu.Current.SubmitComputeDispatch(
|
GuestGpu.Current.SubmitComputeDispatch(
|
||||||
shaderAddress,
|
shaderAddress,
|
||||||
computeShader,
|
computeShader,
|
||||||
textures,
|
textures,
|
||||||
@@ -8913,12 +8897,9 @@ public static partial class AgcExports
|
|||||||
dispatch.ThreadCountX,
|
dispatch.ThreadCountX,
|
||||||
dispatch.ThreadCountY,
|
dispatch.ThreadCountY,
|
||||||
dispatch.ThreadCountZ);
|
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;
|
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()
|
internal static void Install()
|
||||||
{
|
{
|
||||||
Gen5ShaderScalarEvaluator.FallbackMemoryReader =
|
Gen5ShaderScalarEvaluator.FallbackMemoryReader =
|
||||||
KernelMemoryCompatExports.TryReadTrackedLibcHeap;
|
KernelMemoryCompatExports.TryReadShaderGuestMemory;
|
||||||
Gen5ShaderScalarEvaluator.GlobalMemoryPool =
|
Gen5ShaderScalarEvaluator.GlobalMemoryPool =
|
||||||
GuestDataPool.Shared;
|
GuestDataPool.Shared;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using SharpEmu.Libs.Kernel;
|
|||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using Microsoft.Win32.SafeHandles;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Ampr;
|
namespace SharpEmu.Libs.Ampr;
|
||||||
|
|
||||||
@@ -43,17 +44,17 @@ public static class AmprExports
|
|||||||
{
|
{
|
||||||
public CachedHostFile(string path)
|
public CachedHostFile(string path)
|
||||||
{
|
{
|
||||||
Stream = new FileStream(
|
Handle = File.OpenHandle(
|
||||||
path,
|
path,
|
||||||
FileMode.Open,
|
FileMode.Open,
|
||||||
FileAccess.Read,
|
FileAccess.Read,
|
||||||
FileShare.ReadWrite | FileShare.Delete,
|
FileShare.ReadWrite | FileShare.Delete,
|
||||||
bufferSize: 1024 * 1024,
|
|
||||||
FileOptions.RandomAccess);
|
FileOptions.RandomAccess);
|
||||||
|
Length = RandomAccess.GetLength(Handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
public object Gate { get; } = new();
|
public SafeFileHandle Handle { get; }
|
||||||
public FileStream Stream { get; }
|
public long Length { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -735,13 +736,7 @@ public static class AmprExports
|
|||||||
return openResult;
|
return openResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
long fileLength;
|
if (fileOffset >= (ulong)cachedFile.Length)
|
||||||
lock (cachedFile.Gate)
|
|
||||||
{
|
|
||||||
fileLength = cachedFile.Stream.Length;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileOffset >= (ulong)fileLength)
|
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -760,12 +755,10 @@ public static class AmprExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var request = (int)Math.Min((ulong)buffer.Length, size - bytesRead);
|
var request = (int)Math.Min((ulong)buffer.Length, size - bytesRead);
|
||||||
int read;
|
var read = RandomAccess.Read(
|
||||||
lock (cachedFile.Gate)
|
cachedFile.Handle,
|
||||||
{
|
buffer.AsSpan(0, request),
|
||||||
cachedFile.Stream.Position = unchecked((long)absoluteOffset);
|
unchecked((long)absoluteOffset));
|
||||||
read = cachedFile.Stream.Read(buffer, 0, request);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (read <= 0)
|
if (read <= 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -29,10 +29,9 @@ internal static class Bink2MovieBridge
|
|||||||
private static bool _availabilityReported;
|
private static bool _availabilityReported;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns true when the guest should receive a normal "file not found"
|
/// Returns true only when movie skipping was explicitly requested. Without
|
||||||
/// result for a Bink movie. This is the safe default without a decoder:
|
/// a host adapter the guest must be allowed to run the Bink implementation
|
||||||
/// games that treat movies as optional fall through to their next state
|
/// statically linked into its executable.
|
||||||
/// rather than submitting an empty Bink GPU texture forever.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static bool ShouldSkipGuestMovie(string hostPath) =>
|
internal static bool ShouldSkipGuestMovie(string hostPath) =>
|
||||||
hostPath.EndsWith(".bk2", StringComparison.OrdinalIgnoreCase) &&
|
hostPath.EndsWith(".bk2", StringComparison.OrdinalIgnoreCase) &&
|
||||||
@@ -53,12 +52,18 @@ internal static class Bink2MovieBridge
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ResolveMode() == MovieMode.Dummy)
|
var mode = ResolveMode();
|
||||||
|
if (mode == MovieMode.Dummy)
|
||||||
{
|
{
|
||||||
AttachDummyMovieLocked(hostPath);
|
AttachDummyMovieLocked(hostPath);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mode != MovieMode.Native)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var adapter = GetAdapterLocked();
|
var adapter = GetAdapterLocked();
|
||||||
if (adapter is null)
|
if (adapter is null)
|
||||||
{
|
{
|
||||||
@@ -165,16 +170,15 @@ internal static class Bink2MovieBridge
|
|||||||
return MovieMode.Skip;
|
return MovieMode.Skip;
|
||||||
}
|
}
|
||||||
|
|
||||||
// With no SDK adapter present, returning "not found" makes optional
|
// Prefer the optional host adapter when one is supplied. Otherwise let
|
||||||
// cinematics advance. Supplying either an explicit path or the normal
|
// the game's statically linked Bink implementation consume the file.
|
||||||
// side-by-side adapter enables native playback automatically.
|
|
||||||
if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("SHARPEMU_BINK2_BRIDGE")) ||
|
if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("SHARPEMU_BINK2_BRIDGE")) ||
|
||||||
EnumerateAdapterCandidates().Any(File.Exists))
|
EnumerateAdapterCandidates().Any(File.Exists))
|
||||||
{
|
{
|
||||||
return MovieMode.Native;
|
return MovieMode.Native;
|
||||||
}
|
}
|
||||||
|
|
||||||
return MovieMode.Skip;
|
return MovieMode.Guest;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AttachDummyMovieLocked(string hostPath)
|
private static void AttachDummyMovieLocked(string hostPath)
|
||||||
@@ -335,6 +339,7 @@ internal static class Bink2MovieBridge
|
|||||||
|
|
||||||
private enum MovieMode
|
private enum MovieMode
|
||||||
{
|
{
|
||||||
|
Guest,
|
||||||
Skip,
|
Skip,
|
||||||
Dummy,
|
Dummy,
|
||||||
Native,
|
Native,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Gpu;
|
namespace SharpEmu.Libs.Gpu;
|
||||||
|
|
||||||
@@ -15,7 +16,125 @@ namespace SharpEmu.Libs.Gpu;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal static class GuestDataPool
|
internal static class GuestDataPool
|
||||||
{
|
{
|
||||||
public static ArrayPool<byte> Shared { get; } = ArrayPool<byte>.Create(
|
public static ArrayPool<byte> Shared { get; } = new BoundedByteArrayPool(
|
||||||
maxArrayLength: 16 * 1024 * 1024,
|
maxArrayLength: 16 * 1024 * 1024,
|
||||||
maxArraysPerBucket: 96);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6010,7 +6010,7 @@ public static partial class KernelMemoryCompatExports
|
|||||||
return highWaterMark;
|
return highWaterMark;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryReadHostMemory(ulong address, Span<byte> destination)
|
private static unsafe bool TryReadHostMemory(ulong address, Span<byte> destination)
|
||||||
{
|
{
|
||||||
if (destination.IsEmpty || !IsHostRangeAccessible(address, (ulong)destination.Length, writeAccess: false))
|
if (destination.IsEmpty || !IsHostRangeAccessible(address, (ulong)destination.Length, writeAccess: false))
|
||||||
{
|
{
|
||||||
@@ -6019,9 +6019,7 @@ public static partial class KernelMemoryCompatExports
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var temporary = new byte[destination.Length];
|
new ReadOnlySpan<byte>((void*)address, destination.Length).CopyTo(destination);
|
||||||
Marshal.Copy((nint)address, temporary, 0, temporary.Length);
|
|
||||||
temporary.AsSpan().CopyTo(destination);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -6061,6 +6059,41 @@ public static partial class KernelMemoryCompatExports
|
|||||||
return false;
|
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(
|
internal static bool TryReadTrackedLibcHeapGpuAlias(
|
||||||
ulong packedAddress,
|
ulong packedAddress,
|
||||||
Span<byte> destination)
|
Span<byte> destination)
|
||||||
@@ -6340,7 +6373,7 @@ public static partial class KernelMemoryCompatExports
|
|||||||
return value != 0 && (value & (value - 1)) == 0;
|
return value != 0 && (value & (value - 1)) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryWriteHostMemory(ulong address, ReadOnlySpan<byte> source)
|
private static unsafe bool TryWriteHostMemory(ulong address, ReadOnlySpan<byte> source)
|
||||||
{
|
{
|
||||||
if (source.IsEmpty || !IsHostRangeAccessible(address, (ulong)source.Length, writeAccess: true))
|
if (source.IsEmpty || !IsHostRangeAccessible(address, (ulong)source.Length, writeAccess: true))
|
||||||
{
|
{
|
||||||
@@ -6349,8 +6382,7 @@ public static partial class KernelMemoryCompatExports
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var temporary = source.ToArray();
|
source.CopyTo(new Span<byte>((void*)address, source.Length));
|
||||||
Marshal.Copy(temporary, 0, (nint)address, temporary.Length);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -6377,20 +6409,37 @@ public static partial class KernelMemoryCompatExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryQueryHostPage(address, out var startInfo) || !HasRequiredProtection(startInfo.Protect, writeAccess))
|
var endAddress = address + length - 1;
|
||||||
|
var currentAddress = address;
|
||||||
|
while (currentAddress <= endAddress)
|
||||||
|
{
|
||||||
|
if (!TryQueryHostPage(currentAddress, out var info) ||
|
||||||
|
!HasRequiredProtection(info.Protect, writeAccess))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var endAddress = address + length - 1;
|
var regionBase = unchecked((ulong)info.BaseAddress);
|
||||||
if (endAddress == address)
|
var regionSize = (ulong)info.RegionSize;
|
||||||
|
if (regionSize == 0 ||
|
||||||
|
regionBase > currentAddress ||
|
||||||
|
ulong.MaxValue - regionBase < regionSize)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var regionEnd = regionBase + regionSize;
|
||||||
|
if (regionEnd <= currentAddress)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (regionEnd > endAddress)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryQueryHostPage(endAddress, out var endInfo) || !HasRequiredProtection(endInfo.Protect, writeAccess))
|
currentAddress = regionEnd;
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Threading;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Kernel;
|
||||||
|
|
||||||
|
// libKernel's address-wait primitives (sceKernelSyncOnAddress*) are the PS5's
|
||||||
|
// futex-style wait/wake: a thread parks on a guest address until another thread
|
||||||
|
// wakes that address. Guest runtimes (seen driving Juicy Realm, PPSA19268)
|
||||||
|
// build their own spinlocks/queues on top of it and call the wait in a hot
|
||||||
|
// loop; left unimplemented, every wait returns immediately and the runtime
|
||||||
|
// busy-spins forever (millions of calls, no forward progress).
|
||||||
|
//
|
||||||
|
// This implements wait/wake over the existing cooperative-block scheduler,
|
||||||
|
// keyed on the address. The real primitive takes a compare value so the wait
|
||||||
|
// only sleeps while the address still holds the expected value; that exact
|
||||||
|
// value is not recovered here, so each wait is given a bounded deadline and
|
||||||
|
// treated as a spurious-wakeup-tolerant park: a genuinely missed wake
|
||||||
|
// self-heals when the deadline expires and the guest re-checks its own
|
||||||
|
// condition, which futex callers already tolerate. A matching wake releases
|
||||||
|
// waiters immediately through the same key.
|
||||||
|
public static class KernelSyncOnAddressCompatExports
|
||||||
|
{
|
||||||
|
// Safety-net poll interval. Real releases come from the wake side (generation
|
||||||
|
// bump + WakeBlockedThreads); this only bounds how long a wait that genuinely
|
||||||
|
// raced/missed its wake stays parked before the guest re-evaluates. Kept
|
||||||
|
// large: a short interval turns every parked waiter into a hot re-poll that
|
||||||
|
// steals scheduler bandwidth from the threads that actually make progress
|
||||||
|
// (including the ones that would issue the wake), so it must be a rare last
|
||||||
|
// resort, not a spin substitute.
|
||||||
|
private static readonly TimeSpan WaitSelfHealTimeout = TimeSpan.FromMilliseconds(100);
|
||||||
|
|
||||||
|
// Per-address host gate for the non-cooperative (host main thread) fallback,
|
||||||
|
// which cannot use the guest-thread scheduler's block mechanism.
|
||||||
|
private static readonly ConcurrentDictionary<ulong, object> _hostAddressGates = new();
|
||||||
|
|
||||||
|
// Per-address wake generation. A wait captures the current generation and
|
||||||
|
// its wake predicate stays unsatisfied (keeps the thread parked) until a
|
||||||
|
// wake bumps it. This is what actually holds the thread blocked: a bare
|
||||||
|
// "always satisfied" predicate is treated as an immediate late-arrival by
|
||||||
|
// the dispatcher's race guard and never yields, leaving the guest to
|
||||||
|
// busy-spin. The generation also closes the register-vs-park race for free:
|
||||||
|
// a wake landing in that window bumps the generation, so the predicate is
|
||||||
|
// already satisfied and the guest correctly resumes at once.
|
||||||
|
private static readonly ConcurrentDictionary<ulong, long> _wakeGenerations = new();
|
||||||
|
|
||||||
|
private static long CurrentGeneration(ulong address) =>
|
||||||
|
_wakeGenerations.TryGetValue(address, out var generation) ? generation : 0;
|
||||||
|
|
||||||
|
private static string WakeKey(ulong address) => $"sceKernelSyncOnAddress:{address:X16}";
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Hc4CaR6JBL0",
|
||||||
|
ExportName = "sceKernelSyncOnAddressWait",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int SyncOnAddressWait(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var address = ctx[CpuRegister.Rdi];
|
||||||
|
if (address == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var observedGeneration = CurrentGeneration(address);
|
||||||
|
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(WaitSelfHealTimeout);
|
||||||
|
|
||||||
|
// Cooperative path: stay parked until a wake bumps this address's
|
||||||
|
// generation (or the deadline expires as a self-heal). The guest
|
||||||
|
// re-evaluates its own condition after resuming.
|
||||||
|
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
|
ctx,
|
||||||
|
"sceKernelSyncOnAddressWait",
|
||||||
|
WakeKey(address),
|
||||||
|
resumeHandler: () => (int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||||
|
wakeHandler: () => CurrentGeneration(address) != observedGeneration,
|
||||||
|
deadline))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-cooperative caller (host main thread): bounded host wait so a
|
||||||
|
// missed wake self-heals instead of hanging.
|
||||||
|
var gate = _hostAddressGates.GetOrAdd(address, static _ => new object());
|
||||||
|
lock (gate)
|
||||||
|
{
|
||||||
|
if (CurrentGeneration(address) == observedGeneration)
|
||||||
|
{
|
||||||
|
Monitor.Wait(gate, WaitSelfHealTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "q2y-wDIVWZA",
|
||||||
|
ExportName = "sceKernelSyncOnAddressWake",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int SyncOnAddressWake(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var address = ctx[CpuRegister.Rdi];
|
||||||
|
if (address == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// rsi carries the number of waiters to release (1 = wake-one, a large
|
||||||
|
// value = wake-all); default to all if it looks unset.
|
||||||
|
var requested = unchecked((long)ctx[CpuRegister.Rsi]);
|
||||||
|
var wakeCount = requested is > 0 and < int.MaxValue ? (int)requested : int.MaxValue;
|
||||||
|
|
||||||
|
// Bump the generation first so a wait that has registered but not yet
|
||||||
|
// parked sees the change and resumes instead of missing this wake.
|
||||||
|
_wakeGenerations.AddOrUpdate(address, 1, static (_, current) => current + 1);
|
||||||
|
|
||||||
|
GuestThreadExecution.Scheduler?.WakeBlockedThreads(WakeKey(address), wakeCount);
|
||||||
|
|
||||||
|
if (_hostAddressGates.TryGetValue(address, out var gate))
|
||||||
|
{
|
||||||
|
lock (gate)
|
||||||
|
{
|
||||||
|
Monitor.PulseAll(gate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||||
|
{
|
||||||
|
var value = (int)result;
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Random;
|
||||||
|
|
||||||
|
public static class RandomExports
|
||||||
|
{
|
||||||
|
private const int RandomErrorInvalid = unchecked((int)0x817C0016);
|
||||||
|
private const int MaxRandomBytes = 64;
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "PI7jIZj4pcE",
|
||||||
|
ExportName = "sceRandomGetRandomNumber",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRandom")]
|
||||||
|
public static int RandomGetRandomNumber(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var destination = ctx[CpuRegister.Rdi];
|
||||||
|
var size = ctx[CpuRegister.Rsi];
|
||||||
|
if ((destination == 0 && size != 0) || size > MaxRandomBytes)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(RandomErrorInvalid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> bytes = stackalloc byte[(int)size];
|
||||||
|
RandomNumberGenerator.Fill(bytes);
|
||||||
|
return ctx.Memory.TryWrite(destination, bytes)
|
||||||
|
? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,6 +36,7 @@ public static class PerfOverlay
|
|||||||
private static long _presentedInWindow;
|
private static long _presentedInWindow;
|
||||||
private static long _submittedInWindow;
|
private static long _submittedInWindow;
|
||||||
private static long _drawsInWindow;
|
private static long _drawsInWindow;
|
||||||
|
private static long _guestBufferCacheBytes;
|
||||||
|
|
||||||
// Refreshed once per second so per-frame fills never allocate.
|
// Refreshed once per second so per-frame fills never allocate.
|
||||||
private static long _statsWindowStart = Stopwatch.GetTimestamp();
|
private static long _statsWindowStart = Stopwatch.GetTimestamp();
|
||||||
@@ -74,13 +75,10 @@ public static class PerfOverlay
|
|||||||
if (last != 0)
|
if (last != 0)
|
||||||
{
|
{
|
||||||
var milliseconds = (now - last) * 1000.0 / Stopwatch.Frequency;
|
var milliseconds = (now - last) * 1000.0 / Stopwatch.Frequency;
|
||||||
if (milliseconds < 1000.0)
|
|
||||||
{
|
|
||||||
_frameMilliseconds[_frameHistoryIndex] = milliseconds;
|
_frameMilliseconds[_frameHistoryIndex] = milliseconds;
|
||||||
_frameHistoryIndex = (_frameHistoryIndex + 1) % FrameHistorySize;
|
_frameHistoryIndex = (_frameHistoryIndex + 1) % FrameHistorySize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Called on every guest flip submission.</summary>
|
/// <summary>Called on every guest flip submission.</summary>
|
||||||
public static void RecordSubmit() => Interlocked.Increment(ref _submittedInWindow);
|
public static void RecordSubmit() => Interlocked.Increment(ref _submittedInWindow);
|
||||||
@@ -88,6 +86,9 @@ public static class PerfOverlay
|
|||||||
/// <summary>Called per translated draw/dispatch executed.</summary>
|
/// <summary>Called per translated draw/dispatch executed.</summary>
|
||||||
public static void RecordDraw() => Interlocked.Increment(ref _drawsInWindow);
|
public static void RecordDraw() => Interlocked.Increment(ref _drawsInWindow);
|
||||||
|
|
||||||
|
public static void SetGuestBufferCacheBytes(ulong bytes) =>
|
||||||
|
Interlocked.Exchange(ref _guestBufferCacheBytes, checked((long)bytes));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Rasterizes the panel into a BGRA byte span of PanelWidth x PanelHeight.
|
/// Rasterizes the panel into a BGRA byte span of PanelWidth x PanelHeight.
|
||||||
/// Runs on the render thread.
|
/// Runs on the render thread.
|
||||||
@@ -165,7 +166,7 @@ public static class PerfOverlay
|
|||||||
Environment.ProcessorCount;
|
Environment.ProcessorCount;
|
||||||
_lastCpuTime = cpuTime;
|
_lastCpuTime = cpuTime;
|
||||||
|
|
||||||
var drawsPerFrame = _fps > 0.5 ? _drawsPerSecond / _fps : 0;
|
var drawsPerFrame = _fps > 0 ? _drawsPerSecond / _fps : 0;
|
||||||
var sessionStart = Interlocked.Read(ref _sessionStartTimestamp);
|
var sessionStart = Interlocked.Read(ref _sessionStartTimestamp);
|
||||||
var elapsedSeconds = sessionStart == 0
|
var elapsedSeconds = sessionStart == 0
|
||||||
? 0L
|
? 0L
|
||||||
@@ -176,7 +177,9 @@ public static class PerfOverlay
|
|||||||
_line1 = $"FPS {_fps:0.0} FLIP {_submittedFps:0.0} {_averageFrameMs:0.0} MS";
|
_line1 = $"FPS {_fps:0.0} FLIP {_submittedFps:0.0} {_averageFrameMs:0.0} MS";
|
||||||
_line2 = $"DRAWS {_drawsPerSecond:0}/S {drawsPerFrame:0}/F Q {pendingWork}+{inFlightSubmissions}";
|
_line2 = $"DRAWS {_drawsPerSecond:0}/S {drawsPerFrame:0}/F Q {pendingWork}+{inFlightSubmissions}";
|
||||||
_line3 = $"ALLOC {_allocatedMbPerSecond:0.0} MB/S GC {_gen0PerWindow}/{_gen1PerWindow}/{_gen2PerWindow}";
|
_line3 = $"ALLOC {_allocatedMbPerSecond:0.0} MB/S GC {_gen0PerWindow}/{_gen1PerWindow}/{_gen2PerWindow}";
|
||||||
_line4 = $"CPU {_cpuPercent:0}% HEAP {GC.GetTotalMemory(false) / (1024 * 1024)} MB F1 HIDE";
|
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}%";
|
||||||
_line5 = $"TIME {elapsedHours:00}:{elapsedMinutes:00}:{elapsedRemainingSeconds:00}";
|
_line5 = $"TIME {elapsedHours:00}:{elapsedMinutes:00}:{elapsedRemainingSeconds:00}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2412,7 +2412,9 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryTakeGuestWork(out PendingGuestWork work)
|
private static bool TryTakeGuestWork(
|
||||||
|
out PendingGuestWork work,
|
||||||
|
HashSet<string>? excludedQueues = null)
|
||||||
{
|
{
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
@@ -2425,6 +2427,14 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
|
|
||||||
var queueName = _pendingGuestQueueSchedule[_pendingGuestQueueCursor];
|
var queueName = _pendingGuestQueueSchedule[_pendingGuestQueueCursor];
|
||||||
|
if (excludedQueues?.Contains(queueName) == true)
|
||||||
|
{
|
||||||
|
_pendingGuestQueueCursor =
|
||||||
|
(_pendingGuestQueueCursor + 1) % _pendingGuestQueueSchedule.Count;
|
||||||
|
queuesToProbe--;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!_pendingGuestWorkByQueue.TryGetValue(queueName, out var queue) ||
|
if (!_pendingGuestWorkByQueue.TryGetValue(queueName, out var queue) ||
|
||||||
queue.First is not { } first)
|
queue.First is not { } first)
|
||||||
{
|
{
|
||||||
@@ -2466,6 +2476,32 @@ 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)
|
private static void CompleteGuestWork(in PendingGuestWork pending)
|
||||||
{
|
{
|
||||||
SharpEmu.HLE.GuestImageWriteTracker.FlushPendingDiagnostics();
|
SharpEmu.HLE.GuestImageWriteTracker.FlushPendingDiagnostics();
|
||||||
@@ -2875,11 +2911,14 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
DescriptorSetLayout DescriptorSetLayout,
|
DescriptorSetLayout DescriptorSetLayout,
|
||||||
PipelineLayout PipelineLayout);
|
PipelineLayout PipelineLayout);
|
||||||
|
|
||||||
private readonly record struct DirtyGuestBufferRange(ulong Offset, ulong Length);
|
private readonly record struct DirtyGuestBufferRange(
|
||||||
|
ulong Offset,
|
||||||
|
ulong Length,
|
||||||
|
string QueueName,
|
||||||
|
ulong Timeline);
|
||||||
|
|
||||||
private sealed class GuestBufferAllocation
|
private sealed class GuestBufferAllocation
|
||||||
{
|
{
|
||||||
public string QueueName = VulkanGuestQueueIdentity.Default.Name;
|
|
||||||
public ulong BaseAddress;
|
public ulong BaseAddress;
|
||||||
public ulong Size;
|
public ulong Size;
|
||||||
public VkBuffer Buffer;
|
public VkBuffer Buffer;
|
||||||
@@ -2890,8 +2929,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
public List<DirtyGuestBufferRange> DirtyRanges { get; } = [];
|
public List<DirtyGuestBufferRange> DirtyRanges { get; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
private const string SharedReadOnlyGuestBufferQueue = "shared.readonly";
|
|
||||||
|
|
||||||
private sealed class TranslatedDrawResources
|
private sealed class TranslatedDrawResources
|
||||||
{
|
{
|
||||||
public string DebugName = "SharpEmu translated";
|
public string DebugName = "SharpEmu translated";
|
||||||
@@ -4795,7 +4832,9 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
MarkGuestBufferDirty(
|
MarkGuestBufferDirty(
|
||||||
allocation,
|
allocation,
|
||||||
globalBuffer.GuestOffset,
|
globalBuffer.GuestOffset,
|
||||||
globalBuffer.GuestSize);
|
globalBuffer.GuestSize,
|
||||||
|
_activeGuestQueue.Name,
|
||||||
|
_submitTimeline);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4991,7 +5030,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WaitForActiveGuestQueueSubmissionsForCpuVisibility()
|
private bool TryMakeActiveGuestQueueSubmissionsCpuVisible()
|
||||||
{
|
{
|
||||||
FlushBatchedGuestCommands();
|
FlushBatchedGuestCommands();
|
||||||
if (!_lastSubmittedTimelineByGuestQueue.TryGetValue(
|
if (!_lastSubmittedTimelineByGuestQueue.TryGetValue(
|
||||||
@@ -4999,7 +5038,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
out var targetTimeline) ||
|
out var targetTimeline) ||
|
||||||
targetTimeline <= _completedTimeline)
|
targetTimeline <= _completedTimeline)
|
||||||
{
|
{
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
PendingGuestSubmission? target = null;
|
PendingGuestSubmission? target = null;
|
||||||
@@ -5019,27 +5058,77 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
$"{targetTimeline} (completed={_completedTimeline}).");
|
$"{targetTimeline} (completed={_completedTimeline}).");
|
||||||
}
|
}
|
||||||
|
|
||||||
var waitStart = System.Diagnostics.Stopwatch.GetTimestamp();
|
|
||||||
var fence = target.Fence;
|
var fence = target.Fence;
|
||||||
Check(
|
var status = _vk.GetFenceStatus(_device, fence);
|
||||||
_vk.WaitForFences(_device, 1, &fence, true, ulong.MaxValue),
|
if (status == Result.NotReady)
|
||||||
$"vkWaitForFences(queue visibility: {_activeGuestQueue.Name})");
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status == Result.ErrorDeviceLost)
|
||||||
|
{
|
||||||
|
_deviceLost = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Check(status, $"vkGetFenceStatus(queue visibility: {_activeGuestQueue.Name})");
|
||||||
CollectCompletedGuestSubmissions(waitForOldest: false);
|
CollectCompletedGuestSubmissions(waitForOldest: false);
|
||||||
var waitedMs = (System.Diagnostics.Stopwatch.GetTimestamp() - waitStart) *
|
|
||||||
1000.0 / System.Diagnostics.Stopwatch.Frequency;
|
|
||||||
if (_traceVulkanShaderEnabled)
|
if (_traceVulkanShaderEnabled)
|
||||||
{
|
{
|
||||||
TraceVulkanShader(
|
TraceVulkanShader(
|
||||||
$"vk.queue_visibility queue={_activeGuestQueue.Name} " +
|
$"vk.queue_visibility queue={_activeGuestQueue.Name} " +
|
||||||
$"submission={_activeGuestQueue.SubmissionId} " +
|
$"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)
|
||||||
|
{
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExecuteOrderedGuestAction(VulkanOrderedGuestAction work)
|
if (target is null)
|
||||||
{
|
{
|
||||||
WaitForActiveGuestQueueSubmissionsForCpuVisibility();
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
WriteBackAllDirtyGuestBuffers(_activeGuestQueue.Name);
|
WriteBackAllDirtyGuestBuffers(_activeGuestQueue.Name);
|
||||||
work.Action();
|
work.Action();
|
||||||
if (_traceVulkanShaderEnabled)
|
if (_traceVulkanShaderEnabled)
|
||||||
@@ -5049,6 +5138,8 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
$"submission={_activeGuestQueue.SubmissionId} " +
|
$"submission={_activeGuestQueue.SubmissionId} " +
|
||||||
$"work_sequence={_activeGuestWorkSequence} name='{work.DebugName}'");
|
$"work_sequence={_activeGuestWorkSequence} name='{work.DebugName}'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExecuteOrderedGuestFlip(VulkanOrderedGuestFlip work)
|
private void ExecuteOrderedGuestFlip(VulkanOrderedGuestFlip work)
|
||||||
@@ -8186,9 +8277,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
var size = (ulong)Math.Max(guestBuffer.Length, sizeof(uint));
|
var size = (ulong)Math.Max(guestBuffer.Length, sizeof(uint));
|
||||||
var endAddress = checked(guestBuffer.BaseAddress + size);
|
var endAddress = checked(guestBuffer.BaseAddress + size);
|
||||||
GuestBufferAllocation? allocation = null;
|
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)
|
foreach (var candidate in _guestBufferAllocations)
|
||||||
{
|
{
|
||||||
if (candidate.BaseAddress > guestBuffer.BaseAddress ||
|
if (candidate.BaseAddress > guestBuffer.BaseAddress ||
|
||||||
@@ -8197,22 +8285,8 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
allocation = candidate;
|
||||||
allocationPriority = candidatePriority;
|
break;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allocation is null)
|
if (allocation is null)
|
||||||
@@ -8247,11 +8321,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
var shadow = allocation.Shadow.AsSpan(checked((int)guestOffset), guestBuffer.Length);
|
var shadow = allocation.Shadow.AsSpan(checked((int)guestOffset), guestBuffer.Length);
|
||||||
if (!source.SequenceEqual(shadow))
|
if (!source.SequenceEqual(shadow))
|
||||||
{
|
{
|
||||||
var sharedReadOnly = string.Equals(
|
if (!guestBuffer.Writable &&
|
||||||
allocation.QueueName,
|
|
||||||
SharedReadOnlyGuestBufferQueue,
|
|
||||||
StringComparison.Ordinal);
|
|
||||||
if (sharedReadOnly &&
|
|
||||||
(allocation.LastUseTimeline > _completedTimeline ||
|
(allocation.LastUseTimeline > _completedTimeline ||
|
||||||
IsGuestBufferAllocationReferencedByOpenBatch(allocation)))
|
IsGuestBufferAllocationReferencedByOpenBatch(allocation)))
|
||||||
{
|
{
|
||||||
@@ -8265,14 +8335,8 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
// an in-flight shader access. Retire prior users, publish their
|
// an in-flight shader access. Retire prior users, publish their
|
||||||
// dirty ranges to guest memory, then upload the current guest
|
// dirty ranges to guest memory, then upload the current guest
|
||||||
// bytes (which may be newer than the parser's captured array).
|
// bytes (which may be newer than the parser's captured array).
|
||||||
if (!sharedReadOnly)
|
WaitForGuestBufferAllocationForCpuVisibility(allocation);
|
||||||
{
|
WriteBackAllDirtyGuestBuffers();
|
||||||
// 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
|
// Populate the cached shadow copy first and write it out to the
|
||||||
// mapped allocation in one pass. The mapped memory is
|
// mapped allocation in one pass. The mapped memory is
|
||||||
// HOST_VISIBLE|HOST_COHERENT (write-combined on most drivers),
|
// HOST_VISIBLE|HOST_COHERENT (write-combined on most drivers),
|
||||||
@@ -8418,7 +8482,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var ranges = new List<(ulong Start, ulong End, bool Writable)>(buffers.Count);
|
var ranges = new List<(ulong Start, ulong End)>(buffers.Count);
|
||||||
foreach (var buffer in buffers)
|
foreach (var buffer in buffers)
|
||||||
{
|
{
|
||||||
if (buffer.BaseAddress == 0)
|
if (buffer.BaseAddress == 0)
|
||||||
@@ -8432,8 +8496,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
var paddedEnd = checked(buffer.BaseAddress + size + 3) & ~3UL;
|
var paddedEnd = checked(buffer.BaseAddress + size + 3) & ~3UL;
|
||||||
ranges.Add((
|
ranges.Add((
|
||||||
alignedStart,
|
alignedStart,
|
||||||
paddedEnd,
|
paddedEnd));
|
||||||
buffer.Writable && buffer.WriteBackToGuest));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ranges.Count == 0)
|
if (ranges.Count == 0)
|
||||||
@@ -8442,7 +8505,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
|
|
||||||
ranges.Sort(static (left, right) => left.Start.CompareTo(right.Start));
|
ranges.Sort(static (left, right) => left.Start.CompareTo(right.Start));
|
||||||
var merged = new List<(ulong Start, ulong End, bool Writable)>(ranges.Count);
|
var merged = new List<(ulong Start, ulong End)>(ranges.Count);
|
||||||
foreach (var range in ranges)
|
foreach (var range in ranges)
|
||||||
{
|
{
|
||||||
if (merged.Count == 0 || range.Start > merged[^1].End)
|
if (merged.Count == 0 || range.Start > merged[^1].End)
|
||||||
@@ -8454,25 +8517,18 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
var previous = merged[^1];
|
var previous = merged[^1];
|
||||||
merged[^1] = (
|
merged[^1] = (
|
||||||
previous.Start,
|
previous.Start,
|
||||||
Math.Max(previous.End, range.End),
|
Math.Max(previous.End, range.End));
|
||||||
previous.Writable || range.Writable);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var range in merged)
|
foreach (var range in merged)
|
||||||
{
|
{
|
||||||
EnsureGuestBufferAllocation(
|
EnsureGuestBufferAllocation(range.Start, range.End);
|
||||||
range.Start,
|
|
||||||
range.End,
|
|
||||||
range.Writable
|
|
||||||
? _activeGuestQueue.Name
|
|
||||||
: SharedReadOnlyGuestBufferQueue);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EnsureGuestBufferAllocation(
|
private void EnsureGuestBufferAllocation(
|
||||||
ulong requestedStart,
|
ulong requestedStart,
|
||||||
ulong requestedEnd,
|
ulong requestedEnd)
|
||||||
string queueName)
|
|
||||||
{
|
{
|
||||||
var start = requestedStart;
|
var start = requestedStart;
|
||||||
var end = requestedEnd;
|
var end = requestedEnd;
|
||||||
@@ -8481,10 +8537,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
{
|
{
|
||||||
overlaps = _guestBufferAllocations
|
overlaps = _guestBufferAllocations
|
||||||
.Where(allocation =>
|
.Where(allocation =>
|
||||||
string.Equals(
|
|
||||||
allocation.QueueName,
|
|
||||||
queueName,
|
|
||||||
StringComparison.Ordinal) &&
|
|
||||||
allocation.BaseAddress < end &&
|
allocation.BaseAddress < end &&
|
||||||
start < allocation.BaseAddress + allocation.Size)
|
start < allocation.BaseAddress + allocation.Size)
|
||||||
.ToList();
|
.ToList();
|
||||||
@@ -8521,7 +8573,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
WriteBackAllDirtyGuestBuffers();
|
WriteBackAllDirtyGuestBuffers();
|
||||||
}
|
}
|
||||||
|
|
||||||
var replacement = CreateGuestBufferAllocation(start, end, queueName);
|
var replacement = CreateGuestBufferAllocation(start, end);
|
||||||
foreach (var overlap in overlaps)
|
foreach (var overlap in overlaps)
|
||||||
{
|
{
|
||||||
_guestBufferAllocations.Remove(overlap);
|
_guestBufferAllocations.Remove(overlap);
|
||||||
@@ -8530,22 +8582,27 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
_guestBufferAllocations.Add(replacement);
|
_guestBufferAllocations.Add(replacement);
|
||||||
_guestBufferAllocations.Sort(static (left, right) =>
|
_guestBufferAllocations.Sort(static (left, right) =>
|
||||||
{
|
left.BaseAddress.CompareTo(right.BaseAddress));
|
||||||
var queueOrder = string.CompareOrdinal(left.QueueName, right.QueueName);
|
UpdateGuestBufferCacheMetric();
|
||||||
return queueOrder != 0
|
|
||||||
? queueOrder
|
|
||||||
: left.BaseAddress.CompareTo(right.BaseAddress);
|
|
||||||
});
|
|
||||||
TraceVulkanShader(
|
TraceVulkanShader(
|
||||||
$"vk.guest_buffer_allocation queue={queueName} " +
|
$"vk.guest_buffer_allocation base=0x{start:X16} bytes={replacement.Size} " +
|
||||||
$"base=0x{start:X16} bytes={replacement.Size} " +
|
|
||||||
$"merged={overlaps.Count}");
|
$"merged={overlaps.Count}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpdateGuestBufferCacheMetric()
|
||||||
|
{
|
||||||
|
var bytes = 0UL;
|
||||||
|
foreach (var allocation in _guestBufferAllocations)
|
||||||
|
{
|
||||||
|
bytes = checked(bytes + allocation.Size);
|
||||||
|
}
|
||||||
|
|
||||||
|
PerfOverlay.SetGuestBufferCacheBytes(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
private GuestBufferAllocation CreateGuestBufferAllocation(
|
private GuestBufferAllocation CreateGuestBufferAllocation(
|
||||||
ulong start,
|
ulong start,
|
||||||
ulong end,
|
ulong end)
|
||||||
string queueName)
|
|
||||||
{
|
{
|
||||||
var size = checked(end - start);
|
var size = checked(end - start);
|
||||||
if (size == 0 || size > int.MaxValue)
|
if (size == 0 || size > int.MaxValue)
|
||||||
@@ -8571,7 +8628,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
$"SharpEmu guest VA 0x{start:X16}-0x{end:X16}");
|
$"SharpEmu guest VA 0x{start:X16}-0x{end:X16}");
|
||||||
return new GuestBufferAllocation
|
return new GuestBufferAllocation
|
||||||
{
|
{
|
||||||
QueueName = queueName,
|
|
||||||
BaseAddress = start,
|
BaseAddress = start,
|
||||||
Size = size,
|
Size = size,
|
||||||
Buffer = buffer,
|
Buffer = buffer,
|
||||||
@@ -9577,15 +9633,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
MarkSampledImagesInitialized(resources);
|
MarkSampledImagesInitialized(resources);
|
||||||
MarkStorageImagesInitialized(resources, traceContents: false);
|
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(
|
TraceVulkanShader(
|
||||||
$"vk.compute_dispatch groups={work.GroupCountX}x" +
|
$"vk.compute_dispatch groups={work.GroupCountX}x" +
|
||||||
$"{work.GroupCountY}x{work.GroupCountZ} " +
|
$"{work.GroupCountY}x{work.GroupCountZ} " +
|
||||||
@@ -9815,7 +9862,9 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
private static void MarkGuestBufferDirty(
|
private static void MarkGuestBufferDirty(
|
||||||
GuestBufferAllocation allocation,
|
GuestBufferAllocation allocation,
|
||||||
ulong offset,
|
ulong offset,
|
||||||
ulong length)
|
ulong length,
|
||||||
|
string queueName,
|
||||||
|
ulong timeline)
|
||||||
{
|
{
|
||||||
if (length == 0)
|
if (length == 0)
|
||||||
{
|
{
|
||||||
@@ -9827,6 +9876,11 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
for (var index = allocation.DirtyRanges.Count - 1; index >= 0; index--)
|
for (var index = allocation.DirtyRanges.Count - 1; index >= 0; index--)
|
||||||
{
|
{
|
||||||
var existing = allocation.DirtyRanges[index];
|
var existing = allocation.DirtyRanges[index];
|
||||||
|
if (!string.Equals(existing.QueueName, queueName, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var existingEnd = existing.Offset + existing.Length;
|
var existingEnd = existing.Offset + existing.Length;
|
||||||
if (end < existing.Offset || existingEnd < start)
|
if (end < existing.Offset || existingEnd < start)
|
||||||
{
|
{
|
||||||
@@ -9835,10 +9889,12 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
start = Math.Min(start, existing.Offset);
|
start = Math.Min(start, existing.Offset);
|
||||||
end = Math.Max(end, existingEnd);
|
end = Math.Max(end, existingEnd);
|
||||||
|
timeline = Math.Max(timeline, existing.Timeline);
|
||||||
allocation.DirtyRanges.RemoveAt(index);
|
allocation.DirtyRanges.RemoveAt(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
allocation.DirtyRanges.Add(new DirtyGuestBufferRange(start, end - start));
|
allocation.DirtyRanges.Add(
|
||||||
|
new DirtyGuestBufferRange(start, end - start, queueName, timeline));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WriteBackAllDirtyGuestBuffers(string? queueName = null)
|
private void WriteBackAllDirtyGuestBuffers(string? queueName = null)
|
||||||
@@ -9851,33 +9907,51 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
foreach (var allocation in _guestBufferAllocations)
|
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--)
|
for (var index = allocation.DirtyRanges.Count - 1; index >= 0; index--)
|
||||||
{
|
{
|
||||||
var range = allocation.DirtyRanges[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)
|
if (range.Length == 0 || range.Length > int.MaxValue)
|
||||||
{
|
{
|
||||||
continue;
|
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>(
|
var mappedBytes = new ReadOnlySpan<byte>(
|
||||||
(void*)(allocation.Mapped + checked((nint)range.Offset)),
|
(void*)(allocation.Mapped + checked((nint)range.Offset)),
|
||||||
checked((int)range.Length));
|
checked((int)range.Length));
|
||||||
@@ -9907,6 +9981,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
const int pageSize = 4096;
|
const int pageSize = 4096;
|
||||||
const int unreadableMergeGap = 16;
|
const int unreadableMergeGap = 16;
|
||||||
var livePageBuffer = GuestDataPool.Shared.Rent(pageSize);
|
var livePageBuffer = GuestDataPool.Shared.Rent(pageSize);
|
||||||
|
var mappedPageBuffer = GuestDataPool.Shared.Rent(pageSize);
|
||||||
var pageRuns = new List<(int Start, int Length)>(64);
|
var pageRuns = new List<(int Start, int Length)>(64);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -9915,35 +9990,49 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
pageStart += pageSize)
|
pageStart += pageSize)
|
||||||
{
|
{
|
||||||
var pageEnd = Math.Min(pageStart + pageSize, mappedBytes.Length);
|
var pageEnd = Math.Min(pageStart + pageSize, mappedBytes.Length);
|
||||||
pageRuns.Clear();
|
var pageLength = pageEnd - pageStart;
|
||||||
var cursor = pageStart;
|
var mappedPageSource = mappedBytes.Slice(pageStart, pageLength);
|
||||||
while (cursor < pageEnd)
|
var shadowPage = shadowBytes.Slice(pageStart, pageLength);
|
||||||
|
if (mappedPageSource.SequenceEqual(shadowPage))
|
||||||
{
|
{
|
||||||
while (cursor < pageEnd &&
|
continue;
|
||||||
mappedBytes[cursor] == shadowBytes[cursor])
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
while (cursor < pageLength &&
|
||||||
|
mappedPage[cursor] == shadowPage[cursor])
|
||||||
{
|
{
|
||||||
cursor++;
|
cursor++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cursor == pageEnd)
|
if (cursor == pageLength)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
var runStart = cursor;
|
var runStart = cursor;
|
||||||
while (cursor < pageEnd &&
|
while (cursor < pageLength &&
|
||||||
mappedBytes[cursor] != shadowBytes[cursor])
|
mappedPage[cursor] != shadowPage[cursor])
|
||||||
{
|
{
|
||||||
cursor++;
|
cursor++;
|
||||||
}
|
}
|
||||||
|
|
||||||
var runLength = cursor - runStart;
|
var runLength = cursor - runStart;
|
||||||
pageRuns.Add((runStart, runLength));
|
pageRuns.Add((pageStart + runStart, runLength));
|
||||||
changedRuns++;
|
changedRuns++;
|
||||||
changedBytes += (ulong)runLength;
|
changedBytes += (ulong)runLength;
|
||||||
if (firstChangedOffset < 0)
|
if (firstChangedOffset < 0)
|
||||||
{
|
{
|
||||||
firstChangedOffset = runStart;
|
firstChangedOffset = pageStart + runStart;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9953,13 +10042,12 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
|
|
||||||
changedPages++;
|
changedPages++;
|
||||||
var pageLength = pageEnd - pageStart;
|
|
||||||
var livePage = livePageBuffer.AsSpan(0, pageLength);
|
var livePage = livePageBuffer.AsSpan(0, pageLength);
|
||||||
if (memory.TryRead(guestAddress + (ulong)pageStart, livePage))
|
if (memory.TryRead(guestAddress + (ulong)pageStart, livePage))
|
||||||
{
|
{
|
||||||
foreach (var run in pageRuns)
|
foreach (var run in pageRuns)
|
||||||
{
|
{
|
||||||
mappedBytes.Slice(run.Start, run.Length).CopyTo(
|
mappedPage.Slice(run.Start - pageStart, run.Length).CopyTo(
|
||||||
livePage.Slice(run.Start - pageStart, run.Length));
|
livePage.Slice(run.Start - pageStart, run.Length));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9967,7 +10055,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
{
|
{
|
||||||
foreach (var run in pageRuns)
|
foreach (var run in pageRuns)
|
||||||
{
|
{
|
||||||
mappedBytes.Slice(run.Start, run.Length).CopyTo(
|
mappedPage.Slice(run.Start - pageStart, run.Length).CopyTo(
|
||||||
shadowBytes.Slice(run.Start, run.Length));
|
shadowBytes.Slice(run.Start, run.Length));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9982,7 +10070,9 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
MarkGuestBufferDirty(
|
MarkGuestBufferDirty(
|
||||||
allocation,
|
allocation,
|
||||||
range.Offset + (ulong)run.Start,
|
range.Offset + (ulong)run.Start,
|
||||||
(ulong)run.Length);
|
(ulong)run.Length,
|
||||||
|
range.QueueName,
|
||||||
|
range.Timeline);
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
@@ -10018,7 +10108,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
overlayIndex++)
|
overlayIndex++)
|
||||||
{
|
{
|
||||||
var run = pageRuns[overlayIndex];
|
var run = pageRuns[overlayIndex];
|
||||||
mappedBytes.Slice(run.Start, run.Length).CopyTo(
|
mappedPage.Slice(run.Start - pageStart, run.Length).CopyTo(
|
||||||
mergedLive.Slice(
|
mergedLive.Slice(
|
||||||
run.Start - mergedStart,
|
run.Start - mergedStart,
|
||||||
run.Length));
|
run.Length));
|
||||||
@@ -10034,7 +10124,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
overlayIndex++)
|
overlayIndex++)
|
||||||
{
|
{
|
||||||
var run = pageRuns[overlayIndex];
|
var run = pageRuns[overlayIndex];
|
||||||
mappedBytes.Slice(run.Start, run.Length).CopyTo(
|
mappedPage.Slice(run.Start - pageStart, run.Length).CopyTo(
|
||||||
shadowBytes.Slice(run.Start, run.Length));
|
shadowBytes.Slice(run.Start, run.Length));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10051,7 +10141,9 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
exactIndex++)
|
exactIndex++)
|
||||||
{
|
{
|
||||||
var run = pageRuns[exactIndex];
|
var run = pageRuns[exactIndex];
|
||||||
var changed = mappedBytes.Slice(run.Start, run.Length);
|
var changed = mappedPage.Slice(
|
||||||
|
run.Start - pageStart,
|
||||||
|
run.Length);
|
||||||
fallbackWrites++;
|
fallbackWrites++;
|
||||||
if (memory.TryWrite(
|
if (memory.TryWrite(
|
||||||
guestAddress + (ulong)run.Start,
|
guestAddress + (ulong)run.Start,
|
||||||
@@ -10068,7 +10160,9 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
MarkGuestBufferDirty(
|
MarkGuestBufferDirty(
|
||||||
allocation,
|
allocation,
|
||||||
range.Offset + (ulong)run.Start,
|
range.Offset + (ulong)run.Start,
|
||||||
(ulong)run.Length);
|
(ulong)run.Length,
|
||||||
|
range.QueueName,
|
||||||
|
range.Timeline);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10077,6 +10171,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
GuestDataPool.Shared.Return(livePageBuffer);
|
GuestDataPool.Shared.Return(livePageBuffer);
|
||||||
|
GuestDataPool.Shared.Return(mappedPageBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
var probe = mappedBytes[..Math.Min(mappedBytes.Length, 256)];
|
var probe = mappedBytes[..Math.Min(mappedBytes.Length, 256)];
|
||||||
@@ -10346,6 +10441,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
GuestDepthResource? depth = null;
|
GuestDepthResource? depth = null;
|
||||||
DepthFramebufferResource? depthFramebuffer = null;
|
DepthFramebufferResource? depthFramebuffer = null;
|
||||||
|
var clearDepthSeparately = false;
|
||||||
if (ShouldAttachGuestDepth(
|
if (ShouldAttachGuestDepth(
|
||||||
work.DepthTarget,
|
work.DepthTarget,
|
||||||
draw.RenderState.Depth) &&
|
draw.RenderState.Depth) &&
|
||||||
@@ -10373,12 +10469,26 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
depth.GuestClearDepth = effectiveDepthTarget.ClearDepth;
|
depth.GuestClearDepth = effectiveDepthTarget.ClearDepth;
|
||||||
depth.ClearDepth = effectiveDepthTarget.ClearDepth;
|
depth.ClearDepth = effectiveDepthTarget.ClearDepth;
|
||||||
}
|
}
|
||||||
if (targets.Length == 1)
|
clearDepthSeparately = clearDepthForDraw &&
|
||||||
|
(depth.Width < firstTarget.Width ||
|
||||||
|
depth.Height < firstTarget.Height);
|
||||||
|
if (targets.Length == 1 && !clearDepthSeparately)
|
||||||
{
|
{
|
||||||
depthFramebuffer = GetOrCreateDepthFramebuffer(firstTarget, depth);
|
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)
|
if (clearDepthForDraw)
|
||||||
{
|
{
|
||||||
// DB_RENDER_CONTROL.DEPTH_CLEAR_ENABLE makes this a DB
|
// DB_RENDER_CONTROL.DEPTH_CLEAR_ENABLE makes this a DB
|
||||||
@@ -10412,17 +10522,18 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
var framebuffer = depthFramebuffer?.Framebuffer ?? firstTarget.Framebuffer;
|
var framebuffer = depthFramebuffer?.Framebuffer ?? firstTarget.Framebuffer;
|
||||||
if (targets.Length > 1)
|
if (targets.Length > 1)
|
||||||
{
|
{
|
||||||
|
var attachedDepth = clearDepthSeparately ? null : depth;
|
||||||
(renderPass, framebuffer) = CreateRenderPassAndFramebuffer(
|
(renderPass, framebuffer) = CreateRenderPassAndFramebuffer(
|
||||||
formats,
|
formats,
|
||||||
targets.Select(target => target.MipViews.Length > 0
|
targets.Select(target => target.MipViews.Length > 0
|
||||||
? target.MipViews[0]
|
? target.MipViews[0]
|
||||||
: target.View).ToArray(),
|
: target.View).ToArray(),
|
||||||
firstTarget.Width,
|
extent.Width,
|
||||||
firstTarget.Height,
|
extent.Height,
|
||||||
targets.Select(target =>
|
targets.Select(target =>
|
||||||
target.Initialized || target.InitialUploadPending).ToArray(),
|
target.Initialized || target.InitialUploadPending).ToArray(),
|
||||||
depth,
|
attachedDepth,
|
||||||
depth?.Initialized == true && !clearDepthForDraw);
|
attachedDepth?.Initialized == true && !clearDepthForDraw);
|
||||||
transientRenderPass = renderPass;
|
transientRenderPass = renderPass;
|
||||||
transientFramebuffer = framebuffer;
|
transientFramebuffer = framebuffer;
|
||||||
}
|
}
|
||||||
@@ -10433,8 +10544,8 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
formats,
|
formats,
|
||||||
extent,
|
extent,
|
||||||
targets,
|
targets,
|
||||||
hasDepthAttachment: depth is not null,
|
hasDepthAttachment: depth is not null && !clearDepthSeparately,
|
||||||
feedbackDepth: depth);
|
feedbackDepth: clearDepthSeparately ? null : depth);
|
||||||
resources.TransientRenderPass = transientRenderPass;
|
resources.TransientRenderPass = transientRenderPass;
|
||||||
resources.TransientFramebuffer = transientFramebuffer;
|
resources.TransientFramebuffer = transientFramebuffer;
|
||||||
transientRenderPass = default;
|
transientRenderPass = default;
|
||||||
@@ -10454,6 +10565,10 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
submitted = true;
|
submitted = true;
|
||||||
|
|
||||||
BeginDebugLabel(_commandBuffer, resources.DebugName);
|
BeginDebugLabel(_commandBuffer, resources.DebugName);
|
||||||
|
if (clearDepthSeparately && depth is not null)
|
||||||
|
{
|
||||||
|
RecordStandaloneGuestDepthClear(depth);
|
||||||
|
}
|
||||||
var hasStorageImages = false;
|
var hasStorageImages = false;
|
||||||
foreach (var texture in resources.Textures)
|
foreach (var texture in resources.Textures)
|
||||||
{
|
{
|
||||||
@@ -10517,6 +10632,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
toColorAttachments);
|
toColorAttachments);
|
||||||
|
|
||||||
if (depth is not null &&
|
if (depth is not null &&
|
||||||
|
!clearDepthSeparately &&
|
||||||
depth.Layout == ImageLayout.ShaderReadOnlyOptimal)
|
depth.Layout == ImageLayout.ShaderReadOnlyOptimal)
|
||||||
{
|
{
|
||||||
var toDepthAttachment = new ImageMemoryBarrier
|
var toDepthAttachment = new ImageMemoryBarrier
|
||||||
@@ -10554,7 +10670,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
framebuffer,
|
framebuffer,
|
||||||
extent,
|
extent,
|
||||||
colorAttachmentCount: targets.Length,
|
colorAttachmentCount: targets.Length,
|
||||||
hasDepthAttachment: depth is not null,
|
hasDepthAttachment: depth is not null && !clearDepthSeparately,
|
||||||
clearDepth: depth?.ClearDepth ?? 1f);
|
clearDepth: depth?.ClearDepth ?? 1f);
|
||||||
RecordTranslatedDrawInPass(resources, extent);
|
RecordTranslatedDrawInPass(resources, extent);
|
||||||
_vk.CmdEndRenderPass(_commandBuffer);
|
_vk.CmdEndRenderPass(_commandBuffer);
|
||||||
@@ -10610,7 +10726,10 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
if (depth is not null)
|
if (depth is not null)
|
||||||
{
|
{
|
||||||
depth.Initialized = true;
|
depth.Initialized = true;
|
||||||
|
if (!clearDepthSeparately)
|
||||||
|
{
|
||||||
depth.Layout = ImageLayout.DepthStencilAttachmentOptimal;
|
depth.Layout = ImageLayout.DepthStencilAttachmentOptimal;
|
||||||
|
}
|
||||||
if (clearDepthForDraw)
|
if (clearDepthForDraw)
|
||||||
{
|
{
|
||||||
depth.InitializationSource = "guest-depth-clear";
|
depth.InitializationSource = "guest-depth-clear";
|
||||||
@@ -11624,14 +11743,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return existing;
|
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 attachmentView = color.MipViews.Length > 0 ? color.MipViews[0] : color.View;
|
||||||
var loadRenderPass = CreateDepthRenderPass(
|
var loadRenderPass = CreateDepthRenderPass(
|
||||||
color.Format,
|
color.Format,
|
||||||
@@ -11658,8 +11769,8 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
RenderPass = loadRenderPass,
|
RenderPass = loadRenderPass,
|
||||||
AttachmentCount = 2,
|
AttachmentCount = 2,
|
||||||
PAttachments = attachments,
|
PAttachments = attachments,
|
||||||
Width = color.Width,
|
Width = Math.Min(color.Width, depth.Width),
|
||||||
Height = color.Height,
|
Height = Math.Min(color.Height, depth.Height),
|
||||||
Layers = 1,
|
Layers = 1,
|
||||||
};
|
};
|
||||||
Check(
|
Check(
|
||||||
@@ -12155,6 +12266,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
EvictDirtyCachedTextures();
|
EvictDirtyCachedTextures();
|
||||||
var completedWork = 0;
|
var completedWork = 0;
|
||||||
|
HashSet<string>? deferredOrderedQueues = null;
|
||||||
var renderWorkDeadline = _renderWorkBudgetTicks > 0
|
var renderWorkDeadline = _renderWorkBudgetTicks > 0
|
||||||
? System.Diagnostics.Stopwatch.GetTimestamp() + _renderWorkBudgetTicks
|
? System.Diagnostics.Stopwatch.GetTimestamp() + _renderWorkBudgetTicks
|
||||||
: long.MaxValue;
|
: long.MaxValue;
|
||||||
@@ -12172,7 +12284,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryTakeGuestWork(out var pendingGuestWork))
|
if (!TryTakeGuestWork(out var pendingGuestWork, deferredOrderedQueues))
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -12199,6 +12311,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
_enqueueAsImmediateQueueFollowup = true;
|
_enqueueAsImmediateQueueFollowup = true;
|
||||||
_immediateFollowupTail = null;
|
_immediateFollowupTail = null;
|
||||||
var work = pendingGuestWork.Work;
|
var work = pendingGuestWork.Work;
|
||||||
|
var deferGuestWork = false;
|
||||||
|
|
||||||
var traceWork = ShouldTracePresentedGuestImageContentsForDiagnostics();
|
var traceWork = ShouldTracePresentedGuestImageContentsForDiagnostics();
|
||||||
var workStart = traceWork ? System.Diagnostics.Stopwatch.GetTimestamp() : 0L;
|
var workStart = traceWork ? System.Diagnostics.Stopwatch.GetTimestamp() : 0L;
|
||||||
@@ -12226,7 +12339,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
ExecuteGuestImageWrite(guestImageWrite);
|
ExecuteGuestImageWrite(guestImageWrite);
|
||||||
break;
|
break;
|
||||||
case VulkanOrderedGuestAction orderedAction:
|
case VulkanOrderedGuestAction orderedAction:
|
||||||
ExecuteOrderedGuestAction(orderedAction);
|
deferGuestWork = !TryExecuteOrderedGuestAction(orderedAction);
|
||||||
break;
|
break;
|
||||||
case VulkanOrderedGuestFlip orderedFlip:
|
case VulkanOrderedGuestFlip orderedFlip:
|
||||||
ExecuteOrderedGuestFlip(orderedFlip);
|
ExecuteOrderedGuestFlip(orderedFlip);
|
||||||
@@ -12237,13 +12350,22 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
{
|
||||||
|
if (!deferGuestWork || !RequeueGuestWorkFront(pendingGuestWork))
|
||||||
{
|
{
|
||||||
CompleteGuestWork(pendingGuestWork);
|
CompleteGuestWork(pendingGuestWork);
|
||||||
|
}
|
||||||
_enqueueAsImmediateQueueFollowup = false;
|
_enqueueAsImmediateQueueFollowup = false;
|
||||||
_immediateFollowupTail = null;
|
_immediateFollowupTail = null;
|
||||||
Volatile.Write(ref _executingGuestWorkSequence, 0);
|
Volatile.Write(ref _executingGuestWorkSequence, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (deferGuestWork)
|
||||||
|
{
|
||||||
|
deferredOrderedQueues ??= new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
deferredOrderedQueues.Add(pendingGuestWork.Queue.Name);
|
||||||
|
}
|
||||||
|
|
||||||
if (workStart != 0)
|
if (workStart != 0)
|
||||||
{
|
{
|
||||||
var elapsedMs = (System.Diagnostics.Stopwatch.GetTimestamp() - workStart)
|
var elapsedMs = (System.Diagnostics.Stopwatch.GetTimestamp() - workStart)
|
||||||
@@ -13200,6 +13322,79 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
depth.Layout = ImageLayout.ShaderReadOnlyOptimal;
|
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(
|
private void RecordRenderTargetFeedbackSnapshots(
|
||||||
TranslatedDrawResources resources,
|
TranslatedDrawResources resources,
|
||||||
PipelineStageFlags shaderStage)
|
PipelineStageFlags shaderStage)
|
||||||
@@ -14965,6 +15160,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
DestroyGuestBufferAllocation(allocation);
|
DestroyGuestBufferAllocation(allocation);
|
||||||
}
|
}
|
||||||
_guestBufferAllocations.Clear();
|
_guestBufferAllocations.Clear();
|
||||||
|
PerfOverlay.SetGuestBufferCacheBytes(0);
|
||||||
_hostBufferPool.Dispose();
|
_hostBufferPool.Dispose();
|
||||||
foreach (var guestImage in _guestImages.Values)
|
foreach (var guestImage in _guestImages.Values)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -953,22 +953,13 @@ public static partial class Gen5SpirvTranslator
|
|||||||
case "VPkMulF16":
|
case "VPkMulF16":
|
||||||
case "VPkMinF16":
|
case "VPkMinF16":
|
||||||
case "VPkMaxF16":
|
case "VPkMaxF16":
|
||||||
|
case "VPkFmaF16":
|
||||||
if (!TryEmitPackedF16(instruction, out result, out error))
|
if (!TryEmitPackedF16(instruction, out result, out error))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
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:
|
default:
|
||||||
error = $"unsupported vector opcode {instruction.Opcode}";
|
error = $"unsupported vector opcode {instruction.Opcode}";
|
||||||
return false;
|
return false;
|
||||||
@@ -1008,8 +999,9 @@ public static partial class Gen5SpirvTranslator
|
|||||||
// even. For add and mul this is bit-exact to a true f16 op (the f32 result
|
// 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
|
// 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
|
// fits in f32 exactly). min/max carry no rounding, so they are exact once the
|
||||||
// conversions are. v_pk_fma_f16 is intentionally not routed here because a
|
// conversions are. v_pk_fma_f16 cannot be reproduced by a plain f32
|
||||||
// fused f16 FMA cannot be reproduced by an f32 multiply-add plus a pack.
|
// multiply-add plus a pack (that double-rounds), so it goes through the
|
||||||
|
// round-to-odd sequence in EmitPackedF16FusedMultiplyAdd instead.
|
||||||
private bool TryEmitPackedF16(
|
private bool TryEmitPackedF16(
|
||||||
Gen5ShaderInstruction instruction,
|
Gen5ShaderInstruction instruction,
|
||||||
out uint result,
|
out uint result,
|
||||||
@@ -1029,7 +1021,8 @@ public static partial class Gen5SpirvTranslator
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var index = 0; index < 2; index++)
|
var sourceCount = instruction.Opcode == "VPkFmaF16" ? 3 : 2;
|
||||||
|
for (var index = 0; index < sourceCount; index++)
|
||||||
{
|
{
|
||||||
var source = instruction.Sources[index];
|
var source = instruction.Sources[index];
|
||||||
if (source.Kind is not (Gen5OperandKind.VectorRegister or Gen5OperandKind.ScalarRegister))
|
if (source.Kind is not (Gen5OperandKind.VectorRegister or Gen5OperandKind.ScalarRegister))
|
||||||
@@ -1054,6 +1047,12 @@ public static partial class Gen5SpirvTranslator
|
|||||||
{
|
{
|
||||||
var left = EmitPackedF16Operand(instruction, control, 0, highLane);
|
var left = EmitPackedF16Operand(instruction, control, 0, highLane);
|
||||||
var right = EmitPackedF16Operand(instruction, control, 1, 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
|
var value = instruction.Opcode switch
|
||||||
{
|
{
|
||||||
"VPkAddF16" => _module.AddInstruction(SpirvOp.FAdd, _floatType, left, right),
|
"VPkAddF16" => _module.AddInstruction(SpirvOp.FAdd, _floatType, left, right),
|
||||||
@@ -1065,6 +1064,75 @@ public static partial class Gen5SpirvTranslator
|
|||||||
return EmitFloatToHalf(Bitcast(_uintType, value));
|
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),
|
// 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).
|
// widens it exactly to f32 and applies the lane's negate modifier (neg_lo / neg_hi).
|
||||||
private uint EmitPackedF16Operand(
|
private uint EmitPackedF16Operand(
|
||||||
|
|||||||
@@ -238,6 +238,7 @@ public enum SpirvDecoration : uint
|
|||||||
Binding = 33,
|
Binding = 33,
|
||||||
DescriptorSet = 34,
|
DescriptorSet = 34,
|
||||||
Offset = 35,
|
Offset = 35,
|
||||||
|
NoContraction = 42,
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum SpirvBuiltIn : uint
|
public enum SpirvBuiltIn : uint
|
||||||
|
|||||||
@@ -2350,7 +2350,8 @@ public static class Gen5ShaderScalarEvaluator
|
|||||||
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||||
{
|
{
|
||||||
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||||
if (!ctx.Memory.TryRead(address, bytes))
|
if (!ctx.Memory.TryRead(address, bytes) &&
|
||||||
|
FallbackMemoryReader?.Invoke(address, bytes) != true)
|
||||||
{
|
{
|
||||||
value = 0;
|
value = 0;
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
// 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,26 @@
|
|||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
// 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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.Random;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Tests.Random;
|
||||||
|
|
||||||
|
public sealed class RandomExportsTests
|
||||||
|
{
|
||||||
|
private const ulong BaseAddress = 0x1000;
|
||||||
|
private const int RandomErrorInvalid = unchecked((int)0x817C0016);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetRandomNumberWritesRequestedBytes()
|
||||||
|
{
|
||||||
|
var memory = new FakeCpuMemory(BaseAddress, 64);
|
||||||
|
var ctx = CreateContext(memory, BaseAddress, 64);
|
||||||
|
|
||||||
|
Assert.Equal(0, RandomExports.RandomGetRandomNumber(ctx));
|
||||||
|
|
||||||
|
var bytes = new byte[64];
|
||||||
|
Assert.True(memory.TryRead(BaseAddress, bytes));
|
||||||
|
Assert.NotEqual(new byte[64], bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0, 1)]
|
||||||
|
[InlineData(BaseAddress, 65)]
|
||||||
|
public void GetRandomNumberRejectsInvalidArguments(ulong address, ulong size)
|
||||||
|
{
|
||||||
|
var memory = new FakeCpuMemory(BaseAddress, 64);
|
||||||
|
var ctx = CreateContext(memory, address, size);
|
||||||
|
|
||||||
|
Assert.Equal(RandomErrorInvalid, RandomExports.RandomGetRandomNumber(ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetRandomNumberAcceptsEmptyRequest()
|
||||||
|
{
|
||||||
|
var memory = new FakeCpuMemory(BaseAddress, 64);
|
||||||
|
var ctx = CreateContext(memory, 0, 0);
|
||||||
|
|
||||||
|
Assert.Equal(0, RandomExports.RandomGetRandomNumber(ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetRandomNumberReportsUnmappedDestination()
|
||||||
|
{
|
||||||
|
var memory = new FakeCpuMemory(BaseAddress, 64);
|
||||||
|
var ctx = CreateContext(memory, BaseAddress + 64, 1);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT,
|
||||||
|
RandomExports.RandomGetRandomNumber(ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CpuContext CreateContext(
|
||||||
|
ICpuMemory memory,
|
||||||
|
ulong destination,
|
||||||
|
ulong size)
|
||||||
|
{
|
||||||
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||||
|
ctx[CpuRegister.Rdi] = destination;
|
||||||
|
ctx[CpuRegister.Rsi] = size;
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,11 @@
|
|||||||
// [2] v_mul_lo_i32 -> low 32 bits of the same product
|
// [2] v_mul_lo_i32 -> low 32 bits of the same product
|
||||||
// [3] store attempted with EXEC=0 -> must NOT land (sentinel remains)
|
// [3] store attempted with EXEC=0 -> must NOT land (sentinel remains)
|
||||||
// [4] store after EXEC restored -> 1.5f (0x3FC00000)
|
// [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.
|
// Every other word of the buffer must still hold the sentinel afterwards.
|
||||||
//
|
//
|
||||||
// Creating the compute pipeline doubles as a driver-acceptance check for the
|
// Creating the compute pipeline doubles as a driver-acceptance check for the
|
||||||
@@ -35,6 +40,12 @@ var expectedHi = (uint)(product >> 32);
|
|||||||
var expectedLo = (uint)product;
|
var expectedLo = (uint)product;
|
||||||
var expectedRestored = BitConverter.SingleToUInt32Bits(1.5f);
|
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
|
unsafe
|
||||||
{
|
{
|
||||||
var spvPath = args.Length > 0
|
var spvPath = args.Length > 0
|
||||||
@@ -352,6 +363,8 @@ unsafe
|
|||||||
("v_mul_lo_i32 lo(0x7FFFFFFF*0x10003)", words[2], expectedLo),
|
("v_mul_lo_i32 lo(0x7FFFFFFF*0x10003)", words[2], expectedLo),
|
||||||
("exec=0 store suppressed (offset 12 sentinel)", words[3], Sentinel),
|
("exec=0 store suppressed (offset 12 sentinel)", words[3], Sentinel),
|
||||||
("store after exec restore (offset 16)", words[4], expectedRestored),
|
("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;
|
var failures = 0;
|
||||||
foreach (var (name, actual, expected) in results)
|
foreach (var (name, actual, expected) in results)
|
||||||
|
|||||||
@@ -42,6 +42,23 @@ const ulong ProgramAddress = 0x100000;
|
|||||||
0xD56C0005, 0x00020501, // v_mul_hi_i32 v5, v1, v2
|
0xD56C0005, 0x00020501, // v_mul_hi_i32 v5, v1, v2
|
||||||
0xBF810000, // s_endpgm
|
0xBF810000, // s_endpgm
|
||||||
]),
|
]),
|
||||||
|
// 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, [
|
("mrt", true, [
|
||||||
0x7E0002FF, 0x3F800000, // v_mov_b32 v0, 1.0f
|
0x7E0002FF, 0x3F800000, // v_mov_b32 v0, 1.0f
|
||||||
0x7E0202FF, 0x00000000, // v_mov_b32 v1, 0.0f
|
0x7E0202FF, 0x00000000, // v_mov_b32 v1, 0.0f
|
||||||
@@ -115,7 +132,10 @@ const ulong ProgramAddress = 0x100000;
|
|||||||
// Executable end-to-end test: compute with real ALU instructions, then
|
// Executable end-to-end test: compute with real ALU instructions, then
|
||||||
// buffer_store_dword results to guestBuffers[0] at offsets 0/4/8, prove
|
// 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 a store with EXEC=0 does not land (offset 12 stays sentinel), and
|
||||||
// that stores work again after EXEC is restored (offset 16).
|
// 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.
|
||||||
("exec", true, [
|
("exec", true, [
|
||||||
0xBFA10001, // s_clause 0x1 (hint no-op in an executed program, needs #108)
|
0xBFA10001, // s_clause 0x1 (hint no-op in an executed program, needs #108)
|
||||||
0x7E0002FF, 0x3FC00000, // v_mov_b32 v0, 1.5f
|
0x7E0002FF, 0x3FC00000, // v_mov_b32 v0, 1.5f
|
||||||
@@ -133,6 +153,13 @@ const ulong ProgramAddress = 0x100000;
|
|||||||
0xE070000C, 0x80020200, // buffer_store_dword v2, off, s[8:11], 0 offset:12 (masked, must not land)
|
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
|
0xBEFE03C1, // s_mov_b32 exec_lo, -1 -> lane active again
|
||||||
0xE0700010, 0x80020000, // buffer_store_dword v0, off, s[8:11], 0 offset:16
|
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
|
0xBF810000, // s_endpgm
|
||||||
]),
|
]),
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user